feat: 新增 MCP 只读查询能力,包含多表分页与批量查询
新增 AgentInfo、CustomerInfo、ProductInfo 等多主数据的游标分页查询 新增应收/应付/开票/收款等财务单据的分页查询 新增库存、发货、项目等业务数据的批量与分页查询 新增 MCP 工具鉴权、异常处理、格式工具类 新增 OmsPurchaseOrderMap 绑定关系查询 新增 MCP 时间区间工具类dev_1.0.3
parent
a14993e1b8
commit
c81faba391
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,70 @@
|
|||
-- =====================================================================
|
||||
-- MCP 只读数据工具 —— 索引脚本(对应方案第 6 章 / 16.4)
|
||||
-- 适用:MySQL 8.0+
|
||||
--
|
||||
-- 【2026-09-23 实测更新】在 oms_test 上核对 information_schema 后发现:
|
||||
-- • P0-1 oms_inventory_outer.idx_outer_code(outer_code) —— 已存在,无需执行
|
||||
-- • P0-2 oms_inventory_outer_detail.idx_outer_code(outer_code) —— 已存在,无需执行
|
||||
-- • P1-1 oms_inventory_inner.idx_order_code(order_code) —— 已存在,无需执行
|
||||
-- • P1-2 oms_purchase_order.idx_vendor_id(vendor_id) —— 已存在,无需执行
|
||||
-- • P1-3 oms_inventory_info 覆盖索引 —— 缺失,已在 oms_test 执行并实测收益
|
||||
-- (建索引耗时 1.3s;按产品聚合 141.8ms → 66.6ms;EXPLAIN 显示 Using index,免回表)
|
||||
--
|
||||
-- 因此本脚本默认全部注释,仅作为「生产执行前的核对与补建」清单使用。
|
||||
-- 执行前请先跑文末的核对语句,确认目标索引是否缺失。
|
||||
-- =====================================================================
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- P0 必加(inventory_flow 出库链路依赖;若缺失将导致按出库单号全表扫描)
|
||||
-- 实测:oms_test 已存在
|
||||
-- ---------------------------------------------------------------------
|
||||
-- ALTER TABLE oms_inventory_outer ADD INDEX idx_outer_code (outer_code), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_inventory_outer_detail ADD INDEX idx_outer_code (outer_code), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- P1 建议
|
||||
-- 实测:P1-1 / P1-2 在 oms_test 已存在
|
||||
-- ---------------------------------------------------------------------
|
||||
-- ALTER TABLE oms_inventory_inner ADD INDEX idx_order_code (order_code), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_purchase_order ADD INDEX idx_vendor_id (vendor_id), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- P1-3 覆盖索引(库存聚合免回表)
|
||||
-- 实测收益显著;但存在写入放大(每次 insert/update 需维护 4 列索引),
|
||||
-- 生产库请由 DBA 结合写入量评估后再执行;回滚语句见文末。
|
||||
-- oms_test 已于 2026-09-23 执行。
|
||||
-- ---------------------------------------------------------------------
|
||||
-- ALTER TABLE oms_inventory_info
|
||||
-- ADD INDEX idx_pc_status_amt (product_code, inventory_status, inner_price, outer_price),
|
||||
-- ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- P2 按表规模触发(当前数据量下无需执行;表增长到 10 万+ 或成为热点时再补)
|
||||
-- 完整编号清单见方案 6.3 节(P2-1 ~ P2-26,其中 P2-3/P2-4/P2-19 已作废)
|
||||
-- ---------------------------------------------------------------------
|
||||
-- ALTER TABLE oms_purchase_order_map ADD INDEX idx_order_id (order_id), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_purchase_order_map ADD INDEX idx_purchase_id (purchase_id), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_inventory_info ADD INDEX idx_warehouse_id (warehouse_id), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_stock_info ADD INDEX idx_order_code (order_code), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_receivable_receipt_detail ADD INDEX idx_write_off_id (write_off_id), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_payable_payment_detail ADD INDEX idx_write_off_id (write_off_id), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE bu_todo_completed ADD INDEX idx_approve_user_time (approve_user, approve_time), ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
||||
-- =====================================================================
|
||||
-- 执行前核对:列出目标表当前索引(若目标索引已出现则无需再建)
|
||||
-- =====================================================================
|
||||
-- SELECT table_name, index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) AS cols
|
||||
-- FROM information_schema.statistics
|
||||
-- WHERE table_schema = DATABASE()
|
||||
-- AND table_name IN ('oms_inventory_outer','oms_inventory_outer_detail','oms_inventory_inner',
|
||||
-- 'oms_purchase_order','oms_inventory_info')
|
||||
-- GROUP BY table_name, index_name;
|
||||
|
||||
-- =====================================================================
|
||||
-- 回滚(仅在需要撤销新增索引时执行)
|
||||
-- =====================================================================
|
||||
-- ALTER TABLE oms_inventory_outer DROP INDEX idx_outer_code, ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_inventory_outer_detail DROP INDEX idx_outer_code, ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_inventory_inner DROP INDEX idx_order_code, ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_purchase_order DROP INDEX idx_vendor_id, ALGORITHM=INPLACE, LOCK=NONE;
|
||||
-- ALTER TABLE oms_inventory_info DROP INDEX idx_pc_status_amt, ALGORITHM=INPLACE, LOCK=NONE;
|
||||
|
|
@ -10,6 +10,8 @@ public final class McpErrorUtils {
|
|||
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 RATE_LIMITED = -32002;
|
||||
public static final int QUERY_TIMEOUT = -32003;
|
||||
public static final int SERVER_ERROR = -32000;
|
||||
|
||||
private McpErrorUtils() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.ruoyi.sip.flowable.domain.Todo;
|
|||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 待办Mapper接口
|
||||
|
|
@ -90,5 +91,19 @@ public interface TodoMapper
|
|||
|
||||
List<Todo> listTodoByBusinessKeyAndProcessKey(@Param("businessKeys") List<String> businessKeyList, @Param("processKeys") List<String> list);
|
||||
|
||||
/**
|
||||
* MCP 只读:待办分页(bu_todo),排序键 apply_time desc,id desc,游标 keyset 推进。
|
||||
*
|
||||
* @param query approveUser / processKeyList / businessKeyList / startTime / endTime / lastTime / lastId / limit
|
||||
*/
|
||||
List<Todo> listTodoPage(Map<String, Object> query);
|
||||
|
||||
/**
|
||||
* MCP 只读:已办分页(bu_todo_completed),排序键 approve_time desc,id desc,游标 keyset 推进。
|
||||
*
|
||||
* @param query approveUser / processKeyList / businessKeyList / startTime / endTime / lastTime / lastId / limit
|
||||
*/
|
||||
List<Todo> listTodoCompletedPage(Map<String, Object> query);
|
||||
|
||||
// List<Todo> listApprove(WorkOrderStatisticsRequestDto dto);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ package com.ruoyi.sip.llm;
|
|||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.utils.McpErrorUtils;
|
||||
import com.ruoyi.common.utils.ShiroUtils;
|
||||
|
|
@ -12,6 +17,10 @@ import javax.servlet.http.HttpServletResponse;
|
|||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -26,13 +35,28 @@ public class McpController {
|
|||
|
||||
public McpController() {
|
||||
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
|
||||
// java.time 支持:SQL 查询以 Map 返回时,DATETIME/DATE 列可能是 LocalDateTime/LocalDate,
|
||||
// 若不注册序列化器会抛 InvalidDefinitionException,导致响应体为空。
|
||||
// 统一按工具约定输出:日期 yyyy-MM-dd,时间 yyyy-MM-dd HH:mm:ss。
|
||||
SimpleModule javaTimeModule = new SimpleModule();
|
||||
javaTimeModule.addSerializer(LocalDateTime.class,
|
||||
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
javaTimeModule.addSerializer(LocalDate.class,
|
||||
new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
||||
javaTimeModule.addSerializer(LocalTime.class,
|
||||
new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||
javaTimeModule.addDeserializer(LocalDate.class,
|
||||
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
||||
objectMapper.registerModule(javaTimeModule);
|
||||
}
|
||||
|
||||
@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");
|
||||
// 注意:不要手工设置 Transfer-Encoding,Tomcat 会自行按分块输出,
|
||||
// 手工设置会导致响应头出现两个同名头,部分客户端(如 curl)会丢弃响应体。
|
||||
|
||||
try (OutputStream os = response.getOutputStream()) {
|
||||
McpResponse mcpResponse = new McpResponse();
|
||||
|
|
@ -55,8 +79,31 @@ public class McpController {
|
|||
os.write(bytes, i, length);
|
||||
os.flush();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
// 序列化/写出异常不能静默,否则客户端会收到 HTTP 200 + 空响应体(难以排查)。
|
||||
// 尽量回写一个可读的 error 响应。
|
||||
e.printStackTrace();
|
||||
writeErrorQuietly(response, request, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeErrorQuietly(HttpServletResponse response, McpRequest request, Exception e) {
|
||||
if (response.isCommitted()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
McpResponse errorResponse = new McpResponse();
|
||||
errorResponse.setId(request == null ? null : request.getId());
|
||||
errorResponse.setJsonrpc(request == null ? "2.0" : request.getJsonrpc());
|
||||
errorResponse.setError(toMcpError(e));
|
||||
response.reset();
|
||||
response.setContentType("application/json");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
byte[] bytes = objectMapper.writeValueAsBytes(errorResponse);
|
||||
response.getOutputStream().write(bytes);
|
||||
response.getOutputStream().flush();
|
||||
} catch (Exception ignored) {
|
||||
// 响应已提交或连接已断开,无法回写,仅保留上面的堆栈日志
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,6 +126,13 @@ public class McpController {
|
|||
String message = e.getMessage() != null ? e.getMessage() : "Internal server error";
|
||||
String lowerMessage = message.toLowerCase();
|
||||
|
||||
// 工具层显式抛出的错误:按其声明的错误码与类型返回(如 AUTH_ERROR / INVALID_PARAMS)
|
||||
if (e instanceof com.ruoyi.sip.llm.tools.support.McpToolException) {
|
||||
com.ruoyi.sip.llm.tools.support.McpToolException toolException =
|
||||
(com.ruoyi.sip.llm.tools.support.McpToolException) e;
|
||||
return McpErrorUtils.buildError(toolException.getCode(), message, toolException.getType());
|
||||
}
|
||||
|
||||
if (lowerMessage.contains("tool name is required")
|
||||
|| lowerMessage.contains("invalid")
|
||||
|| lowerMessage.contains("missing")) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
package com.ruoyi.sip.llm;
|
||||
|
||||
import java.sql.SQLTimeoutException;
|
||||
|
||||
/**
|
||||
* MCP 工具调用的查询超时上下文(线程级)。
|
||||
*
|
||||
* <p>由 {@link McpService} 在调用工具前 {@link #begin(int)}、调用后 {@link #end()};
|
||||
* {@link McpQueryTimeoutInterceptor} 在 JDBC 语句准备阶段读取该值并调用
|
||||
* {@link java.sql.Statement#setQueryTimeout(int)},从而给 MCP 触发的 SQL 加上超时保护。</p>
|
||||
*
|
||||
* <p>未设置时(例如页面/报表等非 MCP 调用)不生效,对既有行为零影响。</p>
|
||||
*/
|
||||
public final class McpQueryTimeout {
|
||||
|
||||
/** 聚合类工具超时(秒) */
|
||||
public static final int AGGREGATE_SECONDS = 3;
|
||||
/** 列表 / 点查类工具超时(秒) */
|
||||
public static final int DEFAULT_SECONDS = 5;
|
||||
|
||||
private static final ThreadLocal<Integer> HOLDER = new ThreadLocal<>();
|
||||
|
||||
private McpQueryTimeout() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按工具类型取超时秒数:聚合类(工具名以 {@code _aggregate} 结尾)3 秒,其余 5 秒。
|
||||
*/
|
||||
public static int secondsFor(String toolName) {
|
||||
return toolName != null && toolName.endsWith("_aggregate") ? AGGREGATE_SECONDS : DEFAULT_SECONDS;
|
||||
}
|
||||
|
||||
public static void begin(int seconds) {
|
||||
HOLDER.set(seconds);
|
||||
}
|
||||
|
||||
public static Integer currentSeconds() {
|
||||
return HOLDER.get();
|
||||
}
|
||||
|
||||
public static void end() {
|
||||
HOLDER.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断异常是否由查询超时引起(逐层查找 cause,兼容 MyBatis/Spring 的异常包装)。
|
||||
*/
|
||||
public static boolean isTimeout(Throwable error) {
|
||||
Throwable current = error;
|
||||
int depth = 0;
|
||||
while (current != null && depth++ < 20) {
|
||||
if (current instanceof SQLTimeoutException) {
|
||||
return true;
|
||||
}
|
||||
String message = current.getMessage();
|
||||
if (message != null) {
|
||||
String lower = message.toLowerCase();
|
||||
if (lower.contains("timeout") || lower.contains("cancel")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
current = current.getCause() == current ? null : current.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.ruoyi.sip.llm;
|
||||
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.apache.ibatis.plugin.Intercepts;
|
||||
import org.apache.ibatis.plugin.Invocation;
|
||||
import org.apache.ibatis.plugin.Plugin;
|
||||
import org.apache.ibatis.plugin.Signature;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* MCP 查询超时拦截器:语句准备阶段按 {@link McpQueryTimeout} 的线程上下文设置 JDBC 查询超时。
|
||||
*
|
||||
* <p>仅当 MCP 工具调用设置了线程上下文时才生效,其它页面/定时任务 SQL 不受影响。
|
||||
* 超时由 MySQL 驱动从服务端中止查询并抛 {@link java.sql.SQLTimeoutException},
|
||||
* 避免慢 SQL 长期占用 Druid 连接(maxActive=20)。</p>
|
||||
*
|
||||
* <p>注册方式:项目自定义了 {@code SqlSessionFactory} Bean(MyBatisConfig),MyBatis 自动配置已退让,
|
||||
* 故此处显式 {@code addInterceptor},与 PageHelper 的注册方式保持一致。</p>
|
||||
*/
|
||||
@Component
|
||||
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare",
|
||||
args = {Connection.class, Integer.class}))
|
||||
public class McpQueryTimeoutInterceptor implements Interceptor, InitializingBean {
|
||||
|
||||
@Autowired
|
||||
private List<SqlSessionFactory> sqlSessionFactoryList;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
if (sqlSessionFactoryList == null) {
|
||||
return;
|
||||
}
|
||||
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
|
||||
if (sqlSessionFactory == null) {
|
||||
continue;
|
||||
}
|
||||
boolean registered = false;
|
||||
for (Interceptor interceptor : sqlSessionFactory.getConfiguration().getInterceptors()) {
|
||||
if (interceptor instanceof McpQueryTimeoutInterceptor) {
|
||||
registered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!registered) {
|
||||
sqlSessionFactory.getConfiguration().addInterceptor(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
Object statement = invocation.proceed();
|
||||
Integer seconds = McpQueryTimeout.currentSeconds();
|
||||
if (seconds != null && seconds > 0 && statement instanceof Statement) {
|
||||
try {
|
||||
((Statement) statement).setQueryTimeout(seconds);
|
||||
} catch (Exception ignored) {
|
||||
// 驱动不支持 setQueryTimeout 时不阻断查询(仅失去超时保护)
|
||||
}
|
||||
}
|
||||
return statement;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return Plugin.wrap(target, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperties(Properties properties) {
|
||||
// 无需外部属性
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.ruoyi.sip.llm;
|
||||
|
||||
import com.ruoyi.common.utils.ShiroUtils;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* MCP 层限流:同一机器人(同 X-Bot-Id,无凭证时按匿名/绑定用户)每 {@value #WINDOW_MS} 毫秒最多 {@value #LIMIT} 次调用。
|
||||
*
|
||||
* <p>实现为进程内滑动窗口(项目未引入 Redis,故不落地"状态存 Redis"的原始设计),
|
||||
* 目的是防止 Agent 循环翻页 / 并发调用打满 Druid 连接池(maxActive=20)。
|
||||
* 多实例部署时该计数为单实例口径,如需全局精确限流需改为 Redis 计数。</p>
|
||||
*/
|
||||
@Component
|
||||
public class McpRateLimiter {
|
||||
|
||||
private static final String HEADER_BOT_ID = "X-Bot-Id";
|
||||
/** 窗口内允许的最大调用次数 */
|
||||
private static final int LIMIT = 60;
|
||||
/** 滑动窗口长度(毫秒) */
|
||||
private static final long WINDOW_MS = 60_000L;
|
||||
/** 超过该 key 数量时做一次过期清理,避免长期运行内存增长 */
|
||||
private static final int SWEEP_THRESHOLD = 1024;
|
||||
|
||||
private final ConcurrentHashMap<String, ArrayDeque<Long>> buckets = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 按当前请求的机器人身份限流,超限抛 {@code RATE_LIMITED}。
|
||||
*/
|
||||
public void checkCurrent() {
|
||||
check(resolveKey());
|
||||
}
|
||||
|
||||
public void check(String key) {
|
||||
long now = System.currentTimeMillis();
|
||||
ArrayDeque<Long> window = buckets.computeIfAbsent(key, ignored -> new ArrayDeque<>());
|
||||
synchronized (window) {
|
||||
while (!window.isEmpty() && now - window.peekFirst() > WINDOW_MS) {
|
||||
window.pollFirst();
|
||||
}
|
||||
if (window.size() >= LIMIT) {
|
||||
long retryAfterMs = WINDOW_MS - (now - window.peekFirst());
|
||||
long retryAfterSeconds = Math.max(1L, (retryAfterMs + 999L) / 1000L);
|
||||
throw McpToolException.rateLimited("调用过于频繁:同一机器人每 " + (WINDOW_MS / 1000)
|
||||
+ " 秒最多 " + LIMIT + " 次,请 " + retryAfterSeconds + " 秒后重试(翻页请间隔调用)");
|
||||
}
|
||||
window.addLast(now);
|
||||
}
|
||||
if (buckets.size() > SWEEP_THRESHOLD) {
|
||||
sweep(now);
|
||||
}
|
||||
}
|
||||
|
||||
private void sweep(long now) {
|
||||
for (Iterator<Map.Entry<String, ArrayDeque<Long>>> iterator = buckets.entrySet().iterator();
|
||||
iterator.hasNext(); ) {
|
||||
ArrayDeque<Long> window = iterator.next().getValue();
|
||||
synchronized (window) {
|
||||
while (!window.isEmpty() && now - window.peekFirst() > WINDOW_MS) {
|
||||
window.pollFirst();
|
||||
}
|
||||
if (window.isEmpty()) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流身份:优先机器人凭证标识(与 McpBotAuthFilter 的 X-Bot-Id 对齐),
|
||||
* 无凭证时退回绑定的 Shiro 用户,再退回匿名。
|
||||
*/
|
||||
private String resolveKey() {
|
||||
String botId = currentBotId();
|
||||
if (botId != null) {
|
||||
return "bot:" + botId;
|
||||
}
|
||||
try {
|
||||
if (ShiroUtils.getSysUser() != null && ShiroUtils.getSysUser().getUserId() != null) {
|
||||
return "user:" + ShiroUtils.getSysUser().getUserId();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 无 Shiro 主体(匿名调用)时按匿名计数
|
||||
}
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
private String currentBotId() {
|
||||
try {
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return null;
|
||||
}
|
||||
String botId = attributes.getRequest().getHeader(HEADER_BOT_ID);
|
||||
return botId == null || botId.trim().isEmpty() ? null : botId.trim();
|
||||
} catch (Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.llm;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
|
@ -16,6 +17,12 @@ public class McpService {
|
|||
@Autowired
|
||||
private McpToolRegistry registry;
|
||||
|
||||
@Autowired
|
||||
private com.ruoyi.sip.llm.router.ToolRouter toolRouter;
|
||||
|
||||
@Autowired
|
||||
private McpRateLimiter rateLimiter;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object handleRequest(McpRequest request) {
|
||||
String method = request.getMethod();
|
||||
|
|
@ -45,28 +52,66 @@ public class McpService {
|
|||
}
|
||||
|
||||
if ("tools/list".equals(method)) {
|
||||
rateLimiter.checkCurrent();
|
||||
String query = asText(params == null ? null : params.get("query"));
|
||||
boolean detail = params == null || params.get("detail") == null
|
||||
|| !"false".equalsIgnoreCase(asText(params.get("detail")));
|
||||
java.util.Set<String> routedNames = toolRouter.filterToolNames(query);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<McpTool> tools = registry.list();
|
||||
result.put("tools", tools.stream().map(t -> {
|
||||
List<Map<String, Object>> toolList = new ArrayList<>();
|
||||
for (McpTool tool : registry.list()) {
|
||||
if (routedNames != null && !routedNames.contains(tool.getName())) {
|
||||
continue;
|
||||
}
|
||||
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()));
|
||||
tm.put("name", tool.getName());
|
||||
tm.put("description", tool.getDescription());
|
||||
if (detail) {
|
||||
tm.put("inputSchema", tool.getInputSchema());
|
||||
}
|
||||
toolList.add(tm);
|
||||
}
|
||||
if (query != null && !query.isEmpty()) {
|
||||
result.put("routed_by_query", query);
|
||||
result.put("routed_tool_count", toolList.size());
|
||||
result.put("hint", "以上为按 query 召回的工具子集;需要查看全部工具时不要传 query,需要精简 schema 时传 detail=false。");
|
||||
}
|
||||
result.put("tools", toolList);
|
||||
return result;
|
||||
}
|
||||
|
||||
if ("tools/route".equals(method)) {
|
||||
rateLimiter.checkCurrent();
|
||||
String query = asText(params == null ? null : params.get("query"));
|
||||
if (query == null || query.isEmpty()) {
|
||||
throw new RuntimeException("Invalid params: tools/route 需要 query 参数");
|
||||
}
|
||||
Integer limit = params.get("limit") == null ? null : Integer.valueOf(asText(params.get("limit")));
|
||||
return toolRouter.route(query, limit);
|
||||
}
|
||||
|
||||
if ("tools/call".equals(method)) {
|
||||
String toolName = params != null ? (String) params.get("name") : null;
|
||||
rateLimiter.checkCurrent();
|
||||
String toolName = params != null ? asText(params.get("name")) : null;
|
||||
if (toolName == null || toolName.isEmpty()) {
|
||||
throw new RuntimeException("Tool name is required");
|
||||
String query = asText(params == null ? null : params.get("query"));
|
||||
if (query != null && !query.isEmpty()) {
|
||||
toolName = toolRouter.pickToolName(query);
|
||||
if (toolName == null) {
|
||||
throw new RuntimeException("Invalid params: 无法从 query 推断出工具(得分低于阈值),"
|
||||
+ "请显式指定 name,或先调用 tools/route 查看候选。候选:"
|
||||
+ JSON.toJSONString(toolRouter.retrieve(query, 5)));
|
||||
}
|
||||
} else {
|
||||
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));
|
||||
return callTool(toolName, tool, args == null ? new HashMap<>() : args);
|
||||
} else {
|
||||
throw new RuntimeException("Tool not found: " + toolName);
|
||||
}
|
||||
|
|
@ -75,6 +120,33 @@ public class McpService {
|
|||
throw new RuntimeException("Method not supported: " + method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行工具:设置查询超时上下文(聚合 3s / 其余 5s),并把 JDBC 超时异常翻译为明确的 QUERY_TIMEOUT 错误。
|
||||
*/
|
||||
private Object callTool(String toolName, McpTool tool, Map<String, Object> args) {
|
||||
int timeoutSeconds = McpQueryTimeout.secondsFor(toolName);
|
||||
McpQueryTimeout.begin(timeoutSeconds);
|
||||
try {
|
||||
return wrapToolResult(tool.getHandler().apply(args));
|
||||
} catch (RuntimeException e) {
|
||||
if (McpQueryTimeout.isTimeout(e)) {
|
||||
throw McpToolException.timeout("查询超时:工具 " + toolName + " 执行超过 " + timeoutSeconds
|
||||
+ " 秒已被中止,请收窄过滤条件、减少 batch 或改用聚合工具(mode=SUMMARY)");
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
McpQueryTimeout.end();
|
||||
}
|
||||
}
|
||||
|
||||
private String asText(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object wrapToolResult(Object rawResult) {
|
||||
if (rawResult instanceof Map) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
package com.ruoyi.sip.llm.router;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 轻量文本向量化:中文按字符 2-gram,英文/数字按词切分(驼峰与下划线拆开),
|
||||
* 不依赖任何外部 embedding 服务,配合关键词加权做余弦匹配。
|
||||
*/
|
||||
public final class ToolEmbedding {
|
||||
|
||||
private static final int CJK_NGRAM = 2;
|
||||
|
||||
private ToolEmbedding() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分词:CJK 连续段取字符 2-gram(长度为 1 时保留单字),ASCII 段按分隔符/驼峰切词。
|
||||
*/
|
||||
public static List<String> tokenize(String text, Set<String> stopwords) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
if (text == null || text.isEmpty()) {
|
||||
return tokens;
|
||||
}
|
||||
String lower = text.toLowerCase();
|
||||
StringBuilder ascii = new StringBuilder();
|
||||
StringBuilder cjk = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < lower.length(); i++) {
|
||||
char ch = lower.charAt(i);
|
||||
if (isCjk(ch)) {
|
||||
flushAscii(ascii, tokens, stopwords);
|
||||
cjk.append(ch);
|
||||
} else if (isWordChar(ch)) {
|
||||
flushCjk(cjk, tokens, stopwords);
|
||||
ascii.append(ch);
|
||||
} else {
|
||||
flushAscii(ascii, tokens, stopwords);
|
||||
flushCjk(cjk, tokens, stopwords);
|
||||
}
|
||||
}
|
||||
flushAscii(ascii, tokens, stopwords);
|
||||
flushCjk(cjk, tokens, stopwords);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static void flushAscii(StringBuilder buffer, List<String> tokens, Set<String> stopwords) {
|
||||
if (buffer.length() == 0) {
|
||||
return;
|
||||
}
|
||||
String word = buffer.toString();
|
||||
buffer.setLength(0);
|
||||
for (String part : splitCamel(word)) {
|
||||
if (part.length() >= 2 && !isStopword(part, stopwords)) {
|
||||
tokens.add(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void flushCjk(StringBuilder buffer, List<String> tokens, Set<String> stopwords) {
|
||||
if (buffer.length() == 0) {
|
||||
return;
|
||||
}
|
||||
String segment = buffer.toString();
|
||||
buffer.setLength(0);
|
||||
if (segment.length() == 1) {
|
||||
addToken(segment, tokens, stopwords);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i + CJK_NGRAM <= segment.length(); i++) {
|
||||
addToken(segment.substring(i, i + CJK_NGRAM), tokens, stopwords);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addToken(String token, List<String> tokens, Set<String> stopwords) {
|
||||
if (token != null && !token.isEmpty() && !isStopword(token, stopwords)) {
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isStopword(String token, Set<String> stopwords) {
|
||||
return stopwords != null && stopwords.contains(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆驼峰与下划线:inventoryStockAggregate -> inventory, stock, aggregate
|
||||
*/
|
||||
private static List<String> splitCamel(String word) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (word == null || word.isEmpty()) {
|
||||
return parts;
|
||||
}
|
||||
for (String chunk : word.split("[_\\-]+")) {
|
||||
if (chunk.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
StringBuilder current = new StringBuilder();
|
||||
for (int i = 0; i < chunk.length(); i++) {
|
||||
char ch = chunk.charAt(i);
|
||||
if (i > 0 && Character.isUpperCase(ch) && current.length() > 0
|
||||
&& Character.isLowerCase(chunk.charAt(i - 1))) {
|
||||
parts.add(current.toString().toLowerCase());
|
||||
current.setLength(0);
|
||||
}
|
||||
current.append(ch);
|
||||
}
|
||||
if (current.length() > 0) {
|
||||
parts.add(current.toString().toLowerCase());
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
private static boolean isCjk(char ch) {
|
||||
return ch >= 0x4E00 && ch <= 0x9FFF;
|
||||
}
|
||||
|
||||
private static boolean isWordChar(char ch) {
|
||||
return Character.isLetterOrDigit(ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* 词频向量(去重计数)。
|
||||
*/
|
||||
public static Map<String, Double> termFrequency(List<String> tokens) {
|
||||
Map<String, Double> vector = new LinkedHashMap<>();
|
||||
if (tokens == null) {
|
||||
return vector;
|
||||
}
|
||||
for (String token : tokens) {
|
||||
vector.merge(token, 1.0, Double::sum);
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
|
||||
/**
|
||||
* 余弦相似度。
|
||||
*/
|
||||
public static double cosine(Map<String, Double> left, Map<String, Double> right) {
|
||||
if (left == null || right == null || left.isEmpty() || right.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
Map<String, Double> small = left.size() <= right.size() ? left : right;
|
||||
Map<String, Double> large = small == left ? right : left;
|
||||
double dot = 0.0;
|
||||
for (Map.Entry<String, Double> entry : small.entrySet()) {
|
||||
Double other = large.get(entry.getKey());
|
||||
if (other != null) {
|
||||
dot += entry.getValue() * other;
|
||||
}
|
||||
}
|
||||
if (dot == 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
return dot / (norm(left) * norm(right));
|
||||
}
|
||||
|
||||
private static double norm(Map<String, Double> vector) {
|
||||
double sum = 0.0;
|
||||
for (Double value : vector.values()) {
|
||||
sum += value * value;
|
||||
}
|
||||
return Math.sqrt(sum);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去重后的词集合,便于做"命中率"类打分。
|
||||
*/
|
||||
public static Set<String> distinctTokens(String text, Set<String> stopwords) {
|
||||
return new LinkedHashSet<>(tokenize(text, stopwords));
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法:把文本转成词频向量。
|
||||
*/
|
||||
public static Map<String, Double> vectorize(String text, Set<String> stopwords) {
|
||||
Map<String, Double> vector = new HashMap<>();
|
||||
for (String token : distinctTokens(text, stopwords)) {
|
||||
vector.put(token, 1.0);
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
package com.ruoyi.sip.llm.router;
|
||||
|
||||
import com.ruoyi.sip.llm.McpTool;
|
||||
import com.ruoyi.sip.llm.McpToolRegistry;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工具路由(RAG):用中文 2-gram + 关键词加权做工具召回,避免每轮把全部工具的 schema 下发。
|
||||
* <p>配置来自 classpath:mcp-tool-router.yml,新增工具只需补别名与同义词,无需改代码。</p>
|
||||
*/
|
||||
@Component
|
||||
public class ToolRouter {
|
||||
|
||||
private static final double WEIGHT_ALIAS = 3.0;
|
||||
private static final double WEIGHT_SYNONYM = 2.5;
|
||||
private static final double WEIGHT_TOOL_NAME = 2.0;
|
||||
private static final double WEIGHT_DESCRIPTION = 1.0;
|
||||
|
||||
@Autowired
|
||||
private McpToolRegistry registry;
|
||||
|
||||
private boolean enabled = true;
|
||||
private int topK = 5;
|
||||
private double minScore = 0.15;
|
||||
private Set<String> stopwords = new LinkedHashSet<>();
|
||||
private final Map<String, List<String>> toolAliases = new LinkedHashMap<>();
|
||||
private final Map<String, List<String>> synonyms = new LinkedHashMap<>();
|
||||
|
||||
/** 每个工具的词 -> 权重(取最大值),由别名/同义词/工具名/描述合成 */
|
||||
private final Map<String, Map<String, Double>> toolTermWeights = new LinkedHashMap<>();
|
||||
/** 同义词 -> 工具(含 #entity 建议) */
|
||||
private final Map<String, List<String>> synonymTools = new LinkedHashMap<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
loadConfig();
|
||||
rebuildIndex();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void loadConfig() {
|
||||
ClassPathResource resource = new ClassPathResource("mcp-tool-router.yml");
|
||||
if (!resource.exists()) {
|
||||
return;
|
||||
}
|
||||
try (InputStream input = resource.getInputStream()) {
|
||||
Map<String, Object> root = new Yaml().load(input);
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> router = asMap(root.get("router"));
|
||||
if (router.isEmpty()) {
|
||||
router = root;
|
||||
}
|
||||
enabled = asBoolean(router.get("enabled"), true);
|
||||
topK = asInt(router.get("top-k"), 5);
|
||||
minScore = asDouble(router.get("min-score"), 0.15);
|
||||
stopwords = new LinkedHashSet<>(asStringList(router.get("stopwords")));
|
||||
toolAliases.putAll(asStringListMap(router.get("tool-aliases")));
|
||||
synonyms.putAll(asStringListMap(router.get("synonyms")));
|
||||
} catch (Exception ex) {
|
||||
// 配置异常时退化为"仅用工具名与描述",不影响主流程
|
||||
enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> asMap(Object value) {
|
||||
if (value instanceof Map) {
|
||||
return (Map<String, Object>) value;
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, List<String>> asStringListMap(Object value) {
|
||||
Map<String, List<String>> result = new LinkedHashMap<>();
|
||||
if (value instanceof Map) {
|
||||
for (Map.Entry<String, Object> entry : ((Map<String, Object>) value).entrySet()) {
|
||||
result.put(String.valueOf(entry.getKey()), asStringList(entry.getValue()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> asStringList(Object value) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (value instanceof List) {
|
||||
for (Object item : (List<Object>) value) {
|
||||
if (item != null) {
|
||||
result.add(String.valueOf(item));
|
||||
}
|
||||
}
|
||||
} else if (value != null) {
|
||||
result.add(String.valueOf(value));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean asBoolean(Object value, boolean defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return Boolean.parseBoolean(String.valueOf(value));
|
||||
}
|
||||
|
||||
private int asInt(Object value, int defaultValue) {
|
||||
try {
|
||||
return value == null ? defaultValue : Integer.parseInt(String.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private double asDouble(Object value, double defaultValue) {
|
||||
try {
|
||||
return value == null ? defaultValue : Double.parseDouble(String.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建索引:把别名、同义词、工具名、描述合成到每个工具的加权词表。
|
||||
*/
|
||||
public synchronized void rebuildIndex() {
|
||||
toolTermWeights.clear();
|
||||
synonymTools.clear();
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : synonyms.entrySet()) {
|
||||
String term = entry.getKey();
|
||||
for (String toolRef : entry.getValue()) {
|
||||
String toolName = toolRef.contains("#") ? toolRef.substring(0, toolRef.indexOf('#')) : toolRef;
|
||||
synonymTools.computeIfAbsent(term, key -> new ArrayList<>()).add(toolRef);
|
||||
mergeTerm(toolName, term, WEIGHT_SYNONYM);
|
||||
for (String token : ToolEmbedding.distinctTokens(term, stopwords)) {
|
||||
mergeTerm(toolName, token, WEIGHT_SYNONYM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (McpTool tool : registry.list()) {
|
||||
String name = tool.getName();
|
||||
mergeTerm(name, name, WEIGHT_TOOL_NAME);
|
||||
for (String token : ToolEmbedding.distinctTokens(name, stopwords)) {
|
||||
mergeTerm(name, token, WEIGHT_TOOL_NAME);
|
||||
}
|
||||
for (String token : ToolEmbedding.distinctTokens(tool.getDescription(), stopwords)) {
|
||||
mergeTerm(name, token, WEIGHT_DESCRIPTION);
|
||||
}
|
||||
for (String alias : toolAliases.getOrDefault(name, Collections.emptyList())) {
|
||||
mergeTerm(name, alias, WEIGHT_ALIAS);
|
||||
for (String token : ToolEmbedding.distinctTokens(alias, stopwords)) {
|
||||
mergeTerm(name, token, WEIGHT_ALIAS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeTerm(String toolName, String term, double weight) {
|
||||
if (toolName == null || term == null || term.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Double> weights = toolTermWeights.computeIfAbsent(toolName, key -> new LinkedHashMap<>());
|
||||
weights.merge(term.trim().toLowerCase(), weight, Math::max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 召回候选工具(按得分降序)。
|
||||
*/
|
||||
public List<Map<String, Object>> retrieve(String query, Integer limit) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
Set<String> queryTokens = ToolEmbedding.distinctTokens(query, stopwords);
|
||||
int querySize = queryTokens.isEmpty() ? 1 : queryTokens.size();
|
||||
double maxWeight = WEIGHT_ALIAS;
|
||||
|
||||
List<Map<String, Object>> scored = new ArrayList<>();
|
||||
for (Map.Entry<String, Map<String, Double>> entry : toolTermWeights.entrySet()) {
|
||||
String toolName = entry.getKey();
|
||||
if (registry.get(toolName) == null) {
|
||||
continue;
|
||||
}
|
||||
double matchedWeight = 0.0;
|
||||
List<String> matchedTerms = new ArrayList<>();
|
||||
for (String token : queryTokens) {
|
||||
Double weight = entry.getValue().get(token);
|
||||
if (weight != null) {
|
||||
matchedWeight += weight;
|
||||
matchedTerms.add(token);
|
||||
}
|
||||
}
|
||||
// 同义词整串命中额外加权
|
||||
String lowered = query.toLowerCase();
|
||||
for (String synonymTerm : synonymTools.keySet()) {
|
||||
if (lowered.contains(synonymTerm.toLowerCase())) {
|
||||
for (String toolRef : synonymTools.get(synonymTerm)) {
|
||||
String refTool = toolRef.contains("#") ? toolRef.substring(0, toolRef.indexOf('#')) : toolRef;
|
||||
if (refTool.equals(toolName)) {
|
||||
matchedWeight += WEIGHT_SYNONYM;
|
||||
matchedTerms.add(synonymTerm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchedWeight <= 0) {
|
||||
continue;
|
||||
}
|
||||
double score = matchedWeight / (maxWeight * querySize);
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("name", toolName);
|
||||
item.put("score", Math.round(score * 10000.0) / 10000.0);
|
||||
item.put("matched_terms", matchedTerms);
|
||||
scored.add(item);
|
||||
}
|
||||
|
||||
scored.sort(Comparator.comparingDouble((Map<String, Object> item) -> (Double) item.get("score")).reversed());
|
||||
int size = limit == null || limit <= 0 ? topK : limit;
|
||||
for (int i = 0; i < scored.size() && i < size; i++) {
|
||||
result.add(scored.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动路由:低于阈值不硬选,返回 candidates 供模型确认。
|
||||
*/
|
||||
public Map<String, Object> route(String query, Integer limit) {
|
||||
List<Map<String, Object>> candidates = retrieve(query, limit);
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("query", query);
|
||||
result.put("candidates", candidates);
|
||||
result.put("min_score", minScore);
|
||||
if (candidates.isEmpty() || ((Double) candidates.get(0).get("score")) < minScore) {
|
||||
result.put("selected_tool", null);
|
||||
result.put("hint", "未找到得分足够高的工具,请补充更具体的业务词(如\"库存\"\"采购单\"\"应收\"\"签收\"),或直接指定工具名调用 tools/call");
|
||||
return result;
|
||||
}
|
||||
Map<String, Object> best = candidates.get(0);
|
||||
result.put("selected_tool", best.get("name"));
|
||||
result.put("selected_score", best.get("score"));
|
||||
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
String lowered = query.toLowerCase();
|
||||
for (Map.Entry<String, List<String>> entry : synonymTools.entrySet()) {
|
||||
if (!lowered.contains(entry.getKey().toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
for (String ref : entry.getValue()) {
|
||||
String refTool = ref.contains("#") ? ref.substring(0, ref.indexOf('#')) : ref;
|
||||
int index = ref.indexOf('#');
|
||||
if (refTool.equals(best.get("name")) && index > 0) {
|
||||
suggestions.add("entity=" + ref.substring(index + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!suggestions.isEmpty()) {
|
||||
Map<String, Object> suggestedArgs = new LinkedHashMap<>();
|
||||
suggestedArgs.put("suggested_args", suggestions);
|
||||
result.putAll(suggestedArgs);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* tools/call 未指定工具时使用:返回最佳工具名;低于阈值返回 null(由调用方报错提示)。
|
||||
*/
|
||||
public String pickToolName(String query) {
|
||||
List<Map<String, Object>> candidates = retrieve(query, 1);
|
||||
if (candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> best = candidates.get(0);
|
||||
if (((Double) best.get("score")) < minScore) {
|
||||
return null;
|
||||
}
|
||||
return (String) best.get("name");
|
||||
}
|
||||
|
||||
/**
|
||||
* tools/list 时按 query 过滤;未启用或未给 query 时返回 null 表示"不过滤"。
|
||||
*/
|
||||
public Set<String> filterToolNames(String query) {
|
||||
if (!enabled || query == null || query.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Map<String, Object>> candidates = retrieve(query, topK);
|
||||
if (candidates.isEmpty()) {
|
||||
throw McpToolException.invalidParams("未找到与 query 匹配的工具,请换个说法或去掉 query 查看全部工具");
|
||||
}
|
||||
Set<String> names = new LinkedHashSet<>();
|
||||
for (Map<String, Object> item : candidates) {
|
||||
names.add((String) item.get("name"));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public double getMinScore() {
|
||||
return minScore;
|
||||
}
|
||||
|
||||
public int getTopK() {
|
||||
return topK;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.flowable.domain.Todo;
|
||||
import com.ruoyi.sip.flowable.mapper.TodoMapper;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpCursor;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpPages;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpTimeRange;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* MCP 工具 approval_list(审批待办 / 已办,游标分页)。
|
||||
*
|
||||
* <p>方案依据:docx/mcp-data-tools-plan.md 第 4.3、5、15.14、16.9 与附录 A.15。</p>
|
||||
*
|
||||
* <p>回答的问题:"我还有哪些单要审""现在卡在谁那儿""为什么被驳回""审批耗了多久"。数据来自业务表
|
||||
* {@code bu_todo}(待办)与 {@code bu_todo_completed}(已办),不读 Flowable 的 act_* 表。</p>
|
||||
*
|
||||
* <p>权限串来源:{@code TodoController} 的 /flow/todo/list、/flow/completed/list 未加
|
||||
* {@code @RequiresPermissions}(仅登录),因此按方案取审批入口页面的权限组合(任意一个满足):
|
||||
* {@code OmsPurchaseOrderController#approveList} / {@code #approvedList}=sip:purchaseorder:list、
|
||||
* {@code OmsPaymentBillController#approveList}=finance:payment:approve、{@code #approvedList}=finance:payment:approve
|
||||
* 与 {@code #list}=finance:payment:list。</p>
|
||||
*/
|
||||
@Component
|
||||
public class ApprovalListToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final Set<String> ENTITIES = new LinkedHashSet<>(Arrays.asList("TODO", "DONE"));
|
||||
|
||||
private static final int MAX_PROCESS_KEY = 20;
|
||||
private static final int DEFAULT_MONTHS = 12;
|
||||
private static final int MAX_MONTHS = 36;
|
||||
private static final long COUNT_CAP = 50000L;
|
||||
|
||||
/** 实测已覆盖的 process_key(含源码拼写错误 fianance_ticket,匹配必须按原样)。 */
|
||||
private static final String[] PROCESS_KEYS = new String[]{
|
||||
"order_approve_online", "order_approve_offline", "purchase_order_online",
|
||||
"finance_payment", "fianance_ticket", "order_reback", "outer_reback"};
|
||||
|
||||
@Autowired
|
||||
private TodoMapper todoMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "approval_list";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "审批待办 / 已办查询(只读)。entity=TODO 我待审批的单据(bu_todo)/ DONE 我已审批的历史(bu_todo_completed,"
|
||||
+ "含审批意见与审批结果)。默认按当前登录用户过滤审批人(approve_user=ALL 表示不按审批人过滤)。"
|
||||
+ "可回答:我还有哪些单要审 / 现在卡在谁那儿 / 为什么被驳回 / 审批耗了多久。"
|
||||
+ "注意:approve_status 语义为 3=通过、2=驳回、1=待审批(与常规枚举相反)。"
|
||||
+ "分页查询工具:若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 "
|
||||
+ "has_more 为 false,否则数据不完整。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("entity", enumProperty("查询实体(必填):TODO=待办(bu_todo,我还没审的)/ "
|
||||
+ "DONE=已办(bu_todo_completed,我已审过的,含审批意见)", "TODO", "DONE"));
|
||||
properties.put("approve_user", stringProperty("审批人(bu_todo.approve_user,用户ID字符串);"
|
||||
+ "不传=默认当前登录用户ID;传 ALL=不按审批人过滤(可看全部人的待办/已办)"));
|
||||
properties.put("process_key_list", arrayProperty("流程KEY列表,≤" + MAX_PROCESS_KEY + ";实测已覆盖:"
|
||||
+ String.join(" / ", PROCESS_KEYS)));
|
||||
properties.put("business_key_list", arrayProperty("业务主键列表(合同编号 / 采购单号等),≤"
|
||||
+ McpPages.MAX_CODE_LIST));
|
||||
properties.put("time_range", stringProperty("时间区间,格式 start~end(yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss);"
|
||||
+ "entity=TODO 基于 apply_time(发起时间),entity=DONE 基于 approve_time(审批时间);"
|
||||
+ "未传时默认近 " + DEFAULT_MONTHS + " 个月,跨度上限 " + MAX_MONTHS + " 个月"));
|
||||
properties.put("time_range_start", stringProperty("时间区间开始(可选,与 time_range 二选一)"));
|
||||
properties.put("time_range_end", stringProperty("时间区间结束(可选,与 time_range 二选一)"));
|
||||
properties.put("include_total", boolProperty("是否统计总条数,默认 false;true 时受 count_cap=" + COUNT_CAP
|
||||
+ " 限制,超出置 total_count_capped=true"));
|
||||
withPagingProperties(properties, McpPages.MAX_LIST_PAGE_SIZE, McpPages.DEFAULT_PAGE_SIZE);
|
||||
return objectSchema(properties, "entity");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
String entity = McpParams.requireEnum(params, "entity", ENTITIES);
|
||||
// 权限来源见类注释(TodoController 无权限注解,取审批入口页面权限,任意一个满足即放行)
|
||||
McpAuth.requireAnyPermission("sip:purchaseorder:list", "finance:payment:approve", "finance:payment:list");
|
||||
|
||||
String approveUser = resolveApproveUser(params);
|
||||
List<String> processKeyList = McpParams.codeList(params, "process_key_list", MAX_PROCESS_KEY);
|
||||
List<String> businessKeyList = McpParams.codeList(params, "business_key_list", McpPages.MAX_CODE_LIST);
|
||||
McpTimeRange range = McpParams.timeRange(params, "time_range", DEFAULT_MONTHS, MAX_MONTHS);
|
||||
int pageSize = McpPages.pageSize(params, McpPages.DEFAULT_PAGE_SIZE, McpPages.MAX_LIST_PAGE_SIZE);
|
||||
boolean includeTotal = McpParams.boolVal(params, "include_total", false);
|
||||
|
||||
String filterHash = McpParams.filterHash(getToolName(), entity, approveUser,
|
||||
sortedJoin(processKeyList), sortedJoin(businessKeyList), McpParams.describe(range));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
int pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("approveUser", approveUser);
|
||||
q.put("processKeyList", processKeyList);
|
||||
q.put("businessKeyList", businessKeyList);
|
||||
q.put("startTime", range.getStart());
|
||||
q.put("endTime", range.getEnd());
|
||||
q.put("limit", pageSize + 1);
|
||||
if (cursor != null) {
|
||||
q.put("lastTime", McpParams.parseDate(cursor.key(0), "cursor"));
|
||||
q.put("lastId", cursor.longKey(1));
|
||||
}
|
||||
|
||||
boolean todo = "TODO".equals(entity);
|
||||
List<Todo> rows = todo ? todoMapper.listTodoPage(q) : todoMapper.listTodoCompletedPage(q);
|
||||
boolean hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<Todo> page = McpPages.trim(rows, pageSize);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
List<String> lastSortKeys = null;
|
||||
for (Todo row : page) {
|
||||
items.add(todo ? toTodoItem(row) : toDoneItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
Todo last = page.get(page.size() - 1);
|
||||
Date lastTime = todo ? last.getApplyTime() : last.getApproveTime();
|
||||
lastSortKeys = keysOf(McpFormat.dateTime(lastTime), last.getId());
|
||||
}
|
||||
|
||||
Long total = null;
|
||||
boolean totalCapped = false;
|
||||
if (includeTotal) {
|
||||
Map<String, Object> countQuery = new HashMap<>(q);
|
||||
countQuery.remove("lastTime");
|
||||
countQuery.remove("lastId");
|
||||
countQuery.put("limit", COUNT_CAP + 1);
|
||||
List<Todo> countRows = todo ? todoMapper.listTodoPage(countQuery) : todoMapper.listTodoCompletedPage(countQuery);
|
||||
long counted = countRows == null ? 0 : countRows.size();
|
||||
totalCapped = counted > COUNT_CAP;
|
||||
total = totalCapped ? COUNT_CAP : counted;
|
||||
}
|
||||
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), pageSize, hasMore,
|
||||
hasMore ? McpCursor.encode(getToolName(), filterHash, pageNo + 1, lastSortKeys) : null,
|
||||
todo ? "apply_time desc,id" : "approve_time desc,id", pageNo, total, totalCapped, false);
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("entity", entity);
|
||||
query.put("approve_user", approveUser == null ? "ALL" : approveUser);
|
||||
query.put("process_key_list", processKeyList);
|
||||
query.put("business_key_list", businessKeyList);
|
||||
query.put("time_field", todo ? "apply_time" : "approve_time");
|
||||
query.put("time_range", McpParams.describe(range));
|
||||
query.put("page_size", pageSize);
|
||||
query.put("page_no", pageNo);
|
||||
query.put("include_total", includeTotal);
|
||||
|
||||
return response(buildMetadata(entity), query, listData(items, pageInfo, total));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 行映射
|
||||
|
||||
private Map<String, Object> toTodoItem(Todo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("todoId", row.getTodoId());
|
||||
item.put("processInstanceId", row.getProcessInstanceId());
|
||||
item.put("taskId", row.getTaskId());
|
||||
item.put("businessKey", McpFormat.trim(row.getBusinessKey()));
|
||||
item.put("processKey", row.getProcessKey());
|
||||
item.put("processName", row.getProcessName());
|
||||
item.put("taskName", row.getTaskName());
|
||||
item.put("approveUser", McpFormat.trim(row.getApproveUser()));
|
||||
item.put("approveUserName", row.getApproveUserName());
|
||||
item.put("applyUserName", row.getApplyUserName());
|
||||
item.put("applyTime", McpFormat.dateTime(row.getApplyTime()));
|
||||
item.put("formKey", row.getFormKey());
|
||||
item.put("extendField1", row.getExtendField1());
|
||||
item.put("extendField2", row.getExtendField2());
|
||||
item.put("extendField3", row.getExtendField3());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toDoneItem(Todo row) {
|
||||
Map<String, Object> item = toTodoItem(row);
|
||||
item.put("approveTime", McpFormat.dateTime(row.getApproveTime()));
|
||||
item.put("approveOpinion", row.getApproveOpinion());
|
||||
item.put("approveStatus", row.getApproveStatus());
|
||||
item.put("approveStatusName", approveStatusName(row.getApproveStatus()));
|
||||
item.put("allApproveUserName", row.getAllApproveUserName());
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ 实测语义与常规相反:3=通过、2=驳回、1=待审批(附录 A.15)。
|
||||
*/
|
||||
private String approveStatusName(Integer status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
switch (status) {
|
||||
case 3:
|
||||
return "通过";
|
||||
case 2:
|
||||
return "驳回";
|
||||
case 1:
|
||||
return "待审批";
|
||||
default:
|
||||
return String.valueOf(status);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private String resolveApproveUser(Map<String, Object> params) {
|
||||
String value = McpParams.str(params, "approve_user");
|
||||
if (value == null) {
|
||||
Long currentUserId = McpAuth.currentUserId();
|
||||
if (currentUserId == null) {
|
||||
throw McpToolException.authError("无法识别当前登录用户,请显式传入 approve_user(用户ID)或 ALL");
|
||||
}
|
||||
return String.valueOf(currentUserId);
|
||||
}
|
||||
if ("ALL".equalsIgnoreCase(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String sortedJoin(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return String.join(",", new TreeSet<>(values));
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata(String entity) {
|
||||
Map<String, Object> metadata = metadata("审批待办 / 已办(entity=" + entity + ");排序键 "
|
||||
+ ("TODO".equals(entity) ? "apply_time desc,id" : "approve_time desc,id")
|
||||
+ ",游标 keyset 分页;数据来自业务表 "
|
||||
+ ("TODO".equals(entity) ? "bu_todo" : "bu_todo_completed") + ",不读 Flowable act_* 表。");
|
||||
metadata.put("query_fields", mapOf(
|
||||
"entity", "TODO=待办 / DONE=已办",
|
||||
"approve_user", "审批人(bu_todo.approve_user,用户ID);默认当前登录用户ID,ALL=不过滤审批人",
|
||||
"process_key_list", "流程KEY列表,≤" + MAX_PROCESS_KEY,
|
||||
"business_key_list", "业务主键(合同编号/采购单号等)列表,≤" + McpPages.MAX_CODE_LIST,
|
||||
"time_range", "start~end;TODO 基于 apply_time,DONE 基于 approve_time;默认近 " + DEFAULT_MONTHS
|
||||
+ " 个月、上限 " + MAX_MONTHS + " 个月",
|
||||
"include_total", "是否统计总数(默认 false,上限 " + COUNT_CAP + ")",
|
||||
"page_size", "每页条数(默认 " + McpPages.DEFAULT_PAGE_SIZE + ",上限 " + McpPages.MAX_LIST_PAGE_SIZE + ")",
|
||||
"cursor", "游标:回传上次的 page_info.next_cursor"));
|
||||
metadata.put("data_fields", mapOf(
|
||||
"total", "include_total=true 时为真实命中条数,否则为当前页条数",
|
||||
"items", "待办/已办列表",
|
||||
"page_info", "分页信息:returned/page_size/has_more/next_cursor/sort_by/page_no/total_count/total_count_capped"));
|
||||
metadata.put("item_fields", itemFields());
|
||||
metadata.put("process_key_coverage", mapOf(
|
||||
"order_approve_online", "订单审批(线上)",
|
||||
"order_approve_offline", "订单审批(线下)",
|
||||
"purchase_order_online", "采购单审批",
|
||||
"finance_payment", "付款审批",
|
||||
"fianance_ticket", "收票审批(⚠️ 源码/数据里的拼写错误,少一个 n,匹配必须按原样,不要写成 finance_ticket)",
|
||||
"order_reback", "订单撤回",
|
||||
"outer_reback", "出库撤回"));
|
||||
metadata.put("approve_status_semantics", "⚠️ 实测 approve_status 语义与常见枚举相反:3=通过、2=驳回、1=待审批;"
|
||||
+ "approveStatusName 按此口径翻译。");
|
||||
metadata.put("usage_questions", "该工具回答:我还有哪些单要审 / 现在卡在谁那儿(task_name 当前节点 + approve_user_name)"
|
||||
+ "/ 为什么被驳回(DONE 的 approve_opinion + approve_status=2)/ 审批耗了多久(DONE 的 approve_time - apply_time)。");
|
||||
metadata.put("notes", "① entity=DONE 的条目可能同一 todo_id 出现多条(bu_todo_completed 中 todo_id 不唯一),"
|
||||
+ "排序键以 id 作为唯一项;② bu_todo / bu_todo_completed 实测没有 business_id / business_name / "
|
||||
+ "project_code 列(实体里的同名字段为非常规冗余字段),故未回显业务名称与项目编号,"
|
||||
+ "如需业务名称请用 business_key 调用其它工具(如 project_list(entity=CONTRACT));"
|
||||
+ "③ 时间条件为区间下推(>= start and < end),不使用 date_format;"
|
||||
+ "④ 默认时间区间为近 12 个月,超过 36 个月会报错。");
|
||||
metadata.put("permission_source", "TodoController 的 /flow/todo/list、/flow/completed/list 无 @RequiresPermissions"
|
||||
+ "(仅登录);取审批入口权限组合(任意一个满足):sip:purchaseorder:list"
|
||||
+ "(OmsPurchaseOrderController#approveList / #approvedList)、finance:payment:approve"
|
||||
+ "(OmsPaymentBillController#approveList / #approvedList)、finance:payment:list(OmsPaymentBillController#list)");
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> itemFields() {
|
||||
return mapOf(
|
||||
"todoId", "流程ID(bu_todo.todo_id,Flowable 任务ID)",
|
||||
"processInstanceId", "流程实例ID",
|
||||
"taskId", "任务ID",
|
||||
"businessKey", "业务主键(合同编号 / 采购单号等)",
|
||||
"processKey", "流程KEY(见 metadata.process_key_coverage)",
|
||||
"processName", "流程名称",
|
||||
"taskName", "当前节点名称(TODO 为待审节点;DONE 为已审节点)",
|
||||
"approveUser", "审批人ID(bu_todo.approve_user)",
|
||||
"approveUserName", "审批人姓名",
|
||||
"allApproveUserName", "所有审批人姓名(仅 DONE)",
|
||||
"applyUserName", "发起人姓名",
|
||||
"applyTime", "发起时间",
|
||||
"approveTime", "审批时间(仅 DONE)",
|
||||
"approveOpinion", "审批意见(仅 DONE,可用于回答“为什么被驳回”)",
|
||||
"approveStatus", "审批结果编码(仅 DONE):3=通过、2=驳回、1=待审批",
|
||||
"approveStatusName", "审批结果名称(仅 DONE)",
|
||||
"formKey", "节点表单KEY",
|
||||
"extendField1", "扩展字段1",
|
||||
"extendField2", "扩展字段2",
|
||||
"extendField3", "扩展字段3");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,674 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpTimeRange;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.CrossDomainAggregateMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MCP 工具 {@code cross_domain_aggregate}(受限跨域透视,只读)。
|
||||
*
|
||||
* <p>方案依据:{@code docx/mcp-data-tools-plan.md} 第 4.4(D 类第 13 个工具)/ 第 7 章第 18 项 / 第 9 章(聚合口径)/
|
||||
* 第 13 章(一次算完,禁止 Agent 翻页累加)/ 第 15 章(时间区间下推、类型规范化、include_zero 判定)/ 16.2(白名单与护栏)。</p>
|
||||
*
|
||||
* <p>★能力边界(16.2 原文:真正的“任意维度”等价于开放任意 SQL,安全与性能都不可控,故只做受限透视):</p>
|
||||
* <ol>
|
||||
* <li>维度与度量均来自<b>白名单</b>,超出白名单直接 {@code INVALID_PARAMS} 并回显允许值;</li>
|
||||
* <li><b>单链路约束</b>:一次请求的 metrics 必须属于同一条链路(SALES / PURCHASE / STOCK / FINANCE_AR / FINANCE_AP),
|
||||
* 跨链路混用直接报错并说明各 metric 的链路归属;</li>
|
||||
* <li>{@code time_range} <b>必填</b>(本工具不默认近 12 个月),跨度上限 36 个月,且一律<b>区间下推</b>
|
||||
* ({@code >= start and < end}),禁止 {@code date_format()} 作为过滤条件;</li>
|
||||
* <li>{@code group_limit} 默认 200 / 上限 1000,{@code top_n} 默认 100 / 上限 1000,超出直接报错并提示收窄;</li>
|
||||
* <li>不开放任意 SQL:维度表达式与排序度量在 XML 内以 {@code <choose>} 映射为固定表达式,模型无法传入任何 SQL 片段。</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>权限来源(按链路做最小权限校验,逐个从 Controller 的 {@code @RequiresPermissions} 抄取):</p>
|
||||
* <ul>
|
||||
* <li>SALES → {@code ProjectOrderInfoController}({@code project:order:list} 见其 list 接口、{@code project:order:view})与
|
||||
* {@code ProjectInfoController}({@code sip:project:list}、{@code sip:project:view});</li>
|
||||
* <li>PURCHASE → {@code OmsPurchaseOrderController#list} 的 {@code sip:purchaseorder:list}(已实测抄取);</li>
|
||||
* <li>STOCK → {@code InventoryInfoController#view} 的 {@code inventory:info:view}
|
||||
* (该 Controller 的 list 接口权限注解已被注释掉,故取 view 接口的权限串;与既有 inventory_stock_aggregate 一致);</li>
|
||||
* <li>FINANCE_AR → {@code OmsReceivableBillController#list} 的 {@code finance:receivable:list}(已实测抄取);</li>
|
||||
* <li>FINANCE_AP → {@code OmsPayableBillController#list} 的 {@code finance:payable:list}(已实测抄取)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>数据链路与表清单见 {@code CrossDomainAggregateMapper} 与其 XML(本工具自包含,未改动任何既有 Mapper/工具类)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class CrossDomainAggregateToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
/** 维度全局白名单(16.2) */
|
||||
private static final List<String> DIMENSION_WHITELIST = Collections.unmodifiableList(
|
||||
Arrays.asList("PARTNER", "CUSTOMER", "AGENT", "PRODUCT", "PROJECT", "WAREHOUSE", "MONTH", "QUARTER"));
|
||||
|
||||
private static final int MAX_DIMENSIONS = 3;
|
||||
private static final int MAX_TIME_MONTHS = 36;
|
||||
private static final int DEFAULT_GROUP_LIMIT = 200;
|
||||
private static final int MAX_GROUP_LIMIT = 1000;
|
||||
private static final int DEFAULT_TOP_N = 100;
|
||||
private static final int MAX_TOP_N = 1000;
|
||||
private static final int MAX_METRICS = 16;
|
||||
|
||||
/** metric → 链路(顺序即回显顺序) */
|
||||
private static final Map<String, String> METRIC_LINK;
|
||||
|
||||
/** metric → 响应字段名(驼峰,与域内专用工具命名保持一致) */
|
||||
private static final Map<String, String> METRIC_ALIAS;
|
||||
|
||||
/** 链路 → 该链路支持的维度(16.2 + 实表列核实结论) */
|
||||
private static final Map<String, List<String>> LINK_DIMENSIONS;
|
||||
|
||||
/** 链路 → 涉及的表清单(写入 data.lineage 与 metadata.lineage) */
|
||||
private static final Map<String, List<String>> LINK_TABLES;
|
||||
|
||||
/** 链路 → 时间字段(区间下推依据,方案 15.1) */
|
||||
private static final Map<String, String> LINK_TIME_FIELD;
|
||||
|
||||
static {
|
||||
Map<String, String> metricLink = new LinkedHashMap<>();
|
||||
metricLink.put("SALES_AMOUNT_WITH_TAX", "SALES");
|
||||
metricLink.put("SALES_AMOUNT_WITHOUT_TAX", "SALES");
|
||||
metricLink.put("PRODUCT_QTY", "SALES");
|
||||
metricLink.put("PURCHASE_QTY", "PURCHASE");
|
||||
metricLink.put("PURCHASE_AMOUNT_WITH_TAX", "PURCHASE");
|
||||
metricLink.put("PURCHASE_TAX", "PURCHASE");
|
||||
metricLink.put("IN_STOCK_QTY", "STOCK");
|
||||
metricLink.put("OUT_STOCK_QTY", "STOCK");
|
||||
metricLink.put("RECEIVABLE_WITH_TAX", "FINANCE_AR");
|
||||
metricLink.put("RECEIVED_WITH_TAX", "FINANCE_AR");
|
||||
metricLink.put("UNRECEIVED_WITH_TAX", "FINANCE_AR");
|
||||
metricLink.put("UNINVOICED_WITH_TAX", "FINANCE_AR");
|
||||
metricLink.put("PAYABLE_WITH_TAX", "FINANCE_AP");
|
||||
metricLink.put("PAID_WITH_TAX", "FINANCE_AP");
|
||||
metricLink.put("UNPAID_WITH_TAX", "FINANCE_AP");
|
||||
metricLink.put("UNTICKETED_WITH_TAX", "FINANCE_AP");
|
||||
METRIC_LINK = Collections.unmodifiableMap(metricLink);
|
||||
|
||||
Map<String, String> alias = new LinkedHashMap<>();
|
||||
alias.put("SALES_AMOUNT_WITH_TAX", "salesAmountWithTax");
|
||||
alias.put("SALES_AMOUNT_WITHOUT_TAX", "salesAmountWithoutTax");
|
||||
alias.put("PRODUCT_QTY", "productQty");
|
||||
alias.put("PURCHASE_QTY", "purchaseQty");
|
||||
alias.put("PURCHASE_AMOUNT_WITH_TAX", "purchaseAmountWithTax");
|
||||
alias.put("PURCHASE_TAX", "purchaseTax");
|
||||
alias.put("IN_STOCK_QTY", "inStockQty");
|
||||
alias.put("OUT_STOCK_QTY", "outStockQty");
|
||||
alias.put("RECEIVABLE_WITH_TAX", "receivableWithTax");
|
||||
alias.put("RECEIVED_WITH_TAX", "receivedWithTax");
|
||||
alias.put("UNRECEIVED_WITH_TAX", "unreceivedWithTax");
|
||||
alias.put("UNINVOICED_WITH_TAX", "uninvoicedWithTax");
|
||||
alias.put("PAYABLE_WITH_TAX", "payableWithTax");
|
||||
alias.put("PAID_WITH_TAX", "paidWithTax");
|
||||
alias.put("UNPAID_WITH_TAX", "unpaidWithTax");
|
||||
alias.put("UNTICKETED_WITH_TAX", "unticketedWithTax");
|
||||
METRIC_ALIAS = Collections.unmodifiableMap(alias);
|
||||
|
||||
Map<String, List<String>> dims = new LinkedHashMap<>();
|
||||
dims.put("SALES", Collections.unmodifiableList(
|
||||
Arrays.asList("CUSTOMER", "AGENT", "PRODUCT", "PROJECT", "MONTH", "QUARTER")));
|
||||
dims.put("PURCHASE", Collections.unmodifiableList(
|
||||
Arrays.asList("PRODUCT", "PARTNER", "MONTH", "QUARTER")));
|
||||
dims.put("STOCK", Collections.unmodifiableList(
|
||||
Arrays.asList("PRODUCT", "WAREHOUSE", "MONTH", "QUARTER")));
|
||||
dims.put("FINANCE_AR", Collections.unmodifiableList(
|
||||
Arrays.asList("CUSTOMER", "MONTH", "QUARTER")));
|
||||
dims.put("FINANCE_AP", Collections.unmodifiableList(
|
||||
Arrays.asList("PARTNER", "MONTH", "QUARTER")));
|
||||
LINK_DIMENSIONS = Collections.unmodifiableMap(dims);
|
||||
|
||||
Map<String, List<String>> tables = new LinkedHashMap<>();
|
||||
tables.put("SALES", Collections.unmodifiableList(Arrays.asList(
|
||||
"project_product_info", "project_order_info", "project_info",
|
||||
"product_info", "customer_info", "agent_info")));
|
||||
tables.put("PURCHASE", Collections.unmodifiableList(Arrays.asList(
|
||||
"oms_purchase_order_item", "oms_purchase_order", "oms_vendor_info", "product_info")));
|
||||
tables.put("STOCK", Collections.unmodifiableList(Arrays.asList(
|
||||
"oms_inventory_info", "product_info", "oms_warehouse_info")));
|
||||
tables.put("FINANCE_AR", Collections.unmodifiableList(Arrays.asList("oms_receivable_bill")));
|
||||
tables.put("FINANCE_AP", Collections.unmodifiableList(Arrays.asList("oms_payable_bill")));
|
||||
LINK_TABLES = Collections.unmodifiableMap(tables);
|
||||
|
||||
Map<String, String> timeField = new LinkedHashMap<>();
|
||||
timeField.put("SALES", "project_order_info.create_time");
|
||||
timeField.put("PURCHASE", "oms_purchase_order.purchase_date");
|
||||
timeField.put("STOCK", "oms_inventory_info.create_time");
|
||||
timeField.put("FINANCE_AR", "oms_receivable_bill.create_time");
|
||||
timeField.put("FINANCE_AP", "oms_payable_bill.create_time");
|
||||
LINK_TIME_FIELD = Collections.unmodifiableMap(timeField);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private CrossDomainAggregateMapper crossDomainAggregateMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "cross_domain_aggregate";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "受限跨域透视(只读,兜底能力):按 dimensions(1~3 个白名单维度,如 CUSTOMER/PRODUCT/MONTH)"
|
||||
+ "对同一条数据链路的 metrics 做一次分组聚合,返回 Top-N 分组 + 全局分组数,不需要翻页累加。"
|
||||
+ "链路与度量必须同源(SALES/PURCHASE/STOCK/FINANCE_AR/FINANCE_AP),跨链路混用会被拒绝;"
|
||||
+ "time_range 必填(≤36 个月),group_limit 默认 200/上限 1000,top_n 默认 100/上限 1000。"
|
||||
+ "无法表达的维度组合请改用域内专用工具(如 project_list / finance_list / warehouse_list)。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("dimensions", arrayProperty("维度(必填,1~3 个,白名单):CUSTOMER 客户 / AGENT 代表处 / PRODUCT 产品 / "
|
||||
+ "PROJECT 项目 / PARTNER 合作伙伴(采购侧=制造商、应付侧=制造商)/ WAREHOUSE 仓库 / MONTH 月(yyyy-MM)/ "
|
||||
+ "QUARTER 季度(yyyyQn)。各链路可用维度:SALES=" + LINK_DIMENSIONS.get("SALES")
|
||||
+ ",PURCHASE=" + LINK_DIMENSIONS.get("PURCHASE")
|
||||
+ ",STOCK=" + LINK_DIMENSIONS.get("STOCK")
|
||||
+ ",FINANCE_AR=" + LINK_DIMENSIONS.get("FINANCE_AR")
|
||||
+ ",FINANCE_AP=" + LINK_DIMENSIONS.get("FINANCE_AP")));
|
||||
properties.put("metrics", arrayProperty("度量(必填,白名单,且必须属于同一条链路):"
|
||||
+ "SALES=SALES_AMOUNT_WITH_TAX/SALES_AMOUNT_WITHOUT_TAX/PRODUCT_QTY;"
|
||||
+ "PURCHASE=PURCHASE_QTY/PURCHASE_AMOUNT_WITH_TAX/PURCHASE_TAX;"
|
||||
+ "STOCK=IN_STOCK_QTY/OUT_STOCK_QTY;"
|
||||
+ "FINANCE_AR=RECEIVABLE_WITH_TAX/RECEIVED_WITH_TAX/UNRECEIVED_WITH_TAX/UNINVOICED_WITH_TAX;"
|
||||
+ "FINANCE_AP=PAYABLE_WITH_TAX/PAID_WITH_TAX/UNPAID_WITH_TAX/UNTICKETED_WITH_TAX"));
|
||||
properties.put("time_range", stringProperty("时间区间(必填,本工具不提供默认区间):格式 \"开始~结束\"(yyyy-MM-dd),"
|
||||
+ "如 2026-01-01~2026-06-30;跨度上限 " + MAX_TIME_MONTHS + " 个月;一律区间下推"));
|
||||
properties.put("group_limit", intProperty("分组数上限(默认 " + DEFAULT_GROUP_LIMIT + ",上限 " + MAX_GROUP_LIMIT
|
||||
+ "):超出会报错,请缩小 time_range 或减少维度"));
|
||||
properties.put("top_n", intProperty("按本次第一个 metric 降序取前 N 组(默认 " + DEFAULT_TOP_N + ",上限 " + MAX_TOP_N + ")"));
|
||||
properties.put("include_zero", boolProperty("是否返回全零分组(默认 false):本次请求的全部 metrics 均为 0/null 的分组会被过滤"));
|
||||
return objectSchema(properties, "dimensions", "metrics", "time_range");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
List<String> dimensions = parseDimensions(params);
|
||||
List<String> metrics = parseMetrics(params);
|
||||
String link = resolveLink(metrics);
|
||||
// 权限:按链路做最小权限校验(权限串来源见类注释)——尽可能早做,权限不足返回 AUTH_ERROR,不返回空列表
|
||||
McpAuth.requireAnyPermission(permissionsOf(link));
|
||||
assertDimensionsSupported(link, dimensions);
|
||||
|
||||
// 时间区间:必填(不能默认近 12 个月),跨度上限 36 个月
|
||||
McpTimeRange timeRange = McpParams.timeRange(params, "time_range", 0, MAX_TIME_MONTHS);
|
||||
if (timeRange.getStart() == null || timeRange.getEnd() == null) {
|
||||
throw McpToolException.invalidParams("time_range 为必填且必须同时给出起止时间,格式 \"开始~结束\"(yyyy-MM-dd,如 2026-01-01~2026-06-30);"
|
||||
+ "本工具不提供默认区间,也不接受开放区间(原因:链路时间列无索引,无界扫描会拖垮库,见方案 16.2 护栏 1)");
|
||||
}
|
||||
|
||||
int groupLimit = McpParams.intVal(params, "group_limit", DEFAULT_GROUP_LIMIT);
|
||||
if (groupLimit < 1 || groupLimit > MAX_GROUP_LIMIT) {
|
||||
throw McpToolException.invalidParams("group_limit 取值非法:" + groupLimit + ",允许范围 1~" + MAX_GROUP_LIMIT
|
||||
+ ";超出请缩小 time_range 或减少维度");
|
||||
}
|
||||
int topN = McpParams.intVal(params, "top_n", DEFAULT_TOP_N);
|
||||
if (topN < 1 || topN > MAX_TOP_N) {
|
||||
throw McpToolException.invalidParams("top_n 取值非法:" + topN + ",允许范围 1~" + MAX_TOP_N);
|
||||
}
|
||||
boolean includeZero = McpParams.boolVal(params, "include_zero", false);
|
||||
|
||||
String d0 = dimensions.get(0);
|
||||
String d1 = dimensions.size() > 1 ? dimensions.get(1) : null;
|
||||
String d2 = dimensions.size() > 2 ? dimensions.get(2) : null;
|
||||
String orderMetric = metrics.get(0);
|
||||
|
||||
// 多取 1 组用于探测是否被 group_limit 截断
|
||||
List<Map<String, Object>> rows = query(link, d0, d1, d2, orderMetric,
|
||||
timeRange.getStart(), timeRange.getEnd(), groupLimit + 1);
|
||||
boolean groupTruncated = rows.size() > groupLimit;
|
||||
List<Map<String, Object>> limited = groupTruncated
|
||||
? new ArrayList<>(rows.subList(0, groupLimit))
|
||||
: rows;
|
||||
|
||||
// include_zero=false 时按本次请求的 metrics 集合过滤全零行(方案 15.6)
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Map<String, Object> row : limited) {
|
||||
Map<String, Object> item = toItem(dimensions, metrics, row);
|
||||
if (!includeZero && isAllZero(item, metrics)) {
|
||||
continue;
|
||||
}
|
||||
items.add(item);
|
||||
}
|
||||
boolean topTruncated = items.size() > topN;
|
||||
if (topTruncated) {
|
||||
items = new ArrayList<>(items.subList(0, topN));
|
||||
}
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("link", link);
|
||||
query.put("dimensions", dimensions);
|
||||
query.put("metrics", metrics);
|
||||
query.put("time_range", McpParams.describe(timeRange));
|
||||
query.put("time_field", LINK_TIME_FIELD.get(link));
|
||||
query.put("group_limit", groupLimit);
|
||||
query.put("top_n", topN);
|
||||
query.put("include_zero", includeZero);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("items", items);
|
||||
data.put("groupCount", items.size());
|
||||
data.put("truncated", groupTruncated || topTruncated);
|
||||
data.put("lineage", lineage(link, dimensions));
|
||||
|
||||
return response(buildMetadata(), query, data);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 入参校验
|
||||
|
||||
private List<String> parseDimensions(Map<String, Object> params) {
|
||||
List<String> raw = McpParams.codeList(params, "dimensions", MAX_DIMENSIONS);
|
||||
if (raw.isEmpty()) {
|
||||
throw McpToolException.invalidParams("缺少必填参数:dimensions(1~" + MAX_DIMENSIONS + " 个),允许值:"
|
||||
+ DIMENSION_WHITELIST + ";示例 [\"CUSTOMER\",\"PRODUCT\",\"MONTH\"]");
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String value : raw) {
|
||||
String dim = value.toUpperCase(Locale.ROOT);
|
||||
if (!DIMENSION_WHITELIST.contains(dim)) {
|
||||
throw McpToolException.invalidParams("dimensions 取值非法:" + value + ",允许值:" + DIMENSION_WHITELIST
|
||||
+ "(本工具不开放任意维度,等价于任意 SQL,见方案 16.2)");
|
||||
}
|
||||
if (result.contains(dim)) {
|
||||
throw McpToolException.invalidParams("dimensions 不允许重复:" + dim + ",当前:" + raw);
|
||||
}
|
||||
result.add(dim);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<String> parseMetrics(Map<String, Object> params) {
|
||||
List<String> raw = McpParams.codeList(params, "metrics", MAX_METRICS);
|
||||
if (raw.isEmpty()) {
|
||||
throw McpToolException.invalidParams("缺少必填参数:metrics,允许值(按链路分组):" + describeMetricWhitelist());
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String value : raw) {
|
||||
String metric = value.toUpperCase(Locale.ROOT);
|
||||
if (!METRIC_LINK.containsKey(metric)) {
|
||||
throw McpToolException.invalidParams("metrics 取值非法:" + value + ",允许值(按链路分组):" + describeMetricWhitelist());
|
||||
}
|
||||
if (result.contains(metric)) {
|
||||
throw McpToolException.invalidParams("metrics 不允许重复:" + metric + ",当前:" + raw);
|
||||
}
|
||||
result.add(metric);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String resolveLink(List<String> metrics) {
|
||||
Set<String> links = new LinkedHashSet<>();
|
||||
for (String metric : metrics) {
|
||||
links.add(METRIC_LINK.get(metric));
|
||||
}
|
||||
if (links.size() > 1) {
|
||||
StringBuilder detail = new StringBuilder();
|
||||
for (String metric : metrics) {
|
||||
if (detail.length() > 0) {
|
||||
detail.append("、");
|
||||
}
|
||||
detail.append(metric).append("→").append(METRIC_LINK.get(metric));
|
||||
}
|
||||
throw McpToolException.invalidParams("metrics 跨链路混用:" + detail
|
||||
+ "。一条请求只能命中一条数据链路(本工具为受限透视,不做跨表 join 拼装);"
|
||||
+ "如需同时看销售与未收款,请分两次调用。各链路可用度量:" + describeMetricWhitelist());
|
||||
}
|
||||
return links.iterator().next();
|
||||
}
|
||||
|
||||
private void assertDimensionsSupported(String link, List<String> dimensions) {
|
||||
List<String> supported = LINK_DIMENSIONS.get(link);
|
||||
List<String> unsupported = new ArrayList<>();
|
||||
for (String dim : dimensions) {
|
||||
if (!supported.contains(dim)) {
|
||||
unsupported.add(dim);
|
||||
}
|
||||
}
|
||||
if (!unsupported.isEmpty()) {
|
||||
throw McpToolException.invalidParams("链路 " + link + " 不支持维度 " + unsupported
|
||||
+ ";该链路可用维度:" + supported + "(链路与可用维度对应关系:" + describeLinkDimensions() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 查询与组装
|
||||
|
||||
private List<Map<String, Object>> query(String link, String d0, String d1, String d2, String orderMetric,
|
||||
Date startTime, Date endTime, int limit) {
|
||||
List<Map<String, Object>> rows;
|
||||
if ("SALES".equals(link)) {
|
||||
rows = crossDomainAggregateMapper.selectSalesAggregate(d0, d1, d2, orderMetric, startTime, endTime, limit);
|
||||
} else if ("PURCHASE".equals(link)) {
|
||||
rows = crossDomainAggregateMapper.selectPurchaseAggregate(d0, d1, d2, orderMetric, startTime, endTime, limit);
|
||||
} else if ("STOCK".equals(link)) {
|
||||
rows = crossDomainAggregateMapper.selectStockAggregate(d0, d1, d2, orderMetric, startTime, endTime, limit);
|
||||
} else if ("FINANCE_AR".equals(link)) {
|
||||
rows = crossDomainAggregateMapper.selectReceivableAggregate(d0, d1, d2, orderMetric, startTime, endTime, limit);
|
||||
} else {
|
||||
rows = crossDomainAggregateMapper.selectPayableAggregate(d0, d1, d2, orderMetric, startTime, endTime, limit);
|
||||
}
|
||||
return rows == null ? Collections.<Map<String, Object>>emptyList() : rows;
|
||||
}
|
||||
|
||||
private Map<String, Object> toItem(List<String> dimensions, List<String> metrics, Map<String, Object> row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
Map<String, Object> dimensionMap = new LinkedHashMap<>();
|
||||
|
||||
for (int i = 0; i < dimensions.size(); i++) {
|
||||
String dim = dimensions.get(i);
|
||||
String codeKey = "d" + i;
|
||||
String nameKey = "d" + i + "Name";
|
||||
String code = McpFormat.trimToNull(stringValue(row.get(codeKey)));
|
||||
String name = McpFormat.trimToNull(stringValue(row.get(nameKey)));
|
||||
dimensionMap.put(dim.toLowerCase(Locale.ROOT), code);
|
||||
|
||||
if ("MONTH".equals(dim) || "QUARTER".equals(dim)) {
|
||||
item.put(dim.toLowerCase(Locale.ROOT), code);
|
||||
} else if ("WAREHOUSE".equals(dim)) {
|
||||
item.put("warehouseId", longVal(row.get(codeKey)));
|
||||
item.put("warehouseName", name);
|
||||
} else {
|
||||
item.put(dim.toLowerCase(Locale.ROOT) + "Code", code);
|
||||
item.put(dim.toLowerCase(Locale.ROOT) + "Name", name);
|
||||
}
|
||||
}
|
||||
|
||||
item.put("dimensionMap", dimensionMap);
|
||||
for (String metric : metrics) {
|
||||
String alias = METRIC_ALIAS.get(metric);
|
||||
item.put(alias, decimal(row.get(alias)));
|
||||
}
|
||||
item.put("rowQty", longVal(row.get("rowQty")));
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* include_zero=false 时判定“全零行”:本次请求的全部 metrics 均为 0 或 null(方案 15.6)。
|
||||
*/
|
||||
private boolean isAllZero(Map<String, Object> item, List<String> metrics) {
|
||||
for (String metric : metrics) {
|
||||
BigDecimal value = decimal(item.get(METRIC_ALIAS.get(metric)));
|
||||
if (value.compareTo(BigDecimal.ZERO) != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Map<String, Object> lineage(String link, List<String> dimensions) {
|
||||
Map<String, Object> lineage = new LinkedHashMap<>();
|
||||
lineage.put("link", link);
|
||||
lineage.put("tables", LINK_TABLES.get(link));
|
||||
lineage.put("time_field", LINK_TIME_FIELD.get(link));
|
||||
Map<String, Object> dimensionFields = new LinkedHashMap<>();
|
||||
for (String dim : dimensions) {
|
||||
dimensionFields.put(dim.toLowerCase(Locale.ROOT), dimensionFieldOf(link, dim));
|
||||
}
|
||||
lineage.put("dimension_fields", dimensionFields);
|
||||
lineage.put("join_note", joinNoteOf(link));
|
||||
return lineage;
|
||||
}
|
||||
|
||||
private String dimensionFieldOf(String link, String dim) {
|
||||
if ("SALES".equals(link)) {
|
||||
if ("CUSTOMER".equals(dim)) {
|
||||
return "project_info.customer_code(名称 customer_info.customer_name)";
|
||||
}
|
||||
if ("AGENT".equals(dim)) {
|
||||
return "project_info.agent_code(名称 agent_info.agent_name)";
|
||||
}
|
||||
if ("PRODUCT".equals(dim)) {
|
||||
return "project_product_info.product_bom_code(名称 product_info.product_name)";
|
||||
}
|
||||
if ("PROJECT".equals(dim)) {
|
||||
return "project_info.project_code(名称 project_info.project_name)";
|
||||
}
|
||||
return "project_order_info.create_time(" + ("MONTH".equals(dim) ? "yyyy-MM" : "yyyyQn") + ")";
|
||||
}
|
||||
if ("PURCHASE".equals(link)) {
|
||||
if ("PRODUCT".equals(dim)) {
|
||||
return "oms_purchase_order_item.product_code(名称 product_info.product_name)";
|
||||
}
|
||||
if ("PARTNER".equals(dim)) {
|
||||
return "oms_vendor_info.vendor_code(经 oms_purchase_order.vendor_id 关联;名称 oms_vendor_info.vendor_name)";
|
||||
}
|
||||
return "oms_purchase_order.purchase_date(" + ("MONTH".equals(dim) ? "yyyy-MM" : "yyyyQn") + ")";
|
||||
}
|
||||
if ("STOCK".equals(link)) {
|
||||
if ("PRODUCT".equals(dim)) {
|
||||
return "oms_inventory_info.product_code(名称 product_info.product_name)";
|
||||
}
|
||||
if ("WAREHOUSE".equals(dim)) {
|
||||
return "oms_inventory_info.warehouse_id(名称 oms_warehouse_info.warehouse_name)";
|
||||
}
|
||||
return "oms_inventory_info.create_time(" + ("MONTH".equals(dim) ? "yyyy-MM" : "yyyyQn") + ")";
|
||||
}
|
||||
if ("FINANCE_AR".equals(link)) {
|
||||
if ("CUSTOMER".equals(dim)) {
|
||||
return "oms_receivable_bill.partner_code(名称取冗余列 partner_name)";
|
||||
}
|
||||
return "oms_receivable_bill.create_time(" + ("MONTH".equals(dim) ? "yyyy-MM" : "yyyyQn") + ")";
|
||||
}
|
||||
if ("PARTNER".equals(dim)) {
|
||||
return "oms_payable_bill.vendor_code(名称取冗余列 vendor_name;★此处 partner 语义为制造商)";
|
||||
}
|
||||
return "oms_payable_bill.create_time(" + ("MONTH".equals(dim) ? "yyyy-MM" : "yyyyQn") + ")";
|
||||
}
|
||||
|
||||
private String joinNoteOf(String link) {
|
||||
if ("SALES".equals(link)) {
|
||||
return "project_product_info ⋈ project_order_info(project_id)⋈ project_info(project_id):"
|
||||
+ "project_info 关联沿用既有 SQL(ProjectOrderInfoMapper.selectProjectOrderInfoRelationVo)的 project_id 主键关联;"
|
||||
+ "★任务描述建议按 order_code 对齐,但 project_info.order_code 可能为空会导致大量丢关联,故采用 project_id(已在回报中标注为偏离项)";
|
||||
}
|
||||
if ("PURCHASE".equals(link)) {
|
||||
return "oms_purchase_order_item ⋈ oms_purchase_order(i.purchase_id = o.id);"
|
||||
+ "名称仅在请求对应维度时 left join(product_info / oms_vendor_info)";
|
||||
}
|
||||
if ("STOCK".equals(link)) {
|
||||
return "oms_inventory_info 单表;名称仅在请求对应维度时 left join(product_info / oms_warehouse_info)";
|
||||
}
|
||||
return "单表聚合,金额直接 sum 冗余列,不重算、不关联明细表";
|
||||
}
|
||||
|
||||
private String[] permissionsOf(String link) {
|
||||
if ("SALES".equals(link)) {
|
||||
return new String[]{"project:order:list", "project:order:view", "sip:project:list", "sip:project:view"};
|
||||
}
|
||||
if ("PURCHASE".equals(link)) {
|
||||
return new String[]{"sip:purchaseorder:list"};
|
||||
}
|
||||
if ("STOCK".equals(link)) {
|
||||
return new String[]{"inventory:info:view"};
|
||||
}
|
||||
if ("FINANCE_AR".equals(link)) {
|
||||
return new String[]{"finance:receivable:list"};
|
||||
}
|
||||
return new String[]{"finance:payable:list"};
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ metadata
|
||||
|
||||
private Map<String, Object> buildMetadata() {
|
||||
Map<String, Object> metadata = metadata("受限跨域透视:按 dimensions(1~3 个白名单维度)对同一条链路的 metrics 做一次分组聚合,"
|
||||
+ "返回 Top-N 分组 + 分组数 + 链路血缘;链路为 SALES / PURCHASE / STOCK / FINANCE_AR / FINANCE_AP,度量不可跨链路混用。");
|
||||
|
||||
metadata.put("query_fields", mapOf(
|
||||
"dimensions", "维度(必填,1~3 个,白名单):" + DIMENSION_WHITELIST,
|
||||
"metrics", "度量(必填,必须同属一条链路):" + describeMetricWhitelist(),
|
||||
"time_range", "时间区间(必填)\"开始~结束\"(yyyy-MM-dd),跨度上限 " + MAX_TIME_MONTHS + " 个月",
|
||||
"group_limit", "分组数上限(默认 " + DEFAULT_GROUP_LIMIT + ",上限 " + MAX_GROUP_LIMIT + ")",
|
||||
"top_n", "按第一个 metric 降序取前 N 组(默认 " + DEFAULT_TOP_N + ",上限 " + MAX_TOP_N + ")",
|
||||
"include_zero", "是否返回全零分组(默认 false,按本次 metrics 集合判定)"));
|
||||
metadata.put("dimensions", mapData(
|
||||
"whitelist", DIMENSION_WHITELIST,
|
||||
"by_link", LINK_DIMENSIONS,
|
||||
"time_bucket_format", "MONTH → yyyy-MM(如 2026-01);QUARTER → yyyyQn(如 2026Q1)"));
|
||||
metadata.put("metrics", mapData(
|
||||
"by_link", metricWhitelistByLink(),
|
||||
"response_fields", METRIC_ALIAS));
|
||||
metadata.put("data_fields", mapOf(
|
||||
"items", "分组结果,每行 = 一个维度组合 + 各 metric 值 + rowQty(参与聚合的原始行数)",
|
||||
"groupCount", "本次返回的分组数(已按 include_zero / top_n 处理)",
|
||||
"truncated", "是否被截断:true 表示命中 group_limit(分组过多)或 top_n(按第一个 metric 取前 N)",
|
||||
"lineage", "本次命中的链路、涉及表清单、时间字段、各维度的来源字段与关联说明"));
|
||||
metadata.put("item_fields", mapOf(
|
||||
"dimensionMap", "维度键→维度值(键为维度名小写,如 customer/product/month,值为编码或时间桶;时间桶为字符串)",
|
||||
"customerCode/customerName", "客户编码/名称(维度 CUSTOMER)",
|
||||
"agentCode/agentName", "代表处编码/名称(维度 AGENT,SALES)",
|
||||
"productCode/productName", "产品编码/名称(维度 PRODUCT)",
|
||||
"projectCode/projectName", "项目编码/名称(维度 PROJECT,SALES)",
|
||||
"partnerCode/partnerName", "合作伙伴编码/名称(维度 PARTNER:采购侧=制造商、应付侧=制造商)",
|
||||
"warehouseId/warehouseName", "仓库ID/名称(维度 WAREHOUSE,STOCK)",
|
||||
"month", "月份桶 yyyy-MM(维度 MONTH)",
|
||||
"quarter", "季度桶 yyyyQn(维度 QUARTER)",
|
||||
"salesAmountWithTax", "SALES:含税销售额(Σ project_product_info.all_price)",
|
||||
"salesAmountWithoutTax", "SALES:未税销售额(Σ all_price/(1+税率),税率>1 视为百分数)",
|
||||
"productQty", "SALES:产品数量(Σ project_product_info.quantity)",
|
||||
"purchaseQty", "PURCHASE:采购数量(Σ oms_purchase_order_item.quantity)",
|
||||
"purchaseAmountWithTax", "PURCHASE:含税金额(Σ item.amount_total)",
|
||||
"purchaseTax", "PURCHASE:税额(Σ item.tax_total)",
|
||||
"inStockQty", "STOCK:在库数量(count inventory_status='0')",
|
||||
"outStockQty", "STOCK:已出库数量(count inventory_status='1')",
|
||||
"receivableWithTax", "FINANCE_AR:应收含税总额(Σ total_price_with_tax)",
|
||||
"receivedWithTax", "FINANCE_AR:已收含税金额(Σ received_amount)",
|
||||
"unreceivedWithTax", "FINANCE_AR:未收含税金额(Σ unreceived_amount)",
|
||||
"uninvoicedWithTax", "FINANCE_AR:未开票金额(Σ uninvoiced_amount)",
|
||||
"payableWithTax", "FINANCE_AP:应付含税总额(Σ total_price_with_tax)",
|
||||
"paidWithTax", "FINANCE_AP:已付含税金额(Σ paid_payment_amount)",
|
||||
"unpaidWithTax", "FINANCE_AP:未付含税金额(Σ unpaid_payment_amount)",
|
||||
"unticketedWithTax", "FINANCE_AP:未收票金额(Σ unreceived_ticket_amount)",
|
||||
"rowQty", "该分组参与聚合的原始行数(count(*))"));
|
||||
metadata.put("aggregation_rule", mapOf(
|
||||
"sales_with_tax", "SALES_AMOUNT_WITH_TAX = Σ project_product_info.all_price;与既有 ProjectOrderInfoToolProvider 一致,"
|
||||
+ "all_price 视为含税总价(列注释仅“总价”,含税口径与既有工具保持同源)",
|
||||
"sales_without_tax", "SALES_AMOUNT_WITHOUT_TAX = Σ all_price/(1+tax_rate);tax_rate>1 视为百分数(与既有 normalizePercentTaxRate 口径一致),"
|
||||
+ "tax_rate 为 NULL 按 0 计(等价于未税=含税)",
|
||||
"purchase", "PURCHASE_QTY = Σ item.quantity;PURCHASE_AMOUNT_WITH_TAX = Σ item.amount_total(列注释“含税金额”);"
|
||||
+ "PURCHASE_TAX = Σ item.tax_total(列注释“税额”)",
|
||||
"stock", "IN_STOCK_QTY = count(inventory_status='0');OUT_STOCK_QTY = count(inventory_status='1');"
|
||||
+ "在库口径按方案 15.7 默认 A(inventory_status='0' 即视为在库,★未扣除“已发货未出库”的占用)",
|
||||
"finance", "FINANCE_AR / FINANCE_AP 金额一律直接 sum 账单主表冗余列(total_price_with_tax / received_amount / "
|
||||
+ "unreceived_amount / uninvoiced_amount / paid_payment_amount / unpaid_payment_amount / unreceived_ticket_amount),"
|
||||
+ "★不重算、不换算、与页面展示一致(方案第九章)",
|
||||
"null_as_zero", "所有 sum 均以 ifnull(col,0) 参与,NULL 计 0",
|
||||
"currency", "金额按原币返回,不自动换算",
|
||||
"time_pushdown", "时间条件一律区间下推(>= start and < end);date_format / concat 仅用于分组与输出表达式"));
|
||||
metadata.put("lineage", mapData(
|
||||
"description", "每条响应在 data.lineage 中回传本次命中的链路名、涉及表清单、时间字段与各维度来源字段,避免误读口径(方案 16.2 护栏 6)",
|
||||
"by_link", LINK_TABLES,
|
||||
"join_notes", joinNoteOfAll()));
|
||||
metadata.put("guardrails", mapOf(
|
||||
"time_range_required", "time_range 必填且必须同时给出起止(不接受开放区间、不默认近 12 个月),跨度上限 36 个月;"
|
||||
+ "原因:链路时间列均无索引,无界扫描会拖垮库",
|
||||
"single_link", "metrics 必须属于同一条链路,跨链路混用直接 INVALID_PARAMS 并回显各自的链路归属",
|
||||
"whitelist_only", "dimensions / metrics 均走白名单,且维度必须被该链路支持;超出白名单一律报错,不静默忽略",
|
||||
"group_limit", "group_limit 默认 " + DEFAULT_GROUP_LIMIT + "、上限 " + MAX_GROUP_LIMIT + ";超出报错并提示缩小 time_range 或减少维度",
|
||||
"top_n", "top_n 默认 " + DEFAULT_TOP_N + "、上限 " + MAX_TOP_N + ",按第一个 metric 降序",
|
||||
"no_arbitrary_sql", "★不开放任意 SQL:维度表达式与排序度量在 XML 内以 <choose> 映射为固定表达式,模型无法注入任何 SQL 片段;"
|
||||
+ "表清单固定为 " + LINK_TABLES,
|
||||
"table_whitelist", "仅访问 McpTables.ALLOWED 内的表;严禁备份/历史表(如 project_product_info_bak、oms_purchase_order_1211、"
|
||||
+ "oms_payable_bill_copy1、oms_inventory_info_copy1)",
|
||||
"explicit_time_field", "STOCK 链路时间字段显式取 oms_inventory_info.create_time(方案 15.1 默认口径),已在 lineage.time_field 声明;"
|
||||
+ "如需按出库时间统计请使用库存/出库明细工具",
|
||||
"unsupported_dimension", "FINANCE_AR / FINANCE_AP 不支持 PROJECT 维度:账单主表无 project 列,"
|
||||
+ "经 order_code 关联 project_order_info 存在一对多(多版本)导致金额放大的风险,故不提供(宁缺不错)",
|
||||
"prefer_domain_tools", "本工具为兜底能力(SALES 链路为 3 表 join + 分组,是最重的查询),能用域内专用工具表达的请优先使用"
|
||||
+ "(project_list / finance_list / warehouse_list / 各 *_aggregate)"));
|
||||
metadata.put("permission_source", "按链路最小权限校验:" + permissionsOf("SALES")[0] + " / "
|
||||
+ permissionsOf("SALES")[1] + " / " + permissionsOf("SALES")[2] + " / " + permissionsOf("SALES")[3] + "(SALES,来源 ProjectOrderInfoController 与 ProjectInfoController);"
|
||||
+ permissionsOf("PURCHASE")[0] + "(PURCHASE,来源 OmsPurchaseOrderController#list);"
|
||||
+ permissionsOf("STOCK")[0] + "(STOCK,来源 InventoryInfoController#view,其 list 注解被注释);"
|
||||
+ permissionsOf("FINANCE_AR")[0] + "(FINANCE_AR,来源 OmsReceivableBillController#list);"
|
||||
+ permissionsOf("FINANCE_AP")[0] + "(FINANCE_AP,来源 OmsPayableBillController#list)");
|
||||
metadata.put("notes", "时间桶 MONTH → yyyy-MM、QUARTER → yyyyQn;编码类字段入参/出参统一 trim(库中存在前导制表符脏数据,见方案 15.14);"
|
||||
+ "编码匹配不区分大小写(utf8mb4_unicode_ci);include_zero=false 时按本次请求的 metrics 集合过滤全零行;"
|
||||
+ "truncated=true 说明结果被 group_limit 或 top_n 截断,请收窄 time_range 或减少维度后重试。");
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> metricWhitelistByLink() {
|
||||
Map<String, List<String>> grouped = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String> entry : METRIC_LINK.entrySet()) {
|
||||
List<String> list = grouped.get(entry.getValue());
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
grouped.put(entry.getValue(), list);
|
||||
}
|
||||
list.add(entry.getKey());
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : grouped.entrySet()) {
|
||||
result.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String describeMetricWhitelist() {
|
||||
return metricWhitelistByLink().toString();
|
||||
}
|
||||
|
||||
private String describeLinkDimensions() {
|
||||
return LINK_DIMENSIONS.toString();
|
||||
}
|
||||
|
||||
private Map<String, Object> joinNoteOfAll() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (String link : LINK_DIMENSIONS.keySet()) {
|
||||
result.put(link, joinNoteOf(link));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private BigDecimal decimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (value instanceof BigDecimal) {
|
||||
return (BigDecimal) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return new BigDecimal(value.toString());
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private Long longVal(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,617 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpCursor;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpPages;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpTimeRange;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.OmsPayableBillMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableBillMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
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.TreeSet;
|
||||
|
||||
/**
|
||||
* MCP 工具 finance_balance_aggregate(财务余额聚合:SUMMARY 默认 / LIST 分页)。
|
||||
*
|
||||
* <p>度量固定 8 个:应收侧 receivableWithTax / receivedWithTax / unreceivedWithTax / uninvoicedWithTax;
|
||||
* 应付侧 payableWithTax / paidWithTax / unpaidWithTax / unticketedWithTax。
|
||||
* 未传 as_of_date 时直接取账单主表冗余列求和(不重算,basis=CURRENT);
|
||||
* 传入 as_of_date 时按明细重算(basis=RECALCULATED@<date>,两种口径不可混用)。</p>
|
||||
*
|
||||
* <p>方案依据:docx/mcp-data-tools-plan.md 第 4.2 / 5 / 7 / 8 / 9 / 13 / 15 章与附录 A.6(含 v13 的 OVERDUE_BUCKET 与 as_of_date)。</p>
|
||||
* <p>权限来源(从 Controller 的 {@code @RequiresPermissions} 抄取):
|
||||
* {@code OmsReceivableBillController#list} = {@code finance:receivable:list};
|
||||
* {@code OmsPayableBillController#list} = {@code finance:payable:list};两者"任意一个满足"即放行。</p>
|
||||
*/
|
||||
@Component
|
||||
public class FinanceBalanceAggregateToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final Set<String> MODES = new LinkedHashSet<>(Arrays.asList("SUMMARY", "LIST"));
|
||||
private static final Set<String> GROUP_BYS = new LinkedHashSet<>(
|
||||
Arrays.asList("NONE", "ORDER", "PARTNER", "TIME_MONTH", "OVERDUE_BUCKET"));
|
||||
private static final int DEFAULT_TIME_MONTHS = 12;
|
||||
private static final int MAX_TIME_MONTHS = 36;
|
||||
private static final int DEFAULT_TOP_N = 10;
|
||||
private static final int MAX_TOP_N = 100;
|
||||
private static final long COUNT_CAP = 50000L;
|
||||
/** 账龄分桶行数固定(4 桶 + NO_PLAN),取一个安全上限 */
|
||||
private static final int OVERDUE_BUCKET_LIMIT = 10;
|
||||
|
||||
@Autowired
|
||||
private OmsReceivableBillMapper omsReceivableBillMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsPayableBillMapper omsPayableBillMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "finance_balance_aggregate";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "财务余额聚合(只读)。mode=SUMMARY(默认)按 group_by 一次算完,mode=LIST 按 order_code 游标分页。"
|
||||
+ "固定度量:receivableWithTax 应收含税、receivedWithTax 已收、unreceivedWithTax 未收、uninvoicedWithTax 未开票、"
|
||||
+ "payableWithTax 应付含税、paidWithTax 已付、unpaidWithTax 未付、unticketedWithTax 未收票。"
|
||||
+ "group_by=OVERDUE_BUCKET 返回账龄分桶(0-30/31-60/61-90/90+,仅未收款行);as_of_date 为历史时点重算口径。"
|
||||
+ "分页查询时:若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 has_more 为 false,否则数据不完整。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("mode", enumProperty("模式:SUMMARY 汇总(默认,不分页)/ LIST 明细(按 order_code 游标分页)", "SUMMARY", "LIST"));
|
||||
properties.put("group_by", enumProperty("SUMMARY 分组维度:NONE 总计(默认)/ ORDER 合同编号 / PARTNER 合作伙伴 / "
|
||||
+ "TIME_MONTH 月份 / OVERDUE_BUCKET 账龄分桶(仅 SUMMARY)",
|
||||
"NONE", "ORDER", "PARTNER", "TIME_MONTH", "OVERDUE_BUCKET"));
|
||||
properties.put("order_code_list", arrayProperty("合同编号列表,≤" + McpPages.MAX_CODE_LIST + ";mode=LIST 时与 partner_code_list / vendor_code_list 至少提供一个"));
|
||||
properties.put("partner_code_list", arrayProperty("客户编码列表(应收侧 partner_code),≤" + McpPages.MAX_CODE_LIST));
|
||||
properties.put("vendor_code_list", arrayProperty("制造商编码列表(应付侧 vendor_code),≤" + McpPages.MAX_CODE_LIST));
|
||||
properties.put("time_range", stringProperty("时间区间,格式 \"开始~结束\"(yyyy-MM-dd);应收侧基于 oms_receivable_bill.create_time、"
|
||||
+ "应付侧基于 oms_payable_bill.create_time;未传时默认近 " + DEFAULT_TIME_MONTHS + " 个月,跨度上限 " + MAX_TIME_MONTHS + " 个月"));
|
||||
properties.put("time_range_start", stringProperty("时间区间开始(可选,与 time_range 二选一)"));
|
||||
properties.put("time_range_end", stringProperty("时间区间结束(可选,与 time_range 二选一)"));
|
||||
properties.put("as_of_date", stringProperty("历史时点日期(yyyy-MM-dd,可选):按明细重算该时点余额,"
|
||||
+ "仅支持 mode=SUMMARY + group_by=NONE"));
|
||||
properties.put("top_n", intProperty("SUMMARY 返回前 N 组(默认 " + DEFAULT_TOP_N + ",上限 " + MAX_TOP_N + ");仅 SUMMARY 可用;"
|
||||
+ "group_by=TIME_MONTH 时忽略(返回区间内全部月份)"));
|
||||
properties.put("include_total", boolProperty("是否统计总组数(默认 false);LIST 时受 count_cap=" + COUNT_CAP + " 限制"));
|
||||
return withPagingProperties(properties, McpPages.MAX_AGG_PAGE_SIZE, McpPages.DEFAULT_PAGE_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
// 权限串来源:OmsReceivableBillController#list / OmsPayableBillController#list
|
||||
McpAuth.requireAnyPermission("finance:receivable:list", "finance:payable:list");
|
||||
|
||||
String mode = McpParams.enumVal(params, "mode", MODES, "SUMMARY");
|
||||
String groupBy = McpParams.enumVal(params, "group_by", GROUP_BYS,
|
||||
"LIST".equals(mode) ? "ORDER" : "NONE");
|
||||
List<String> orderCodeList = McpParams.codeList(params, "order_code_list", McpPages.MAX_CODE_LIST);
|
||||
List<String> partnerCodeList = McpParams.codeList(params, "partner_code_list", McpPages.MAX_CODE_LIST);
|
||||
List<String> vendorCodeList = McpParams.codeList(params, "vendor_code_list", McpPages.MAX_CODE_LIST);
|
||||
McpTimeRange timeRange = McpParams.timeRange(params, "time_range", DEFAULT_TIME_MONTHS, MAX_TIME_MONTHS);
|
||||
Date asOfDate = McpParams.date(params, "as_of_date");
|
||||
Integer topN = McpParams.intVal(params, "top_n", DEFAULT_TOP_N);
|
||||
if (topN < 1 || topN > MAX_TOP_N) {
|
||||
throw McpToolException.invalidParams("top_n 取值非法:" + topN + ",允许范围 1~" + MAX_TOP_N);
|
||||
}
|
||||
boolean includeTotal = McpParams.boolVal(params, "include_total", false);
|
||||
String basis = asOfDate == null ? "CURRENT" : "RECALCULATED@" + McpFormat.date(asOfDate);
|
||||
|
||||
if ("LIST".equals(mode)) {
|
||||
McpParams.reject(McpParams.has(params, "top_n"), "mode=LIST 不支持 top_n(top_n 仅 SUMMARY 可用)");
|
||||
if (!"ORDER".equals(groupBy)) {
|
||||
throw McpToolException.invalidParams("mode=LIST 仅支持 group_by=ORDER(排序键 order_code);"
|
||||
+ "如需 PARTNER / TIME_MONTH / OVERDUE_BUCKET 维度请使用 mode=SUMMARY");
|
||||
}
|
||||
if (asOfDate != null) {
|
||||
throw McpToolException.invalidParams("as_of_date 为按明细重算的单行总计口径,仅支持 mode=SUMMARY + group_by=NONE");
|
||||
}
|
||||
if (orderCodeList.isEmpty() && partnerCodeList.isEmpty() && vendorCodeList.isEmpty()) {
|
||||
throw McpToolException.invalidParams("mode=LIST 需要提供 order_code_list 或 partner_code_list / vendor_code_list 以收窄范围,"
|
||||
+ "避免全表扫描;如需全局统计请使用 mode=SUMMARY");
|
||||
}
|
||||
} else {
|
||||
McpParams.reject(McpParams.has(params, "cursor"), "mode=SUMMARY 不支持 cursor(SUMMARY 不分页)");
|
||||
McpParams.reject(McpParams.has(params, "page_size"), "mode=SUMMARY 不支持 page_size,请改用 top_n");
|
||||
if (asOfDate != null && !"NONE".equals(groupBy)) {
|
||||
throw McpToolException.invalidParams("as_of_date 为按明细重算的单行总计口径,仅支持 group_by=NONE");
|
||||
}
|
||||
}
|
||||
if ("OVERDUE_BUCKET".equals(groupBy)) {
|
||||
if ("LIST".equals(mode)) {
|
||||
throw McpToolException.invalidParams("group_by=OVERDUE_BUCKET 仅支持 mode=SUMMARY(分桶行数固定,无需翻页)");
|
||||
}
|
||||
if (asOfDate != null) {
|
||||
throw McpToolException.invalidParams("group_by=OVERDUE_BUCKET 不支持 as_of_date(账龄以当前日期与计划收款日期计算)");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("mode", mode);
|
||||
query.put("group_by", groupBy);
|
||||
query.put("order_code_list", orderCodeList);
|
||||
query.put("partner_code_list", partnerCodeList);
|
||||
query.put("vendor_code_list", vendorCodeList);
|
||||
query.put("time_range", McpParams.describe(timeRange));
|
||||
query.put("as_of_date", McpFormat.date(asOfDate));
|
||||
query.put("basis", basis);
|
||||
query.put("top_n", "SUMMARY".equals(mode) ? topN : null);
|
||||
query.put("include_total", includeTotal);
|
||||
|
||||
if ("SUMMARY".equals(mode)) {
|
||||
return handleSummary(groupBy, orderCodeList, partnerCodeList, vendorCodeList, timeRange,
|
||||
asOfDate, basis, topN, includeTotal, query);
|
||||
}
|
||||
return handleList(params, orderCodeList, partnerCodeList, vendorCodeList, timeRange, basis,
|
||||
includeTotal, query);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ SUMMARY
|
||||
|
||||
private Object handleSummary(String groupBy,
|
||||
List<String> orderCodeList,
|
||||
List<String> partnerCodeList,
|
||||
List<String> vendorCodeList,
|
||||
McpTimeRange timeRange,
|
||||
Date asOfDate,
|
||||
String basis,
|
||||
int topN,
|
||||
boolean includeTotal,
|
||||
Map<String, Object> query) {
|
||||
if (asOfDate != null) {
|
||||
return handleRecalculated(asOfDate, basis, includeTotal, query);
|
||||
}
|
||||
if ("OVERDUE_BUCKET".equals(groupBy)) {
|
||||
return handleOverdueBucket(orderCodeList, partnerCodeList, timeRange, basis, includeTotal, query);
|
||||
}
|
||||
|
||||
// group_by=ORDER/PARTNER 取 top_n;TIME_MONTH 取全部月份;NONE 单行
|
||||
int limit = "NONE".equals(groupBy) ? 1
|
||||
: ("TIME_MONTH".equals(groupBy) ? MAX_TOP_N : topN);
|
||||
List<Map<String, Object>> receivableRows = safe(omsReceivableBillMapper.selectReceivableBalanceSummary(
|
||||
groupBy, orderCodeList, partnerCodeList, timeRange.getStart(), timeRange.getEnd(), 0, null, limit));
|
||||
List<Map<String, Object>> payableRows = safe(omsPayableBillMapper.selectPayableBalanceSummary(
|
||||
groupBy, orderCodeList, vendorCodeList, timeRange.getStart(), timeRange.getEnd(), 0, null, limit));
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
if ("NONE".equals(groupBy)) {
|
||||
Map<String, Object> item = metricsItem("groupKey", "TOTAL");
|
||||
if (!receivableRows.isEmpty()) {
|
||||
applyReceivable(item, receivableRows.get(0));
|
||||
}
|
||||
if (!payableRows.isEmpty()) {
|
||||
applyPayable(item, payableRows.get(0));
|
||||
}
|
||||
items.add(item);
|
||||
} else if ("TIME_MONTH".equals(groupBy)) {
|
||||
Map<String, Map<String, Object>> merged = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : receivableRows) {
|
||||
mergeRow(merged, "timeBucket", row, true);
|
||||
}
|
||||
for (Map<String, Object> row : payableRows) {
|
||||
mergeRow(merged, "timeBucket", row, false);
|
||||
}
|
||||
items.addAll(merged.values());
|
||||
items.sort(Comparator.comparing(item -> String.valueOf(item.get("timeBucket"))));
|
||||
} else if ("PARTNER".equals(groupBy)) {
|
||||
// 应收侧主体为 partner_code(客户),应付侧主体为 vendor_code(制造商),主体不同不可合并
|
||||
for (Map<String, Object> row : receivableRows) {
|
||||
Map<String, Object> item = metricsItem("partnerCode", str(row.get("groupKey")));
|
||||
item.put("subject", "RECEIVABLE");
|
||||
item.put("partnerName", str(row.get("groupName")));
|
||||
applyReceivable(item, row);
|
||||
clearPayable(item);
|
||||
items.add(item);
|
||||
}
|
||||
for (Map<String, Object> row : payableRows) {
|
||||
Map<String, Object> item = metricsItem("vendorCode", str(row.get("groupKey")));
|
||||
item.put("subject", "PAYABLE");
|
||||
item.put("vendorName", str(row.get("groupName")));
|
||||
applyPayable(item, row);
|
||||
clearReceivable(item);
|
||||
items.add(item);
|
||||
}
|
||||
} else {
|
||||
Map<String, Map<String, Object>> merged = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : receivableRows) {
|
||||
mergeRow(merged, "orderCode", row, true);
|
||||
}
|
||||
for (Map<String, Object> row : payableRows) {
|
||||
mergeRow(merged, "orderCode", row, false);
|
||||
}
|
||||
items.addAll(merged.values());
|
||||
items.sort((left, right) -> combinedTotal(right).compareTo(combinedTotal(left)));
|
||||
if (items.size() > topN) {
|
||||
items = new ArrayList<>(items.subList(0, topN));
|
||||
}
|
||||
}
|
||||
|
||||
for (Map<String, Object> item : items) {
|
||||
item.put("basis", basis);
|
||||
}
|
||||
|
||||
Long total = includeTotal ? (long) items.size() : null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), limit, false, null,
|
||||
sortByFor("SUMMARY", groupBy), 1, total, false, false);
|
||||
Map<String, Object> data = listData(items, pageInfo, total);
|
||||
data.put("basis", basis);
|
||||
data.put("as_of_date", null);
|
||||
return response(buildMetadata(), query, data);
|
||||
}
|
||||
|
||||
private Object handleOverdueBucket(List<String> orderCodeList,
|
||||
List<String> partnerCodeList,
|
||||
McpTimeRange timeRange,
|
||||
String basis,
|
||||
boolean includeTotal,
|
||||
Map<String, Object> query) {
|
||||
List<Map<String, Object>> rows = safe(omsReceivableBillMapper.selectReceivableBalanceSummary(
|
||||
"OVERDUE_BUCKET", orderCodeList, partnerCodeList, timeRange.getStart(), timeRange.getEnd(),
|
||||
0, null, OVERDUE_BUCKET_LIMIT));
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("overdueBucket", str(row.get("groupKey")));
|
||||
item.put("inBucketQty", longVal(row.get("itemQty")));
|
||||
item.put("inBucketAmount", dec(row.get("unreceivedWithTax")));
|
||||
item.put("receivableWithTax", dec(row.get("receivableWithTax")));
|
||||
item.put("receivedWithTax", dec(row.get("receivedWithTax")));
|
||||
item.put("unreceivedWithTax", dec(row.get("unreceivedWithTax")));
|
||||
item.put("uninvoicedWithTax", dec(row.get("uninvoicedWithTax")));
|
||||
// 账龄分桶只针对应收未收款,应付侧度量不适用
|
||||
item.put("payableWithTax", null);
|
||||
item.put("paidWithTax", null);
|
||||
item.put("unpaidWithTax", null);
|
||||
item.put("unticketedWithTax", null);
|
||||
item.put("basis", basis);
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
Long total = includeTotal ? (long) items.size() : null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), OVERDUE_BUCKET_LIMIT, false, null,
|
||||
sortByFor("SUMMARY", "OVERDUE_BUCKET"), 1, total, false, false);
|
||||
Map<String, Object> data = listData(items, pageInfo, total);
|
||||
data.put("basis", basis);
|
||||
data.put("as_of_date", null);
|
||||
return response(buildMetadata(), query, data);
|
||||
}
|
||||
|
||||
private Object handleRecalculated(Date asOfDate,
|
||||
String basis,
|
||||
boolean includeTotal,
|
||||
Map<String, Object> query) {
|
||||
Map<String, Object> receivableAsOf = omsReceivableBillMapper.selectReceivableAsOf(asOfDate);
|
||||
Map<String, Object> payableAsOf = omsPayableBillMapper.selectPayableAsOf(asOfDate);
|
||||
|
||||
BigDecimal receivableTotal = dec(receivableAsOf == null ? null : receivableAsOf.get("totalWithTax"));
|
||||
BigDecimal receivableSettled = dec(receivableAsOf == null ? null : receivableAsOf.get("settledAmount"));
|
||||
BigDecimal payableTotal = dec(payableAsOf == null ? null : payableAsOf.get("totalWithTax"));
|
||||
BigDecimal payableSettled = dec(payableAsOf == null ? null : payableAsOf.get("settledAmount"));
|
||||
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("asOfDate", McpFormat.date(asOfDate));
|
||||
item.put("basis", basis);
|
||||
item.put("receivableWithTax", receivableTotal);
|
||||
item.put("receivedWithTax", receivableSettled);
|
||||
item.put("unreceivedWithTax", receivableTotal.subtract(receivableSettled));
|
||||
// 重算口径只有"应收/已收"两个来源表,开票与收票无对应明细,故为 null
|
||||
item.put("uninvoicedWithTax", null);
|
||||
item.put("payableWithTax", payableTotal);
|
||||
item.put("paidWithTax", payableSettled);
|
||||
item.put("unpaidWithTax", payableTotal.subtract(payableSettled));
|
||||
item.put("unticketedWithTax", null);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
items.add(item);
|
||||
|
||||
Long total = includeTotal ? 1L : null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), 1, false, null,
|
||||
sortByFor("SUMMARY", "NONE"), 1, total, false, false);
|
||||
Map<String, Object> data = listData(items, pageInfo, total);
|
||||
data.put("basis", basis);
|
||||
data.put("as_of_date", McpFormat.date(asOfDate));
|
||||
return response(buildMetadata(), query, data);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ LIST
|
||||
|
||||
private Object handleList(Map<String, Object> params,
|
||||
List<String> orderCodeList,
|
||||
List<String> partnerCodeList,
|
||||
List<String> vendorCodeList,
|
||||
McpTimeRange timeRange,
|
||||
String basis,
|
||||
boolean includeTotal,
|
||||
Map<String, Object> query) {
|
||||
int pageSize = McpPages.pageSize(params, McpPages.DEFAULT_PAGE_SIZE, McpPages.MAX_AGG_PAGE_SIZE);
|
||||
String filterHash = McpParams.filterHash(getToolName(), "LIST", "ORDER", sortedJoin(orderCodeList),
|
||||
sortedJoin(partnerCodeList), sortedJoin(vendorCodeList), McpParams.describe(timeRange), basis);
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), filterHash, McpPages.DEFAULT_AGG_MAX_PAGES);
|
||||
int pageNo = cursor == null ? 1 : cursor.getP();
|
||||
String lastGroupKey = cursor == null ? null : cursor.key(0);
|
||||
query.put("page_size", pageSize);
|
||||
query.put("page_no", pageNo);
|
||||
|
||||
int fetch = pageSize + 1;
|
||||
// 两侧各自取 keyset 的前 fetch 条:并集的最小 fetch 条必然落在两侧各自的最小 fetch 条之内
|
||||
List<Map<String, Object>> receivableRows = safe(omsReceivableBillMapper.selectReceivableBalanceSummary(
|
||||
"ORDER", orderCodeList, partnerCodeList, timeRange.getStart(), timeRange.getEnd(), 1, lastGroupKey, fetch));
|
||||
List<Map<String, Object>> payableRows = safe(omsPayableBillMapper.selectPayableBalanceSummary(
|
||||
"ORDER", orderCodeList, vendorCodeList, timeRange.getStart(), timeRange.getEnd(), 1, lastGroupKey, fetch));
|
||||
|
||||
Map<String, Map<String, Object>> merged = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : receivableRows) {
|
||||
mergeRow(merged, "orderCode", row, true);
|
||||
}
|
||||
for (Map<String, Object> row : payableRows) {
|
||||
mergeRow(merged, "orderCode", row, false);
|
||||
}
|
||||
List<Map<String, Object>> all = new ArrayList<>(merged.values());
|
||||
// 与库端 utf8mb4_unicode_ci(大小写不敏感)的 order_code 排序保持一致,避免游标错位
|
||||
all.sort(Comparator.comparing(item -> String.valueOf(item.get("orderCode")), String.CASE_INSENSITIVE_ORDER));
|
||||
for (Map<String, Object> item : all) {
|
||||
item.put("basis", basis);
|
||||
}
|
||||
|
||||
boolean hasMore = all.size() > pageSize;
|
||||
List<Map<String, Object>> page = McpPages.trim(all, pageSize);
|
||||
String nextCursor = null;
|
||||
if (hasMore && !page.isEmpty()) {
|
||||
nextCursor = McpCursor.encode(getToolName(), filterHash, pageNo + 1,
|
||||
keysOf(page.get(page.size() - 1).get("orderCode")));
|
||||
}
|
||||
|
||||
Long total = null;
|
||||
boolean capped = false;
|
||||
if (includeTotal) {
|
||||
int countLimit = (int) Math.min(Integer.MAX_VALUE, COUNT_CAP + 1);
|
||||
List<Map<String, Object>> allReceivable = safe(omsReceivableBillMapper.selectReceivableBalanceSummary(
|
||||
"ORDER", orderCodeList, partnerCodeList, timeRange.getStart(), timeRange.getEnd(), 1, null, countLimit));
|
||||
List<Map<String, Object>> allPayable = safe(omsPayableBillMapper.selectPayableBalanceSummary(
|
||||
"ORDER", orderCodeList, vendorCodeList, timeRange.getStart(), timeRange.getEnd(), 1, null, countLimit));
|
||||
LinkedHashSet<String> keys = new LinkedHashSet<>();
|
||||
for (Map<String, Object> row : allReceivable) {
|
||||
keys.add(str(row.get("groupKey")));
|
||||
}
|
||||
for (Map<String, Object> row : allPayable) {
|
||||
keys.add(str(row.get("groupKey")));
|
||||
}
|
||||
capped = keys.size() > COUNT_CAP;
|
||||
total = capped ? COUNT_CAP : (long) keys.size();
|
||||
}
|
||||
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(page.size(), pageSize, hasMore, nextCursor,
|
||||
"order_code", pageNo, total, capped, false);
|
||||
Map<String, Object> data = listData(page, pageInfo, total);
|
||||
data.put("basis", basis);
|
||||
data.put("as_of_date", null);
|
||||
return response(buildMetadata(), query, data);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 行合并
|
||||
|
||||
private Map<String, Object> metricsItem(String keyName, Object keyValue) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
if (keyName != null) {
|
||||
item.put(keyName, keyValue);
|
||||
}
|
||||
item.put("receivableWithTax", BigDecimal.ZERO);
|
||||
item.put("receivedWithTax", BigDecimal.ZERO);
|
||||
item.put("unreceivedWithTax", BigDecimal.ZERO);
|
||||
item.put("uninvoicedWithTax", BigDecimal.ZERO);
|
||||
item.put("payableWithTax", BigDecimal.ZERO);
|
||||
item.put("paidWithTax", BigDecimal.ZERO);
|
||||
item.put("unpaidWithTax", BigDecimal.ZERO);
|
||||
item.put("unticketedWithTax", BigDecimal.ZERO);
|
||||
item.put("receivableBillQty", 0L);
|
||||
item.put("payableBillQty", 0L);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void mergeRow(Map<String, Map<String, Object>> merged, String keyName, Map<String, Object> row, boolean receivable) {
|
||||
String key = str(row.get("groupKey"));
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> item = merged.get(key);
|
||||
if (item == null) {
|
||||
item = metricsItem(keyName, key);
|
||||
merged.put(key, item);
|
||||
}
|
||||
if (receivable) {
|
||||
item.put("receivableName", str(row.get("groupName")));
|
||||
applyReceivable(item, row);
|
||||
} else {
|
||||
item.put("payableName", str(row.get("groupName")));
|
||||
applyPayable(item, row);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyReceivable(Map<String, Object> item, Map<String, Object> row) {
|
||||
item.put("receivableWithTax", dec(row.get("receivableWithTax")));
|
||||
item.put("receivedWithTax", dec(row.get("receivedWithTax")));
|
||||
item.put("unreceivedWithTax", dec(row.get("unreceivedWithTax")));
|
||||
item.put("uninvoicedWithTax", dec(row.get("uninvoicedWithTax")));
|
||||
item.put("receivableBillQty", longVal(row.get("itemQty")));
|
||||
}
|
||||
|
||||
private void applyPayable(Map<String, Object> item, Map<String, Object> row) {
|
||||
item.put("payableWithTax", dec(row.get("payableWithTax")));
|
||||
item.put("paidWithTax", dec(row.get("paidWithTax")));
|
||||
item.put("unpaidWithTax", dec(row.get("unpaidWithTax")));
|
||||
item.put("unticketedWithTax", dec(row.get("unticketedWithTax")));
|
||||
item.put("payableBillQty", longVal(row.get("itemQty")));
|
||||
}
|
||||
|
||||
private void clearPayable(Map<String, Object> item) {
|
||||
item.put("payableWithTax", null);
|
||||
item.put("paidWithTax", null);
|
||||
item.put("unpaidWithTax", null);
|
||||
item.put("unticketedWithTax", null);
|
||||
item.put("payableBillQty", null);
|
||||
}
|
||||
|
||||
private void clearReceivable(Map<String, Object> item) {
|
||||
item.put("receivableWithTax", null);
|
||||
item.put("receivedWithTax", null);
|
||||
item.put("unreceivedWithTax", null);
|
||||
item.put("uninvoicedWithTax", null);
|
||||
item.put("receivableBillQty", null);
|
||||
}
|
||||
|
||||
private BigDecimal combinedTotal(Map<String, Object> item) {
|
||||
return dec(item.get("receivableWithTax")).add(dec(item.get("payableWithTax")));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private List<Map<String, Object>> safe(List<Map<String, Object>> rows) {
|
||||
return rows == null ? Collections.emptyList() : rows;
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value == null ? null : McpFormat.trimToNull(String.valueOf(value));
|
||||
}
|
||||
|
||||
private BigDecimal dec(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (value instanceof BigDecimal) {
|
||||
return (BigDecimal) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return new BigDecimal(value.toString());
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private Long longVal(Object value) {
|
||||
if (value == null) {
|
||||
return 0L;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private String sortedJoin(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
List<String> sorted = new ArrayList<>(new TreeSet<>(values));
|
||||
return String.join(",", sorted);
|
||||
}
|
||||
|
||||
private String sortByFor(String mode, String groupBy) {
|
||||
if ("LIST".equals(mode)) {
|
||||
return "order_code";
|
||||
}
|
||||
switch (groupBy) {
|
||||
case "ORDER":
|
||||
return "order_code(按两侧含税总额合计 desc)";
|
||||
case "PARTNER":
|
||||
return "partner_code(应收)/ vendor_code(应付),各自按含税总额 desc";
|
||||
case "TIME_MONTH":
|
||||
return "time_bucket asc(yyyy-MM)";
|
||||
case "OVERDUE_BUCKET":
|
||||
return "overdueBucket(0-30 / 31-60 / 61-90 / 90+ / NO_PLAN)";
|
||||
default:
|
||||
return "groupKey(TOTAL 单行)";
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata() {
|
||||
Map<String, Object> metadata = metadata("财务余额聚合:应收侧取 oms_receivable_bill、应付侧取 oms_payable_bill;"
|
||||
+ "SUMMARY 一次算完(可选 top_n),LIST 按 order_code 游标分页。");
|
||||
metadata.put("query_fields", mapOf(
|
||||
"mode", "SUMMARY 汇总(默认)/ LIST 明细(按 order_code 分页)",
|
||||
"group_by", "NONE/ORDER/PARTNER/TIME_MONTH/OVERDUE_BUCKET(OVERDUE_BUCKET 仅 SUMMARY)",
|
||||
"order_code_list", "合同编号列表(≤200)",
|
||||
"partner_code_list", "客户编码列表(应收侧)",
|
||||
"vendor_code_list", "制造商编码列表(应付侧)",
|
||||
"time_range", "时间区间 start~end;应收侧基于 oms_receivable_bill.create_time,应付侧基于 oms_payable_bill.create_time;默认近12个月、上限36个月",
|
||||
"as_of_date", "历史时点日期(仅 SUMMARY + group_by=NONE),按明细重算",
|
||||
"top_n", "SUMMARY 前 N 组(默认10,上限100;TIME_MONTH 忽略)",
|
||||
"include_total", "是否统计总组数(默认false,上限50000)",
|
||||
"page_size", "LIST 每页条数(默认20,上限200)",
|
||||
"cursor", "LIST 游标:回传上次的 page_info.next_cursor"));
|
||||
metadata.put("data_fields", mapOf(
|
||||
"total", "组数(include_total=true 时为真实值)",
|
||||
"items", "分组结果(NONE 为单行总计)",
|
||||
"page_info", "分页信息:returned/page_size/has_more/next_cursor/sort_by/page_no/total_count",
|
||||
"basis", "口径标记:CURRENT(冗余列当前值)或 RECALCULATED@<日期>(按明细重算)",
|
||||
"as_of_date", "重算口径下的时点日期,否则为 null"));
|
||||
metadata.put("item_fields", mapOf(
|
||||
"groupKey/orderCode", "分组键(ORDER 维度为合同编号)",
|
||||
"timeBucket", "月份桶 yyyy-MM(group_by=TIME_MONTH)",
|
||||
"partnerCode/partnerName", "客户编码/名称(PARTNER 维度的应收侧,subject=RECEIVABLE)",
|
||||
"vendorCode/vendorName", "制造商编码/名称(PARTNER 维度的应付侧,subject=PAYABLE)",
|
||||
"subject", "PARTNER 维度行主体:RECEIVABLE(应收/客户)或 PAYABLE(应付/制造商)",
|
||||
"overdueBucket", "账龄分桶:0-30 / 31-60 / 61-90 / 90+ / NO_PLAN(未设置收款计划)",
|
||||
"inBucketQty", "该桶内单据数(仅 unreceived_amount > 0 的行)",
|
||||
"inBucketAmount", "该桶内未收款金额合计",
|
||||
"receivableWithTax", "应收含税总额(Σ oms_receivable_bill.total_price_with_tax)",
|
||||
"receivedWithTax", "已收含税金额(Σ received_amount)",
|
||||
"unreceivedWithTax", "未收含税金额(Σ unreceived_amount)",
|
||||
"uninvoicedWithTax", "未开票金额(Σ uninvoiced_amount)",
|
||||
"payableWithTax", "应付含税总额(Σ oms_payable_bill.total_price_with_tax)",
|
||||
"paidWithTax", "已付含税金额(Σ paid_payment_amount)",
|
||||
"unpaidWithTax", "未付含税金额(Σ unpaid_payment_amount)",
|
||||
"unticketedWithTax", "未收票金额(Σ unreceived_ticket_amount)",
|
||||
"receivableBillQty/payableBillQty", "参与聚合的应收/应付单数",
|
||||
"asOfDate/basis", "时点日期 / 口径标记(CURRENT 或 RECALCULATED@<日期>)"));
|
||||
metadata.put("aggregation_rule", mapOf(
|
||||
"basis_current", "basis=CURRENT:所有金额直接 sum 账单主表冗余列(unreceived_amount / uninvoiced_amount / "
|
||||
+ "unpaid_payment_amount / unreceived_ticket_amount 等),★不重算,与页面展示一致",
|
||||
"basis_recalculated", "basis=RECALCULATED@<date>:应收(≤T)=Σ oms_receivable_bill.total_price_with_tax where create_time ≤ T;"
|
||||
+ "已收(≤T)=Σ oms_receivable_receipt_detail.receipt_amount where receipt_time ≤ T;时点未收=两者差;"
|
||||
+ "应付侧同理(oms_payable_bill.create_time 与 oms_payable_payment_detail.payment_time)",
|
||||
"basis_mixing", "★两种口径不可混用:CURRENT 为『当前值』,RECALCULATED 为『历史时点按明细重算值』,数值天然不同;"
|
||||
+ "重算口径下 uninvoicedWithTax / unticketedWithTax 无对应明细来源,返回 null",
|
||||
"overdue_bucket", "账龄分桶=datediff(今天, plan_receipt_date)(plan_receipt_date 取自 last_receipt_plan_id 关联的收款计划):"
|
||||
+ "0-30(≤30,含未到期) / 31-60 / 61-90 / 90+(>90),边界左闭右闭;仅统计 unreceived_amount > 0 的行;"
|
||||
+ "plan_receipt_date 为空的应收单无法计算账龄,单独归入 NO_PLAN",
|
||||
"time_field", "时间维度字段:应收=oms_receivable_bill.create_time,应付=oms_payable_bill.create_time(记账口径);"
|
||||
+ "时间条件一律区间下推(>= start and < end),不使用 date_format 作为过滤条件",
|
||||
"partner_dimension", "group_by=PARTNER 的维度主体在两侧不同:应收按 partner_code(客户)、应付按 vendor_code(制造商),"
|
||||
+ "故不合并为同一行,以 subject 区分",
|
||||
"order_top_n", "group_by=ORDER 时两侧各自按含税总额降序取 top_n 后合并,再按两侧合计降序截断 top_n;"
|
||||
+ "需要完整订单清单请改用 mode=LIST",
|
||||
"null_as_zero", "冗余列金额为 NULL 时按 0 参与求和",
|
||||
"currency", "金额按原币返回,不自动换算"));
|
||||
metadata.put("notes", "mode=LIST 必须给出 order_code_list 或 partner_code_list / vendor_code_list;"
|
||||
+ "mode=LIST 的 order_code 并集由应收、应付两侧各取 keyset 前 page_size+1 条后合并得到(不重不漏)。"
|
||||
+ "若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 has_more 为 false,否则数据不完整。"
|
||||
+ "编码匹配不区分大小写(utf8mb4_unicode_ci)。");
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,901 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.domain.OmsFinanceCharge;
|
||||
import com.ruoyi.sip.domain.OmsPayableBill;
|
||||
import com.ruoyi.sip.domain.OmsPayablePaymentDetail;
|
||||
import com.ruoyi.sip.domain.OmsPayablePaymentPlan;
|
||||
import com.ruoyi.sip.domain.OmsPayableTicketPlan;
|
||||
import com.ruoyi.sip.domain.OmsReceivableBill;
|
||||
import com.ruoyi.sip.domain.OmsReceivableInvoicePlan;
|
||||
import com.ruoyi.sip.domain.OmsReceivableReceiptDetail;
|
||||
import com.ruoyi.sip.domain.OmsReceivableReceiptPlan;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpSubPage;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.OmsFinanceChargeMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayableBillMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayablePaymentDetailMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayablePaymentPlanMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPayableTicketPlanMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableBillMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableInvoicePlanMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableReceiptDetailMapper;
|
||||
import com.ruoyi.sip.mapper.OmsReceivableReceiptPlanMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MCP 工具 finance_order_position(订单财务全景·标识符点查,不分页但有界)。
|
||||
*
|
||||
* <p>数据链路:应收单 → 收款计划 → 收款明细(含核销 writeOffId)→ 开票计划;应付单 → 付款计划 → 付款明细 → 收票计划;
|
||||
* 并以 order_code 取计收(oms_finance_charge)。</p>
|
||||
*
|
||||
* <p>方案依据:docx/mcp-data-tools-plan.md 第 4.1 / 5 / 7 / 8 / 9 / 15 章与附录 A.3。</p>
|
||||
* <p>权限来源(从 Controller 的 {@code @RequiresPermissions} 抄取):
|
||||
* {@code OmsReceivableBillController#list} = {@code finance:receivable:list};
|
||||
* {@code OmsPayableBillController#list} = {@code finance:payable:list};两者"任意一个满足"即放行。</p>
|
||||
*/
|
||||
@Component
|
||||
public class FinanceOrderPositionToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
/** 单次最多展开的订单数(防越权放大扫描) */
|
||||
private static final int MAX_ORDERS = 20;
|
||||
/** 入口单据(应收/应付)查询条数上界 */
|
||||
private static final int ENTRY_LIMIT = 501;
|
||||
/** 子列表(计划/明细/核销)单次返回上限 */
|
||||
private static final int SUB_LIST_LIMIT = McpSubPage.TRUNCATE_LIMIT;
|
||||
/** 可局部翻页的子列表名 */
|
||||
private static final Set<String> SUB_LISTS = new LinkedHashSet<>(java.util.Arrays.asList(
|
||||
"receiptPlans", "receiptDetails", "invoicePlans", "paymentPlans", "paymentDetails", "ticketPlans"));
|
||||
private static final String SUB_LIST_NAMES =
|
||||
"receiptPlans,receiptDetails,invoicePlans,paymentPlans,paymentDetails,ticketPlans";
|
||||
|
||||
@Autowired
|
||||
private OmsReceivableBillMapper omsReceivableBillMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsPayableBillMapper omsPayableBillMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsReceivableReceiptPlanMapper omsReceivableReceiptPlanMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsReceivableReceiptDetailMapper omsReceivableReceiptDetailMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsReceivableInvoicePlanMapper omsReceivableInvoicePlanMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsPayablePaymentPlanMapper omsPayablePaymentPlanMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsPayablePaymentDetailMapper omsPayablePaymentDetailMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsPayableTicketPlanMapper omsPayableTicketPlanMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsFinanceChargeMapper omsFinanceChargeMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "finance_order_position";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "财务订单全景点查(只读,不分页但有界):按合同编号 / 应收单号 / 应付单号(至少一个)返回该订单的"
|
||||
+ "应收链(应收单 + 收款计划 + 收款明细含核销 + 开票计划)、应付链(应付单 + 付款计划 + 付款明细 + 收票计划)"
|
||||
+ "与计收数据(计收状态、含税/未税收入、各项成本、毛利)。金额汇总取自账单主表冗余列,不重算;"
|
||||
+ "子列表超 " + SUB_LIST_LIMIT + " 条时截断并在 data.truncated_sub_lists 标注,"
|
||||
+ "再用 sub_list + sub_cursor 对该子列表继续翻页(见 data.sub_page_info),直到 has_more=false,否则结果不完整。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("order_code", stringProperty("合同编号(订单号),应收/应付/计收的主关联键"));
|
||||
properties.put("receivable_bill_code", stringProperty("应收单号(可按单号反查所属订单)"));
|
||||
properties.put("payable_bill_code", stringProperty("应付单号(可按单号反查所属订单)"));
|
||||
properties.put("include_detail", boolProperty("是否返回计划/明细/核销子列表,默认 true;false 时仅返回账单主表"));
|
||||
properties.put("include_charge", boolProperty("是否返回计收数据(oms_finance_charge),默认 true"));
|
||||
McpSubPage.withProperties(properties, SUB_LIST_NAMES);
|
||||
return objectSchema(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
McpAuth.requireAnyPermission("finance:receivable:list", "finance:payable:list");
|
||||
|
||||
// 局部游标翻页:只返回指定子列表的一页,不需要主入口参数
|
||||
if (McpSubPage.requested(params)) {
|
||||
return handleSubListPage(params);
|
||||
}
|
||||
|
||||
McpParams.requireAny(params, "order_code", "receivable_bill_code", "payable_bill_code");
|
||||
|
||||
String orderCode = McpParams.str(params, "order_code");
|
||||
String receivableBillCode = McpParams.str(params, "receivable_bill_code");
|
||||
String payableBillCode = McpParams.str(params, "payable_bill_code");
|
||||
boolean includeDetail = McpParams.boolVal(params, "include_detail", true);
|
||||
boolean includeCharge = McpParams.boolVal(params, "include_charge", true);
|
||||
|
||||
// 入口单据:按单号反查所属订单号
|
||||
List<String> entryReceivableCodes = receivableBillCode == null
|
||||
? Collections.emptyList() : Collections.singletonList(receivableBillCode);
|
||||
List<String> entryPayableCodes = payableBillCode == null
|
||||
? Collections.emptyList() : Collections.singletonList(payableBillCode);
|
||||
|
||||
LinkedHashSet<String> orderCodes = new LinkedHashSet<>();
|
||||
if (orderCode != null) {
|
||||
orderCodes.add(orderCode);
|
||||
}
|
||||
if (!entryReceivableCodes.isEmpty()) {
|
||||
for (OmsReceivableBill bill : loadReceivableBills(entryReceivableCodes, null)) {
|
||||
String code = McpFormat.trimToNull(bill.getOrderCode());
|
||||
if (code != null) {
|
||||
orderCodes.add(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!entryPayableCodes.isEmpty()) {
|
||||
for (OmsPayableBill bill : loadPayableBills(entryPayableCodes, null)) {
|
||||
String code = McpFormat.trimToNull(bill.getOrderCode());
|
||||
if (code != null) {
|
||||
orderCodes.add(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (orderCodes.size() > MAX_ORDERS) {
|
||||
throw McpToolException.invalidParams("命中订单数过多(" + orderCodes.size() + "),单次上限 " + MAX_ORDERS
|
||||
+ ",请改为按 order_code 精确点查");
|
||||
}
|
||||
|
||||
// 无订单号可归组时(账单缺 order_code):退化为按单号直接点查
|
||||
List<Scope> scopes = new ArrayList<>();
|
||||
if (!orderCodes.isEmpty()) {
|
||||
for (String code : orderCodes) {
|
||||
scopes.add(new Scope(code, null, null));
|
||||
}
|
||||
} else {
|
||||
scopes.add(new Scope(null, entryReceivableCodes, entryPayableCodes));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> truncated = new ArrayList<>();
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Scope scope : scopes) {
|
||||
items.add(buildItem(scope, includeDetail, includeCharge, truncated));
|
||||
}
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("order_code", orderCode);
|
||||
query.put("receivable_bill_code", receivableBillCode);
|
||||
query.put("payable_bill_code", payableBillCode);
|
||||
query.put("include_detail", includeDetail);
|
||||
query.put("include_charge", includeCharge);
|
||||
query.put("resolved_order_codes", new ArrayList<>(orderCodes));
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("total", items.size());
|
||||
data.put("items", items);
|
||||
data.put("truncated_sub_lists", truncated);
|
||||
|
||||
return response(buildMetadata(), query, data);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 单订单组装
|
||||
|
||||
private Map<String, Object> buildItem(Scope scope, boolean includeDetail, boolean includeCharge,
|
||||
List<Map<String, Object>> truncated) {
|
||||
List<OmsReceivableBill> receivables = loadReceivableBills(scope.receivableBillCodes, scope.orderCode);
|
||||
List<OmsPayableBill> payables = loadPayableBills(scope.payableBillCodes, scope.orderCode);
|
||||
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("orderCode", scope.orderCode);
|
||||
item.put("receivable", buildReceivableGroup(receivables, includeDetail, truncated));
|
||||
item.put("payable", buildPayableGroup(payables, includeDetail, truncated));
|
||||
item.put("charge", includeCharge && scope.orderCode != null ? loadCharge(scope.orderCode) : null);
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildReceivableGroup(List<OmsReceivableBill> bills,
|
||||
boolean includeDetail,
|
||||
List<Map<String, Object>> truncated) {
|
||||
BigDecimal totalPriceWithTax = BigDecimal.ZERO;
|
||||
BigDecimal receivedAmount = BigDecimal.ZERO;
|
||||
BigDecimal unreceivedAmount = BigDecimal.ZERO;
|
||||
BigDecimal invoicedAmount = BigDecimal.ZERO;
|
||||
BigDecimal uninvoicedAmount = BigDecimal.ZERO;
|
||||
|
||||
List<Long> billIds = new ArrayList<>();
|
||||
for (OmsReceivableBill bill : bills) {
|
||||
totalPriceWithTax = add(totalPriceWithTax, bill.getTotalPriceWithTax());
|
||||
receivedAmount = add(receivedAmount, bill.getReceivedAmount());
|
||||
unreceivedAmount = add(unreceivedAmount, bill.getUnreceivedAmount());
|
||||
invoicedAmount = add(invoicedAmount, bill.getInvoicedAmount());
|
||||
uninvoicedAmount = add(uninvoicedAmount, bill.getUninvoicedAmount());
|
||||
if (bill.getId() != null) {
|
||||
billIds.add(bill.getId());
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, List<Map<String, Object>>> planMap = new LinkedHashMap<>();
|
||||
Map<Long, List<Map<String, Object>>> detailMap = new LinkedHashMap<>();
|
||||
Map<Long, List<Map<String, Object>>> invoicePlanMap = new LinkedHashMap<>();
|
||||
if (includeDetail && !billIds.isEmpty()) {
|
||||
List<OmsReceivableReceiptPlan> plans = omsReceivableReceiptPlanMapper.listByReceivableBillIdList(billIds);
|
||||
if (plans != null) {
|
||||
for (OmsReceivableReceiptPlan plan : plans) {
|
||||
if (plan.getReceivableBillId() == null) {
|
||||
continue;
|
||||
}
|
||||
planMap.computeIfAbsent(plan.getReceivableBillId(), key -> new ArrayList<>()).add(toReceiptPlan(plan));
|
||||
}
|
||||
}
|
||||
OmsReceivableReceiptDetail query = new OmsReceivableReceiptDetail();
|
||||
query.setReceivableBillIdList(billIds);
|
||||
List<OmsReceivableReceiptDetail> details = omsReceivableReceiptDetailMapper.list(query);
|
||||
if (details != null) {
|
||||
for (OmsReceivableReceiptDetail detail : details) {
|
||||
if (detail.getReceivableBillId() == null) {
|
||||
continue;
|
||||
}
|
||||
detailMap.computeIfAbsent(detail.getReceivableBillId(), key -> new ArrayList<>()).add(toReceiptDetail(detail));
|
||||
}
|
||||
}
|
||||
List<OmsReceivableInvoicePlan> invoicePlans = omsReceivableInvoicePlanMapper.listByReceivableBillIdList(billIds);
|
||||
if (invoicePlans != null) {
|
||||
for (OmsReceivableInvoicePlan plan : invoicePlans) {
|
||||
if (plan.getReceivableBillId() == null) {
|
||||
continue;
|
||||
}
|
||||
invoicePlanMap.computeIfAbsent(plan.getReceivableBillId(), key -> new ArrayList<>()).add(toInvoicePlan(plan));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> billItems = new ArrayList<>();
|
||||
for (OmsReceivableBill bill : bills) {
|
||||
Map<String, Object> billItem = toReceivableBill(bill);
|
||||
if (includeDetail) {
|
||||
Long id = bill.getId();
|
||||
String parent = McpFormat.trim(bill.getReceivableBillCode());
|
||||
billItem.put("receiptPlans", truncate("receiptPlans", parent,
|
||||
id == null ? Collections.emptyList() : planMap.getOrDefault(id, Collections.emptyList()), truncated));
|
||||
billItem.put("receiptDetails", truncate("receiptDetails", parent,
|
||||
id == null ? Collections.emptyList() : detailMap.getOrDefault(id, Collections.emptyList()), truncated));
|
||||
billItem.put("invoicePlans", truncate("invoicePlans", parent,
|
||||
id == null ? Collections.emptyList() : invoicePlanMap.getOrDefault(id, Collections.emptyList()), truncated));
|
||||
}
|
||||
billItems.add(billItem);
|
||||
}
|
||||
|
||||
Map<String, Object> group = new LinkedHashMap<>();
|
||||
group.put("billCount", billItems.size());
|
||||
group.put("totalPriceWithTax", totalPriceWithTax);
|
||||
group.put("receivedAmount", receivedAmount);
|
||||
group.put("unreceivedAmount", unreceivedAmount);
|
||||
group.put("invoicedAmount", invoicedAmount);
|
||||
group.put("uninvoicedAmount", uninvoicedAmount);
|
||||
group.put("bills", billItems);
|
||||
return group;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildPayableGroup(List<OmsPayableBill> bills,
|
||||
boolean includeDetail,
|
||||
List<Map<String, Object>> truncated) {
|
||||
BigDecimal totalPriceWithTax = BigDecimal.ZERO;
|
||||
BigDecimal paidPaymentAmount = BigDecimal.ZERO;
|
||||
BigDecimal unpaidPaymentAmount = BigDecimal.ZERO;
|
||||
BigDecimal receivedTicketAmount = BigDecimal.ZERO;
|
||||
BigDecimal unreceivedTicketAmount = BigDecimal.ZERO;
|
||||
|
||||
List<Long> billIds = new ArrayList<>();
|
||||
for (OmsPayableBill bill : bills) {
|
||||
totalPriceWithTax = add(totalPriceWithTax, bill.getTotalPriceWithTax());
|
||||
paidPaymentAmount = add(paidPaymentAmount, bill.getPaidPaymentAmount());
|
||||
unpaidPaymentAmount = add(unpaidPaymentAmount, bill.getUnpaidPaymentAmount());
|
||||
receivedTicketAmount = add(receivedTicketAmount, bill.getReceivedTicketAmount());
|
||||
unreceivedTicketAmount = add(unreceivedTicketAmount, bill.getUnreceivedTicketAmount());
|
||||
if (bill.getId() != null) {
|
||||
billIds.add(bill.getId());
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, List<Map<String, Object>>> planMap = new LinkedHashMap<>();
|
||||
Map<Long, List<Map<String, Object>>> detailMap = new LinkedHashMap<>();
|
||||
Map<Long, List<Map<String, Object>>> ticketPlanMap = new LinkedHashMap<>();
|
||||
if (includeDetail && !billIds.isEmpty()) {
|
||||
List<OmsPayablePaymentPlan> plans = omsPayablePaymentPlanMapper.listDetailByPayableBillIdList(billIds);
|
||||
if (plans != null) {
|
||||
for (OmsPayablePaymentPlan plan : plans) {
|
||||
if (plan.getPayableBillId() == null) {
|
||||
continue;
|
||||
}
|
||||
planMap.computeIfAbsent(plan.getPayableBillId(), key -> new ArrayList<>()).add(toPaymentPlan(plan));
|
||||
}
|
||||
}
|
||||
OmsPayablePaymentDetail query = new OmsPayablePaymentDetail();
|
||||
query.setPayableBillIdList(billIds);
|
||||
List<OmsPayablePaymentDetail> details = omsPayablePaymentDetailMapper.list(query);
|
||||
if (details != null) {
|
||||
for (OmsPayablePaymentDetail detail : details) {
|
||||
if (detail.getPayableBillId() == null) {
|
||||
continue;
|
||||
}
|
||||
detailMap.computeIfAbsent(detail.getPayableBillId(), key -> new ArrayList<>()).add(toPaymentDetail(detail));
|
||||
}
|
||||
}
|
||||
List<OmsPayableTicketPlan> ticketPlans = omsPayableTicketPlanMapper.listByPayableBillIdList(billIds);
|
||||
if (ticketPlans != null) {
|
||||
for (OmsPayableTicketPlan plan : ticketPlans) {
|
||||
if (plan.getPayableBillId() == null) {
|
||||
continue;
|
||||
}
|
||||
ticketPlanMap.computeIfAbsent(plan.getPayableBillId(), key -> new ArrayList<>()).add(toTicketPlan(plan));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> billItems = new ArrayList<>();
|
||||
for (OmsPayableBill bill : bills) {
|
||||
Map<String, Object> billItem = toPayableBill(bill);
|
||||
if (includeDetail) {
|
||||
Long id = bill.getId();
|
||||
String parent = McpFormat.trim(bill.getPayableBillCode());
|
||||
billItem.put("paymentPlans", truncate("paymentPlans", parent,
|
||||
id == null ? Collections.emptyList() : planMap.getOrDefault(id, Collections.emptyList()), truncated));
|
||||
billItem.put("paymentDetails", truncate("paymentDetails", parent,
|
||||
id == null ? Collections.emptyList() : detailMap.getOrDefault(id, Collections.emptyList()), truncated));
|
||||
billItem.put("ticketPlans", truncate("ticketPlans", parent,
|
||||
id == null ? Collections.emptyList() : ticketPlanMap.getOrDefault(id, Collections.emptyList()), truncated));
|
||||
}
|
||||
billItems.add(billItem);
|
||||
}
|
||||
|
||||
Map<String, Object> group = new LinkedHashMap<>();
|
||||
group.put("billCount", billItems.size());
|
||||
group.put("totalPriceWithTax", totalPriceWithTax);
|
||||
group.put("paidPaymentAmount", paidPaymentAmount);
|
||||
group.put("unpaidPaymentAmount", unpaidPaymentAmount);
|
||||
group.put("receivedTicketAmount", receivedTicketAmount);
|
||||
group.put("unreceivedTicketAmount", unreceivedTicketAmount);
|
||||
group.put("bills", billItems);
|
||||
return group;
|
||||
}
|
||||
|
||||
private Map<String, Object> loadCharge(String orderCode) {
|
||||
OmsFinanceCharge query = new OmsFinanceCharge();
|
||||
query.setOrderCode(orderCode);
|
||||
List<OmsFinanceCharge> rows = omsFinanceChargeMapper.selectOmsFinanceChargeList(query);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
OmsFinanceCharge row = rows.get(0);
|
||||
Map<String, Object> charge = new LinkedHashMap<>();
|
||||
charge.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
charge.put("chargeStatus", row.getChargeStatus());
|
||||
charge.put("chargeStatusName", chargeStatusName(row.getChargeStatus()));
|
||||
charge.put("bizChargeDate", McpFormat.date(row.getBizChargeDate()));
|
||||
charge.put("financeChargeDate", McpFormat.date(row.getFinanceChargeDate()));
|
||||
charge.put("incomeWithTaxTotal", row.getIncomeWithTaxTotal());
|
||||
charge.put("incomeWithoutTaxTotal", row.getIncomeWithoutTaxTotal());
|
||||
charge.put("allCostWithoutTax", row.getAllCostWithoutTax());
|
||||
charge.put("grossProfit", row.getGrossProfit());
|
||||
charge.put("grossProfitRate", row.getGrossProfitRate());
|
||||
charge.put("orderChannel", row.getOrderChannel());
|
||||
charge.put("supplier", row.getSupplier());
|
||||
charge.put("partnerCode", McpFormat.trim(row.getPartnerCode()));
|
||||
charge.put("partnerName", row.getPartnerName());
|
||||
charge.put("projectCode", row.getProjectCode());
|
||||
charge.put("projectName", row.getProjectName());
|
||||
return charge;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 行映射
|
||||
|
||||
private Map<String, Object> toReceivableBill(OmsReceivableBill row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("receivableBillCode", McpFormat.trim(row.getReceivableBillCode()));
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("inventoryCode", McpFormat.trim(row.getInventoryCode()));
|
||||
item.put("partnerCode", McpFormat.trim(row.getPartnerCode()));
|
||||
item.put("partnerName", row.getPartnerName());
|
||||
item.put("productType", row.getProductType());
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("totalPriceWithTax", row.getTotalPriceWithTax());
|
||||
item.put("totalPriceWithoutTax", row.getTotalPriceWithoutTax());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
item.put("taxAmount", row.getTaxAmount());
|
||||
item.put("receivedAmount", row.getReceivedAmount());
|
||||
item.put("unreceivedAmount", row.getUnreceivedAmount());
|
||||
item.put("invoicedAmount", row.getInvoicedAmount());
|
||||
item.put("uninvoicedAmount", row.getUninvoicedAmount());
|
||||
item.put("planAmount", row.getPlanAmount());
|
||||
item.put("planReceiptDate", McpFormat.date(row.getPlanReceiptDate()));
|
||||
item.put("planInvoiceAmount", row.getPlanInvoiceAmount());
|
||||
item.put("planInvoiceDate", McpFormat.date(row.getPlanInvoiceDate()));
|
||||
item.put("projectCode", row.getProjectCode());
|
||||
item.put("projectName", row.getProjectName());
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
item.put("updateTime", McpFormat.dateTime(row.getUpdateTime()));
|
||||
item.put("remark", row.getRemark());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toPayableBill(OmsPayableBill row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("payableBillCode", McpFormat.trim(row.getPayableBillCode()));
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("inventoryCode", McpFormat.trim(row.getInventoryCode()));
|
||||
item.put("vendorCode", McpFormat.trim(row.getVendorCode()));
|
||||
item.put("vendorName", row.getVendorName());
|
||||
item.put("productType", row.getProductType());
|
||||
item.put("productLevel2Type", row.getProductLevel2Type());
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("totalPriceWithTax", row.getTotalPriceWithTax());
|
||||
item.put("totalPriceWithoutTax", row.getTotalPriceWithoutTax());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
item.put("taxAmount", row.getTaxAmount());
|
||||
item.put("paidPaymentAmount", row.getPaidPaymentAmount());
|
||||
item.put("unpaidPaymentAmount", row.getUnpaidPaymentAmount());
|
||||
item.put("receivedTicketAmount", row.getReceivedTicketAmount());
|
||||
item.put("unreceivedTicketAmount", row.getUnreceivedTicketAmount());
|
||||
item.put("planPaymentDate", McpFormat.date(row.getPlanPaymentDate()));
|
||||
item.put("planAmount", row.getPlanAmount());
|
||||
item.put("planTicketDate", McpFormat.date(row.getPlanTicketDate()));
|
||||
item.put("planTicketAmount", row.getPlanTicketAmount());
|
||||
item.put("projectCode", row.getProjectCode());
|
||||
item.put("projectName", row.getProjectName());
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
item.put("updateTime", McpFormat.dateTime(row.getUpdateTime()));
|
||||
item.put("remark", row.getRemark());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toReceiptPlan(OmsReceivableReceiptPlan row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("planReceiptDate", McpFormat.date(row.getPlanReceiptDate()));
|
||||
item.put("planAmount", row.getPlanAmount());
|
||||
item.put("planRate", row.getPlanRate());
|
||||
item.put("remark", row.getRemark());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toInvoicePlan(OmsReceivableInvoicePlan row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("planInvoiceDate", McpFormat.date(row.getPlanInvoiceDate()));
|
||||
item.put("planAmount", row.getPlanAmount());
|
||||
item.put("planRate", row.getPlanRate());
|
||||
item.put("remark", row.getRemark());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toReceiptDetail(OmsReceivableReceiptDetail row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("receiptPlanId", row.getReceiptPlanId());
|
||||
item.put("receiptTime", McpFormat.dateTime(row.getReceiptTime()));
|
||||
item.put("receiptAmount", row.getReceiptAmount());
|
||||
item.put("receiptAmountWithoutTax", row.getReceiptAmountWithoutTax());
|
||||
item.put("receiptAmountTax", row.getReceiptAmountTax());
|
||||
item.put("receiptRate", row.getReceiptRate());
|
||||
item.put("receiptBillCode", McpFormat.trim(row.getReceiptBillCode()));
|
||||
item.put("receivableDetailType", row.getReceivableDetailType());
|
||||
item.put("receivableDetailTypeName", receivableDetailTypeName(row.getReceivableDetailType()));
|
||||
item.put("receiptStatus", row.getReceiptStatus());
|
||||
item.put("actualReceiptTime", McpFormat.dateTime(row.getActualReceiptTime()));
|
||||
item.put("writeOffId", row.getWriteOffId());
|
||||
item.put("remark", row.getRemark());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toPaymentPlan(OmsPayablePaymentPlan row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("planPaymentDate", McpFormat.date(row.getPlanPaymentDate()));
|
||||
item.put("planAmount", row.getPlanAmount());
|
||||
item.put("planRate", row.getPlanRate());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toPaymentDetail(OmsPayablePaymentDetail row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("paymentPlanId", row.getPaymentPlanId());
|
||||
item.put("paymentTime", McpFormat.dateTime(row.getPaymentTime()));
|
||||
item.put("paymentAmount", row.getPaymentAmount());
|
||||
item.put("paymentAmountWithoutTax", row.getPaymentAmountWithoutTax());
|
||||
item.put("paymentAmountTax", row.getPaymentAmountTax());
|
||||
item.put("paymentRate", row.getPaymentRate());
|
||||
item.put("paymentBillCode", McpFormat.trim(row.getPaymentBillCode()));
|
||||
item.put("payableDetailType", row.getPayableDetailType());
|
||||
item.put("payableDetailTypeName", payableDetailTypeName(row.getPayableDetailType()));
|
||||
item.put("paymentStatus", row.getPaymentStatus());
|
||||
item.put("actualPaymentTime", McpFormat.dateTime(row.getActualPaymentTime()));
|
||||
item.put("writeOffId", row.getWriteOffId());
|
||||
item.put("remark", row.getRemark());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toTicketPlan(OmsPayableTicketPlan row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("planTicketDate", McpFormat.date(row.getPlanTicketDate()));
|
||||
item.put("planAmount", row.getPlanAmount());
|
||||
item.put("planRate", row.getPlanRate());
|
||||
return item;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 局部游标翻页
|
||||
|
||||
/**
|
||||
* 局部游标翻页:只加载并返回某一张账单下某个子列表的一页。
|
||||
*/
|
||||
private Object handleSubListPage(Map<String, Object> params) {
|
||||
String subList = McpParams.requireValue(params, "sub_list", SUB_LISTS);
|
||||
McpSubPage.SubCursor state = McpSubPage.resolve(params, getToolName(), subList, SUB_LIST_NAMES,
|
||||
subListSortKey(subList));
|
||||
String parent = state.getParent() != null
|
||||
? state.getParent() : McpParams.requireStr(params, "sub_parent");
|
||||
int pageSize = McpSubPage.pageSize(params);
|
||||
|
||||
List<Map<String, Object>> rows = McpSubPage.sortedMapsById(loadSubListRows(subList, parent));
|
||||
McpSubPage.SubPageResult<Map<String, Object>> page = McpSubPage.page(state, parent, rows, pageSize);
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("sub_list", subList);
|
||||
query.put("sub_parent", parent);
|
||||
query.put("page_size", pageSize);
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("sub_list", subList);
|
||||
data.put("sub_parent", parent);
|
||||
data.put("total", rows.size());
|
||||
data.put("items", page.getItems());
|
||||
data.put("sub_page_info", page.getPageInfo());
|
||||
return response(buildSubListMetadata(subList), query, data);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> loadSubListRows(String subList, String parent) {
|
||||
List<Map<String, Object>> rows = new ArrayList<>();
|
||||
if (isReceivableSubList(subList)) {
|
||||
List<Long> billIds = receivableBillIds(parent);
|
||||
if ("receiptPlans".equals(subList)) {
|
||||
for (OmsReceivableReceiptPlan row : safeRows(
|
||||
omsReceivableReceiptPlanMapper.listByReceivableBillIdList(billIds))) {
|
||||
rows.add(toReceiptPlan(row));
|
||||
}
|
||||
} else if ("receiptDetails".equals(subList)) {
|
||||
OmsReceivableReceiptDetail query = new OmsReceivableReceiptDetail();
|
||||
query.setReceivableBillIdList(billIds);
|
||||
for (OmsReceivableReceiptDetail row : safeRows(omsReceivableReceiptDetailMapper.list(query))) {
|
||||
rows.add(toReceiptDetail(row));
|
||||
}
|
||||
} else {
|
||||
for (OmsReceivableInvoicePlan row : safeRows(
|
||||
omsReceivableInvoicePlanMapper.listByReceivableBillIdList(billIds))) {
|
||||
rows.add(toInvoicePlan(row));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
List<Long> billIds = payableBillIds(parent);
|
||||
if ("paymentPlans".equals(subList)) {
|
||||
for (OmsPayablePaymentPlan row : safeRows(
|
||||
omsPayablePaymentPlanMapper.listDetailByPayableBillIdList(billIds))) {
|
||||
rows.add(toPaymentPlan(row));
|
||||
}
|
||||
} else if ("paymentDetails".equals(subList)) {
|
||||
OmsPayablePaymentDetail query = new OmsPayablePaymentDetail();
|
||||
query.setPayableBillIdList(billIds);
|
||||
for (OmsPayablePaymentDetail row : safeRows(omsPayablePaymentDetailMapper.list(query))) {
|
||||
rows.add(toPaymentDetail(row));
|
||||
}
|
||||
} else {
|
||||
for (OmsPayableTicketPlan row : safeRows(omsPayableTicketPlanMapper.listByPayableBillIdList(billIds))) {
|
||||
rows.add(toTicketPlan(row));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private List<Long> receivableBillIds(String parent) {
|
||||
List<Long> billIds = new ArrayList<>();
|
||||
for (OmsReceivableBill bill : loadReceivableBills(Collections.singletonList(parent), null)) {
|
||||
if (bill.getId() != null) {
|
||||
billIds.add(bill.getId());
|
||||
}
|
||||
}
|
||||
if (billIds.isEmpty()) {
|
||||
throw McpToolException.invalidParams("未找到应收单:" + parent
|
||||
+ "(sub_parent 取值应取自 data.truncated_sub_lists[].parent 或应收单号)");
|
||||
}
|
||||
return billIds;
|
||||
}
|
||||
|
||||
private List<Long> payableBillIds(String parent) {
|
||||
List<Long> billIds = new ArrayList<>();
|
||||
for (OmsPayableBill bill : loadPayableBills(Collections.singletonList(parent), null)) {
|
||||
if (bill.getId() != null) {
|
||||
billIds.add(bill.getId());
|
||||
}
|
||||
}
|
||||
if (billIds.isEmpty()) {
|
||||
throw McpToolException.invalidParams("未找到应付单:" + parent
|
||||
+ "(sub_parent 取值应取自 data.truncated_sub_lists[].parent 或应付单号)");
|
||||
}
|
||||
return billIds;
|
||||
}
|
||||
|
||||
private boolean isReceivableSubList(String subList) {
|
||||
return "receiptPlans".equals(subList) || "receiptDetails".equals(subList) || "invoicePlans".equals(subList);
|
||||
}
|
||||
|
||||
private String subListSortKey(String subList) {
|
||||
if ("receiptPlans".equals(subList)) {
|
||||
return "oms_receivable_receipt_plan.id";
|
||||
}
|
||||
if ("receiptDetails".equals(subList)) {
|
||||
return "oms_receivable_receipt_detail.id";
|
||||
}
|
||||
if ("invoicePlans".equals(subList)) {
|
||||
return "oms_receivable_invoice_plan.id";
|
||||
}
|
||||
if ("paymentPlans".equals(subList)) {
|
||||
return "oms_payable_payment_plan.id";
|
||||
}
|
||||
if ("paymentDetails".equals(subList)) {
|
||||
return "oms_payable_payment_detail.id";
|
||||
}
|
||||
return "oms_payable_ticket_plan.id";
|
||||
}
|
||||
|
||||
private <T> List<T> safeRows(List<T> rows) {
|
||||
return rows == null ? Collections.<T>emptyList() : rows;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSubListMetadata(String subList) {
|
||||
Map<String, Object> metadata = metadata("finance_order_position 的局部游标翻页:只返回某一张账单下 "
|
||||
+ subList + " 子列表的一页(按 id 升序,与主调用截断点严格衔接)");
|
||||
metadata.put("query_fields", mapData(
|
||||
"sub_list", "要翻页的子列表名,取值 " + SUB_LIST_NAMES,
|
||||
"sub_parent", "父实体标识(应收单号 receivableBillCode / 应付单号 payableBillCode),首次翻页必填",
|
||||
"sub_cursor", "局部游标,原样回传上次的 data.sub_page_info.next_cursor;已携带 sub_parent,与 cursor 互斥",
|
||||
"page_size", "每页条数,默认 " + McpSubPage.DEFAULT_PAGE_SIZE + ",上限 " + McpSubPage.MAX_PAGE_SIZE
|
||||
));
|
||||
metadata.put("data_fields", mapData(
|
||||
"sub_list", "本次翻页的子列表名",
|
||||
"sub_parent", "父实体标识(账单号)",
|
||||
"total", "★该子列表的总条数(不是订单数、也不是账单数)",
|
||||
"items", "本页数据,字段语义与主调用 items[].receivable.bills[].<sub_list>[] / payable.bills[].<sub_list>[] 一致",
|
||||
"sub_page_info", "分页信封,结构同 page_info:returned / page_size / has_more / next_cursor / sort_by / page_no / total_count"
|
||||
));
|
||||
metadata.put("item_fields", subListItemFields(subList));
|
||||
metadata.put("notes", "主调用中每个子列表最多返回 " + SUB_LIST_LIMIT + " 条,超出时 data.truncated_sub_lists 会给出"
|
||||
+ " {list, parent, total, returned, next_cursor};用 sub_list + 该 next_cursor 作为 sub_cursor 继续翻页,"
|
||||
+ "直到 sub_page_info.has_more=false,否则数据不完整。首次翻页(无 sub_cursor)需同时传 sub_parent;"
|
||||
+ "局部游标最多翻 " + McpSubPage.MAX_PAGES + " 页。本工具子列表按 id 升序返回。");
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> subListItemFields(String subList) {
|
||||
if ("receiptPlans".equals(subList) || "paymentPlans".equals(subList)) {
|
||||
return mapData(
|
||||
"id", "主键(排序键)",
|
||||
"planReceiptDate/planPaymentDate", "计划收款/付款日期",
|
||||
"planAmount", "计划金额",
|
||||
"planRate", "比例",
|
||||
"remark", "备注"
|
||||
);
|
||||
}
|
||||
if ("invoicePlans".equals(subList) || "ticketPlans".equals(subList)) {
|
||||
return mapData(
|
||||
"id", "主键(排序键)",
|
||||
"planInvoiceDate/planTicketDate", "计划开票/收票日期",
|
||||
"planAmount", "计划金额",
|
||||
"planRate", "比例",
|
||||
"remark", "备注"
|
||||
);
|
||||
}
|
||||
if ("receiptDetails".equals(subList)) {
|
||||
return mapData(
|
||||
"id", "主键(排序键)",
|
||||
"receiptPlanId", "收款计划ID",
|
||||
"receiptTime", "收款时间",
|
||||
"receiptAmount", "收款金额",
|
||||
"receiptAmountWithoutTax", "未税金额",
|
||||
"receiptAmountTax", "税额",
|
||||
"receiptBillCode", "收款单号",
|
||||
"receivableDetailType", "明细类型(1申请收款/2预收核销/3申请退款)",
|
||||
"receiptStatus", "收款单状态",
|
||||
"actualReceiptTime", "实际收款时间",
|
||||
"writeOffId", "核销单ID"
|
||||
);
|
||||
}
|
||||
return mapData(
|
||||
"id", "主键(排序键)",
|
||||
"paymentPlanId", "付款计划ID",
|
||||
"paymentTime", "付款时间",
|
||||
"paymentAmount", "付款金额",
|
||||
"paymentAmountWithoutTax", "未税金额",
|
||||
"paymentAmountTax", "税额",
|
||||
"paymentBillCode", "付款单号",
|
||||
"payableDetailType", "明细类型(1申请付款/2预付核销/3退款/4反核销/5核销)",
|
||||
"paymentStatus", "付款单状态",
|
||||
"actualPaymentTime", "实际付款时间",
|
||||
"writeOffId", "核销单ID"
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private List<OmsReceivableBill> loadReceivableBills(List<String> billCodes, String orderCode) {
|
||||
List<String> orderCodes = orderCode == null ? Collections.emptyList() : Collections.singletonList(orderCode);
|
||||
List<OmsReceivableBill> rows = omsReceivableBillMapper.listReceivableBillPage(billCodes, orderCodes, null,
|
||||
null, null, null, null, ENTRY_LIMIT);
|
||||
return rows == null ? Collections.emptyList() : rows;
|
||||
}
|
||||
|
||||
private List<OmsPayableBill> loadPayableBills(List<String> billCodes, String orderCode) {
|
||||
List<String> orderCodes = orderCode == null ? Collections.emptyList() : Collections.singletonList(orderCode);
|
||||
List<OmsPayableBill> rows = omsPayableBillMapper.listPayableBillPage(billCodes, orderCodes, null,
|
||||
null, null, null, null, ENTRY_LIMIT);
|
||||
return rows == null ? Collections.emptyList() : rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子列表截断:超出上限时只返回前 SUB_LIST_LIMIT 条,并登记 {list, parent, total, returned, next_cursor},
|
||||
* 调用方可用 sub_list + next_cursor 继续翻页(避免数据缺失)。
|
||||
*/
|
||||
private List<Map<String, Object>> truncate(String subList, String parent,
|
||||
List<Map<String, Object>> rows,
|
||||
List<Map<String, Object>> truncated) {
|
||||
// 先按主键定序,保证"截断点 == 续页起点",避免跨调用重复/漏数据
|
||||
List<Map<String, Object>> sorted = McpSubPage.sortedMapsById(rows);
|
||||
if (sorted.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (sorted.size() > SUB_LIST_LIMIT) {
|
||||
truncated.add(McpSubPage.truncatedItem(getToolName(), subList, parent, SUB_LIST_LIMIT, sorted.size()));
|
||||
return new ArrayList<>(sorted.subList(0, SUB_LIST_LIMIT));
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private BigDecimal add(BigDecimal base, BigDecimal value) {
|
||||
return value == null ? base : base.add(value);
|
||||
}
|
||||
|
||||
private String chargeStatusName(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsFinanceCharge.ChargeStatusEnum value : OmsFinanceCharge.ChargeStatusEnum.values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private String receivableDetailTypeName(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsReceivableReceiptDetail.ReceivableDetailTypeEnum value
|
||||
: OmsReceivableReceiptDetail.ReceivableDetailTypeEnum.values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private String payableDetailTypeName(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsPayablePaymentDetail.PayableDetailTypeEnum value
|
||||
: OmsPayablePaymentDetail.PayableDetailTypeEnum.values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata() {
|
||||
Map<String, Object> metadata = metadata("财务订单全景点查:按 order_code / receivable_bill_code / payable_bill_code 关联"
|
||||
+ "应收链(应收单→收款计划→收款明细→开票计划)、应付链(应付单→付款计划→付款明细→收票计划)与计收数据。");
|
||||
metadata.put("query_fields", mapData(
|
||||
"order_code", "合同编号(订单号)",
|
||||
"receivable_bill_code", "应收单号(按单号反查所属订单)",
|
||||
"payable_bill_code", "应付单号(按单号反查所属订单)",
|
||||
"include_detail", "是否返回计划/明细/核销子列表(默认 true)",
|
||||
"include_charge", "是否返回计收数据(默认 true)",
|
||||
"sub_list", "可选:子列表名(" + SUB_LIST_NAMES + "),传该参数表示只翻该子列表的一页",
|
||||
"sub_parent", "可选:父实体标识(应收单号 / 应付单号),首次翻转子列表时必填",
|
||||
"sub_cursor", "可选:局部游标,原样回传 data.sub_page_info.next_cursor;与 cursor 互斥",
|
||||
"page_size", "可选:子列表每页条数,默认 " + McpSubPage.DEFAULT_PAGE_SIZE + ",上限 " + McpSubPage.MAX_PAGE_SIZE
|
||||
));
|
||||
metadata.put("data_fields", mapData(
|
||||
"total", "返回的订单数",
|
||||
"items", "每个订单一行:orderCode / receivable / payable / charge",
|
||||
"truncated_sub_lists", "被子列表上限(" + SUB_LIST_LIMIT + ")截断的条目:{list, parent, total, returned, next_cursor};"
|
||||
+ "用 sub_list + next_cursor 作为 sub_cursor 继续翻页,直到 has_more=false,否则数据不完整"
|
||||
));
|
||||
metadata.put("item_fields", mapData(
|
||||
"orderCode", "合同编号(账单缺 order_code 时可能为 null,此时按单号直接返回)",
|
||||
"receivable", "应收侧:billCount 应收单数、totalPriceWithTax 含税总额、receivedAmount 已收、unreceivedAmount 未收、"
|
||||
+ "invoicedAmount 已开票、uninvoicedAmount 未开票、bills[] 应收单列表",
|
||||
"receivable.bills[].receivableBillCode", "应收单号",
|
||||
"receivable.bills[].inventoryCode", "出库/入库单号",
|
||||
"receivable.bills[].partnerCode/partnerName", "客户编码/名称",
|
||||
"receivable.bills[].receiptPlans[]", "收款计划:planReceiptDate 计划收款日期 / planAmount 计划金额 / planRate 比例",
|
||||
"receivable.bills[].receiptDetails[]", "收款明细:receiptTime 收款时间 / receiptAmount 收款金额 / receiptAmountWithoutTax 未税 / "
|
||||
+ "receiptAmountTax 税额 / receiptBillCode 收款单号 / receivableDetailType(1申请收款/2预收核销/3申请退款) / "
|
||||
+ "receiptStatus 收款单状态 / actualReceiptTime 实际收款时间 / writeOffId 核销单ID",
|
||||
"receivable.bills[].invoicePlans[]", "开票计划:planInvoiceDate 计划开票日期 / planAmount 计划金额 / planRate 比例",
|
||||
"payable", "应付侧:billCount 应付单数、totalPriceWithTax 含税总额、paidPaymentAmount 已付、unpaidPaymentAmount 未付、"
|
||||
+ "receivedTicketAmount 已收票、unreceivedTicketAmount 未收票、bills[] 应付单列表",
|
||||
"payable.bills[].payableBillCode", "应付单号",
|
||||
"payable.bills[].vendorCode/vendorName", "制造商编码/名称",
|
||||
"payable.bills[].paymentPlans[]", "付款计划:planPaymentDate 计划付款日期 / planAmount 计划金额 / planRate 比例",
|
||||
"payable.bills[].paymentDetails[]", "付款明细:paymentTime 付款时间 / paymentAmount 付款金额 / paymentAmountWithoutTax 未税 / "
|
||||
+ "paymentAmountTax 税额 / paymentBillCode 付款单号 / payableDetailType(1申请付款/2预付核销/3退款/4反核销/5核销) / "
|
||||
+ "paymentStatus 付款单状态 / actualPaymentTime 实际付款时间 / writeOffId 核销单ID",
|
||||
"payable.bills[].ticketPlans[]", "收票计划:planTicketDate 计划收票日期 / planAmount 计划金额 / planRate 比例",
|
||||
"charge", "计收数据(无计收记录时为 null):chargeStatus/chargeStatusName 计收状态、incomeWithTaxTotal 含税收入、"
|
||||
+ "incomeWithoutTaxTotal 未税收入、allCostWithoutTax 成本合计、grossProfit 毛利、grossProfitRate 毛利率"
|
||||
));
|
||||
metadata.put("dict_fields", mapData(
|
||||
"charge.chargeStatusName", "枚举 OmsFinanceCharge.ChargeStatusEnum(0等待收款/1商务可申请计收/2财务已申请计收/3已完成计收)",
|
||||
"receivable.bills[].receiptDetails[].receivableDetailTypeName", "枚举 OmsReceivableReceiptDetail.ReceivableDetailTypeEnum(1申请收款/2预收核销/3申请退款)",
|
||||
"payable.bills[].paymentDetails[].payableDetailTypeName", "枚举 OmsPayablePaymentDetail.PayableDetailTypeEnum(1申请付款/2预付核销/3退款/4反核销/5核销)"
|
||||
));
|
||||
metadata.put("aggregation_rule", mapData(
|
||||
"amount_basis", "★金额一致性:receivable/payable 的汇总(含税总额、已收/未收、已开票/未开票、已付/未付、已收票/未收票)"
|
||||
+ "一律直接取账单主表(oms_receivable_bill / oms_payable_bill)的冗余列求和,不重算;"
|
||||
+ "计划/明细子列表仅作展示与追溯,其金额合计不参与汇总,两者口径可能不一致(明细可能缺少未核销部分或未回填)",
|
||||
"charge_derived", "charge 的 allCostWithoutTax / grossProfit / grossProfitRate 为 OmsFinanceCharge 类内计算属性(非库列)",
|
||||
"sub_list_limit", "每个子列表最多返回 " + SUB_LIST_LIMIT + " 条,超出时截断并在 data.truncated_sub_lists 标注"
|
||||
+ " {list, parent, total, returned, next_cursor},可用 sub_list + sub_cursor 继续翻页(按 id 升序,不重不漏)",
|
||||
"bounded", "本工具不分页但有界:入口单据最多 " + ENTRY_LIMIT + " 条,订单最多 " + MAX_ORDERS + " 个"
|
||||
));
|
||||
metadata.put("notes", "入口为 receivable_bill_code / payable_bill_code 时会先反查所属 order_code,再返回该订单的完整链路;"
|
||||
+ "若账单本身没有 order_code,则退化为按单号直接返回(orderCode=null)。"
|
||||
+ "金额匹配不区分大小写(utf8mb4_unicode_ci),编码类字段入参/出参统一 trim;返回编码不区分大小写。");
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/** 一次点查的作用域:优先按 order_code 归组,无订单号时按单号直接查。 */
|
||||
private static class Scope {
|
||||
private final String orderCode;
|
||||
private final List<String> receivableBillCodes;
|
||||
private final List<String> payableBillCodes;
|
||||
|
||||
private Scope(String orderCode, List<String> receivableBillCodes, List<String> payableBillCodes) {
|
||||
this.orderCode = orderCode;
|
||||
this.receivableBillCodes = receivableBillCodes == null ? Collections.emptyList() : receivableBillCodes;
|
||||
this.payableBillCodes = payableBillCodes == null ? Collections.emptyList() : payableBillCodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,776 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.domain.InventoryDelivery;
|
||||
import com.ruoyi.sip.domain.InventoryInfo;
|
||||
import com.ruoyi.sip.domain.InventoryOuter;
|
||||
import com.ruoyi.sip.domain.InventoryOuterDetail;
|
||||
import com.ruoyi.sip.domain.OmsInventoryDeliveryDetail;
|
||||
import com.ruoyi.sip.domain.OmsInventoryInner;
|
||||
import com.ruoyi.sip.domain.OmsStockInfo;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpSubPage;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.InventoryDeliveryMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryInfoMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryOuterDetailMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryOuterMapper;
|
||||
import com.ruoyi.sip.mapper.OmsInventoryDeliveryDetailMapper;
|
||||
import com.ruoyi.sip.mapper.OmsInventoryInnerMapper;
|
||||
import com.ruoyi.sip.mapper.OmsStockInfoMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MCP 工具 inventory_flow(单据流转链点查,不分页,有界)。
|
||||
* <p>权限来源:InventoryOuterController({@code @RequiresPermissions("inventory:outer:list")})、
|
||||
* InventoryDeliveryController({@code inventory:delivery:list"})。</p>
|
||||
*/
|
||||
@Component
|
||||
public class InventoryFlowToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
/** 单个出库单下的子列表(snDetails / deliveries)单次返回上限;超出时给出 sub_cursor 续页 */
|
||||
private static final int MAX_SUB_LIST = McpSubPage.TRUNCATE_LIMIT;
|
||||
/** order_code 入口可能命中多条出库单,超过该值时不再逐条做"发货反查"兜底,避免 N+1 */
|
||||
private static final int MAX_FALLBACK_OUTERS = 100;
|
||||
/** 可局部翻页的子列表名 */
|
||||
private static final Set<String> SUB_LISTS = new LinkedHashSet<>(
|
||||
java.util.Arrays.asList("outerDetails", "snDetails", "deliveries"));
|
||||
private static final String SUB_LIST_NAMES = "outerDetails,snDetails,deliveries";
|
||||
|
||||
@Autowired
|
||||
private InventoryOuterMapper inventoryOuterMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryOuterDetailMapper inventoryOuterDetailMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryInfoMapper inventoryInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsInventoryInnerMapper omsInventoryInnerMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryDeliveryMapper inventoryDeliveryMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsInventoryDeliveryDetailMapper omsInventoryDeliveryDetailMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsStockInfoMapper omsStockInfoMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "inventory_flow";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "按出库单号或合同编号点查完整单据流转链:出库单 + 出库明细(仓库拆分)+ SN 明细 + 入库信息 + 发货单(含 SN 明细)+ 备货状态。"
|
||||
+ "本工具不分页;子列表单次最多返回 " + MAX_SUB_LIST + " 条,超出时在 data.truncated_sub_lists 标注并给出 next_cursor,"
|
||||
+ "再用 sub_list + sub_cursor 对该子列表继续翻页(见 data.sub_page_info),直到 has_more=false,否则结果不完整。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("outer_code", stringProperty("出库单号(与 order_code 二选一)"));
|
||||
properties.put("order_code", stringProperty("合同编号(与 outer_code 二选一;可能命中多条出库单)"));
|
||||
properties.put("include_detail", boolProperty("是否返回明细(出库明细/SN 明细/入库/发货/备货),默认 true"));
|
||||
McpSubPage.withProperties(properties, SUB_LIST_NAMES);
|
||||
return objectSchema(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
McpAuth.requireAnyPermission("inventory:outer:list", "inventory:outer:view",
|
||||
"inventory:delivery:list", "inventory:delivery:view");
|
||||
|
||||
// 局部游标翻页:只返回指定子列表的一页,不需要主入口参数
|
||||
if (McpSubPage.requested(params)) {
|
||||
return handleSubListPage(params);
|
||||
}
|
||||
|
||||
McpParams.requireAny(params, "outer_code", "order_code");
|
||||
String outerCode = McpParams.str(params, "outer_code");
|
||||
String orderCode = McpParams.str(params, "order_code");
|
||||
boolean includeDetail = McpParams.boolVal(params, "include_detail", true);
|
||||
|
||||
InventoryOuter outerQuery = new InventoryOuter();
|
||||
if (outerCode != null) {
|
||||
outerQuery.setOuterCodeList(Collections.singletonList(outerCode));
|
||||
} else {
|
||||
outerQuery.setOrderCodeList(Collections.singletonList(orderCode));
|
||||
}
|
||||
List<InventoryOuter> outers = safeList(inventoryOuterMapper.selectInventoryOuterList(outerQuery));
|
||||
|
||||
List<String> outerCodes = new ArrayList<>();
|
||||
for (InventoryOuter outer : outers) {
|
||||
String code = McpFormat.trimToNull(outer.getOuterCode());
|
||||
if (code != null && !outerCodes.contains(code)) {
|
||||
outerCodes.add(code);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<InventoryOuterDetail>> detailMap = includeDetail
|
||||
? groupDetailsByOuter(outerCodes) : Collections.emptyMap();
|
||||
Map<String, List<InventoryInfo>> snDetailMap = includeDetail
|
||||
? loadSnDetails(outers, outerCodes) : Collections.emptyMap();
|
||||
Map<String, OmsInventoryInner> innerMap = includeDetail
|
||||
? loadInnerMap(snDetailMap) : Collections.emptyMap();
|
||||
Map<String, List<InventoryDelivery>> deliveryMap = includeDetail
|
||||
? groupDeliveriesByOuter(outerCodes) : Collections.emptyMap();
|
||||
Map<Long, List<String>> deliverySnMap = includeDetail
|
||||
? loadDeliverySnMap(deliveryMap) : Collections.emptyMap();
|
||||
Map<String, List<OmsStockInfo>> stockMap = includeDetail
|
||||
? loadStockMap(outers, orderCode) : Collections.emptyMap();
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
List<Map<String, Object>> truncatedSubLists = new ArrayList<>();
|
||||
|
||||
for (InventoryOuter outer : outers) {
|
||||
String code = McpFormat.trimToNull(outer.getOuterCode());
|
||||
List<InventoryOuterDetail> details = sortById(includeDetail
|
||||
? detailMap.getOrDefault(code, Collections.emptyList()) : Collections.emptyList(),
|
||||
InventoryOuterDetail::getId);
|
||||
List<InventoryInfo> sns = sortById(includeDetail
|
||||
? snDetailMap.getOrDefault(code, Collections.emptyList()) : Collections.emptyList(),
|
||||
InventoryInfo::getId);
|
||||
List<InventoryDelivery> deliveries = sortById(includeDetail
|
||||
? deliveryMap.getOrDefault(code, Collections.emptyList()) : Collections.emptyList(),
|
||||
InventoryDelivery::getId);
|
||||
|
||||
List<InventoryInfo> snPage = sns;
|
||||
if (sns.size() > MAX_SUB_LIST) {
|
||||
snPage = new ArrayList<>(sns.subList(0, MAX_SUB_LIST));
|
||||
truncatedSubLists.add(McpSubPage.truncatedItem(getToolName(), "snDetails", code,
|
||||
MAX_SUB_LIST, sns.size()));
|
||||
}
|
||||
List<InventoryDelivery> deliveryPage = deliveries;
|
||||
if (deliveries.size() > MAX_SUB_LIST) {
|
||||
deliveryPage = new ArrayList<>(deliveries.subList(0, MAX_SUB_LIST));
|
||||
truncatedSubLists.add(McpSubPage.truncatedItem(getToolName(), "deliveries", code,
|
||||
MAX_SUB_LIST, deliveries.size()));
|
||||
}
|
||||
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("outerCode", code);
|
||||
item.put("orderCode", McpFormat.trim(outer.getOrderCode()));
|
||||
item.put("projectCode", outer.getProjectCode());
|
||||
item.put("projectName", outer.getProjectName());
|
||||
item.put("productCode", McpFormat.trim(outer.getProductCode()));
|
||||
item.put("model", outer.getModel());
|
||||
item.put("quantity", outer.getQuantity());
|
||||
item.put("outerStatus", outer.getOuterStatus());
|
||||
item.put("outerStatusName", outerStatusName(outer.getOuterStatus()));
|
||||
item.put("deliveryStatus", outer.getDeliveryStatus());
|
||||
item.put("deliveryStatusName", outerDeliveryStatusName(outer.getDeliveryStatus()));
|
||||
item.put("deliveryTime", McpFormat.date(outer.getDeliveryTime()));
|
||||
item.put("contactPerson", outer.getContactPerson());
|
||||
item.put("contactPhone", outer.getContactPhone());
|
||||
item.put("contactAddress", outer.getContactAddress());
|
||||
item.put("createByName", outer.getCreateByName());
|
||||
item.put("createTime", McpFormat.dateTime(outer.getCreateTime()));
|
||||
item.put("warehouseId", firstWarehouseId(details));
|
||||
item.put("outerDetails", toDetailItems(details));
|
||||
item.put("snDetails", toSnItems(snPage));
|
||||
item.put("inners", toInnerItems(snPage, innerMap));
|
||||
item.put("deliveries", toDeliveryItems(deliveryPage, deliverySnMap));
|
||||
item.put("stock", toStockItem(outer.getOrderCode(), orderCode, stockMap));
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
Map<String, Object> query = mapData(
|
||||
"outer_code", outerCode,
|
||||
"order_code", orderCode,
|
||||
"include_detail", includeDetail
|
||||
);
|
||||
|
||||
Map<String, Object> data = mapData(
|
||||
"total", items.size(),
|
||||
"items", items
|
||||
);
|
||||
if (!truncatedSubLists.isEmpty()) {
|
||||
data.put("truncated_sub_lists", truncatedSubLists);
|
||||
}
|
||||
return response(buildMetadata(), query, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 局部游标翻页:只加载并返回某一个出库单下的某个子列表的一页。
|
||||
*/
|
||||
private Object handleSubListPage(Map<String, Object> params) {
|
||||
String subList = McpParams.requireValue(params, "sub_list", SUB_LISTS);
|
||||
McpSubPage.SubCursor state = McpSubPage.resolve(params, getToolName(), subList, SUB_LIST_NAMES,
|
||||
subListSortKey(subList));
|
||||
String requestedParent = state.getParent() != null
|
||||
? state.getParent() : McpParams.requireStr(params, "sub_parent");
|
||||
int pageSize = McpSubPage.pageSize(params);
|
||||
|
||||
InventoryOuter outerQuery = new InventoryOuter();
|
||||
outerQuery.setOuterCodeList(Collections.singletonList(requestedParent));
|
||||
List<InventoryOuter> outers = safeList(inventoryOuterMapper.selectInventoryOuterList(outerQuery));
|
||||
if (outers.isEmpty()) {
|
||||
throw McpToolException.invalidParams("未找到出库单:" + requestedParent
|
||||
+ "(sub_parent 取值应取自 data.truncated_sub_lists[].parent 或该出库单的 outerCode)");
|
||||
}
|
||||
// 库内 collation 大小写不敏感,统一以实际查出的编码作为父实体标识,避免后续分组取不到
|
||||
String parent = McpFormat.trimToNull(outers.get(0).getOuterCode());
|
||||
if (parent == null) {
|
||||
throw McpToolException.serverError("出库单 " + requestedParent + " 的 outer_code 为空,无法翻转子列表");
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rows;
|
||||
if ("outerDetails".equals(subList)) {
|
||||
List<InventoryOuterDetail> details = sortById(
|
||||
groupDetailsByOuter(Collections.singletonList(parent)).getOrDefault(parent, Collections.emptyList()),
|
||||
InventoryOuterDetail::getId);
|
||||
rows = toDetailItems(details);
|
||||
} else if ("snDetails".equals(subList)) {
|
||||
List<InventoryInfo> sns = sortById(
|
||||
loadSnDetails(outers, Collections.singletonList(parent)).getOrDefault(parent, Collections.emptyList()),
|
||||
InventoryInfo::getId);
|
||||
rows = toSnItems(sns);
|
||||
} else {
|
||||
Map<String, List<InventoryDelivery>> deliveryMap = groupDeliveriesByOuter(Collections.singletonList(parent));
|
||||
List<InventoryDelivery> deliveries = sortById(
|
||||
deliveryMap.getOrDefault(parent, Collections.emptyList()), InventoryDelivery::getId);
|
||||
rows = toDeliveryItems(deliveries, loadDeliverySnMap(deliveryMap));
|
||||
}
|
||||
|
||||
McpSubPage.SubPageResult<Map<String, Object>> page = McpSubPage.page(state, parent, rows, pageSize);
|
||||
|
||||
Map<String, Object> query = mapData(
|
||||
"sub_list", subList,
|
||||
"sub_parent", parent,
|
||||
"page_size", pageSize
|
||||
);
|
||||
Map<String, Object> data = mapData(
|
||||
"sub_list", subList,
|
||||
"sub_parent", parent,
|
||||
"total", rows.size(),
|
||||
"items", page.getItems(),
|
||||
"sub_page_info", page.getPageInfo()
|
||||
);
|
||||
return response(buildSubListMetadata(subList), query, data);
|
||||
}
|
||||
|
||||
private String subListSortKey(String subList) {
|
||||
if ("outerDetails".equals(subList)) {
|
||||
return "oms_inventory_outer_detail.outer_code, id";
|
||||
}
|
||||
if ("snDetails".equals(subList)) {
|
||||
return "oms_inventory_info.id";
|
||||
}
|
||||
return "oms_inventory_delivery.id";
|
||||
}
|
||||
|
||||
private Map<String, List<InventoryOuterDetail>> groupDetailsByOuter(List<String> outerCodes) {
|
||||
Map<String, List<InventoryOuterDetail>> result = new LinkedHashMap<>();
|
||||
if (outerCodes.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<InventoryOuterDetail> rows = inventoryOuterDetailMapper.listByOuterCodeList(outerCodes);
|
||||
if (rows == null) {
|
||||
return result;
|
||||
}
|
||||
for (InventoryOuterDetail row : rows) {
|
||||
String code = McpFormat.trimToNull(row.getOuterCode());
|
||||
if (code != null) {
|
||||
result.computeIfAbsent(code, k -> new ArrayList<>()).add(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, List<InventoryInfo>> loadSnDetails(List<InventoryOuter> outers, List<String> outerCodes) {
|
||||
Map<String, List<InventoryInfo>> result = new LinkedHashMap<>();
|
||||
if (outerCodes.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<InventoryInfo> rows = inventoryInfoMapper.selectInventoryInfoByOuterCodeList(outerCodes);
|
||||
if (rows != null) {
|
||||
for (InventoryInfo row : rows) {
|
||||
String code = McpFormat.trimToNull(row.getOuterCode());
|
||||
if (code != null) {
|
||||
result.computeIfAbsent(code, k -> new ArrayList<>()).add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 撤回后 inventory_info.outer_code 会被清空 → 按发货记录(SN 明细)反查兜底
|
||||
if (outers.size() <= MAX_FALLBACK_OUTERS) {
|
||||
for (InventoryOuter outer : outers) {
|
||||
String code = McpFormat.trimToNull(outer.getOuterCode());
|
||||
if (code == null || result.containsKey(code)) {
|
||||
continue;
|
||||
}
|
||||
List<InventoryInfo> fallback = inventoryInfoMapper.listByOuterCodeViaDelivery(code);
|
||||
if (fallback != null && !fallback.isEmpty()) {
|
||||
result.put(code, new ArrayList<>(fallback));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, OmsInventoryInner> loadInnerMap(Map<String, List<InventoryInfo>> snDetailMap) {
|
||||
Map<String, OmsInventoryInner> result = new LinkedHashMap<>();
|
||||
Set<String> innerCodes = new LinkedHashSet<>();
|
||||
for (List<InventoryInfo> rows : snDetailMap.values()) {
|
||||
for (InventoryInfo row : rows) {
|
||||
String innerCode = McpFormat.trimToNull(row.getInnerCode());
|
||||
if (innerCode != null) {
|
||||
innerCodes.add(innerCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (innerCodes.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<OmsInventoryInner> inners = omsInventoryInnerMapper
|
||||
.selectOmsInventoryInnerByInnerCodeList(new ArrayList<>(innerCodes));
|
||||
if (inners != null) {
|
||||
for (OmsInventoryInner inner : inners) {
|
||||
String code = McpFormat.trimToNull(inner.getInnerCode());
|
||||
if (code != null) {
|
||||
result.putIfAbsent(code, inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, List<InventoryDelivery>> groupDeliveriesByOuter(List<String> outerCodes) {
|
||||
Map<String, List<InventoryDelivery>> result = new LinkedHashMap<>();
|
||||
if (outerCodes.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<InventoryDelivery> rows = inventoryDeliveryMapper.listByOuterCodeList(outerCodes);
|
||||
if (rows == null) {
|
||||
return result;
|
||||
}
|
||||
for (InventoryDelivery row : rows) {
|
||||
String code = McpFormat.trimToNull(row.getOuterCode());
|
||||
if (code != null) {
|
||||
result.computeIfAbsent(code, k -> new ArrayList<>()).add(row);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Long, List<String>> loadDeliverySnMap(Map<String, List<InventoryDelivery>> deliveryMap) {
|
||||
Map<Long, List<String>> result = new LinkedHashMap<>();
|
||||
List<Long> deliveryIds = new ArrayList<>();
|
||||
for (List<InventoryDelivery> rows : deliveryMap.values()) {
|
||||
for (InventoryDelivery row : rows) {
|
||||
if (row.getId() != null && !deliveryIds.contains(row.getId())) {
|
||||
deliveryIds.add(row.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deliveryIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<OmsInventoryDeliveryDetail> details = omsInventoryDeliveryDetailMapper.listByDeliveryIdList(deliveryIds);
|
||||
if (details != null) {
|
||||
for (OmsInventoryDeliveryDetail detail : details) {
|
||||
if (detail.getDeliveryId() == null) {
|
||||
continue;
|
||||
}
|
||||
result.computeIfAbsent(detail.getDeliveryId(), k -> new ArrayList<>())
|
||||
.add(McpFormat.trim(detail.getProductSn()));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, List<OmsStockInfo>> loadStockMap(List<InventoryOuter> outers, String orderCode) {
|
||||
Map<String, List<OmsStockInfo>> result = new LinkedHashMap<>();
|
||||
Set<String> orderCodes = new LinkedHashSet<>();
|
||||
if (orderCode != null) {
|
||||
orderCodes.add(orderCode);
|
||||
}
|
||||
for (InventoryOuter outer : outers) {
|
||||
String code = McpFormat.trimToNull(outer.getOrderCode());
|
||||
if (code != null) {
|
||||
orderCodes.add(code);
|
||||
}
|
||||
}
|
||||
for (String code : orderCodes) {
|
||||
OmsStockInfo condition = new OmsStockInfo();
|
||||
condition.setOrderCode(code);
|
||||
List<OmsStockInfo> rows = omsStockInfoMapper.queryAll(condition);
|
||||
if (rows != null && !rows.isEmpty()) {
|
||||
result.put(code, rows);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object firstWarehouseId(List<InventoryOuterDetail> details) {
|
||||
for (InventoryOuterDetail detail : details) {
|
||||
if (detail.getWarehouseId() != null) {
|
||||
return detail.getWarehouseId();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> toDetailItems(List<InventoryOuterDetail> details) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (InventoryOuterDetail detail : details) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("outerCode", McpFormat.trim(detail.getOuterCode()));
|
||||
item.put("warehouseId", detail.getWarehouseId());
|
||||
item.put("warehouseName", detail.getWarehouseName());
|
||||
item.put("quantity", detail.getQuantity());
|
||||
item.put("outerStatus", detail.getOuterStatus());
|
||||
item.put("productCode", McpFormat.trim(detail.getProductCode()));
|
||||
item.put("model", detail.getModel());
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> toSnItems(List<InventoryInfo> rows) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (InventoryInfo row : rows) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("productSn", McpFormat.trim(row.getProductSn()));
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("model", row.getModel());
|
||||
item.put("inventoryStatus", row.getInventoryStatus());
|
||||
item.put("inventoryStatusName", inventoryStatusName(row.getInventoryStatus()));
|
||||
item.put("innerCode", McpFormat.trim(row.getInnerCode()));
|
||||
item.put("outerCode", McpFormat.trim(row.getOuterCode()));
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("warehouseId", row.getWarehouseId());
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("innerPrice", row.getInnerPrice());
|
||||
item.put("outerPrice", row.getOuterPrice());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> toInnerItems(List<InventoryInfo> snRows, Map<String, OmsInventoryInner> innerMap) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
for (InventoryInfo row : snRows) {
|
||||
String innerCode = McpFormat.trimToNull(row.getInnerCode());
|
||||
if (innerCode == null || !seen.add(innerCode)) {
|
||||
continue;
|
||||
}
|
||||
OmsInventoryInner inner = innerMap.get(innerCode);
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("innerCode", innerCode);
|
||||
item.put("purchaseNo", inner == null ? null : McpFormat.trim(inner.getPurchaseNo()));
|
||||
item.put("vendorName", inner == null ? null : inner.getVendorName());
|
||||
item.put("productCode", inner == null ? null : McpFormat.trim(inner.getProductCode()));
|
||||
item.put("quantity", inner == null ? null : inner.getQuantity());
|
||||
item.put("createTime", inner == null ? null : McpFormat.dateTime(inner.getCreateTime()));
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> toDeliveryItems(List<InventoryDelivery> rows, Map<Long, List<String>> deliverySnMap) {
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (InventoryDelivery row : rows) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("deliveryId", row.getId());
|
||||
item.put("outerCode", McpFormat.trim(row.getOuterCode()));
|
||||
item.put("warehouseId", row.getWarehouseId());
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("logisticsCompany", row.getLogisticsCompany());
|
||||
item.put("logisticsCode", row.getLogisticsCode());
|
||||
item.put("deliveryType", row.getDeliveryType());
|
||||
item.put("deliveryTypeName", deliveryTypeName(row.getDeliveryType()));
|
||||
item.put("deliveryStatus", row.getDeliveryStatus());
|
||||
item.put("deliveryStatusName", deliveryStatusName(row.getDeliveryStatus()));
|
||||
item.put("deliveryTime", McpFormat.date(row.getDeliveryTime()));
|
||||
item.put("receiverAddress", row.getNotifierAddress());
|
||||
item.put("productSns", deliverySnMap.getOrDefault(row.getId(), Collections.emptyList()));
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private Object toStockItem(String outerOrderCode, String queryOrderCode,
|
||||
Map<String, List<OmsStockInfo>> stockMap) {
|
||||
String orderCode = McpFormat.trimToNull(queryOrderCode) != null
|
||||
? McpFormat.trimToNull(queryOrderCode) : McpFormat.trimToNull(outerOrderCode);
|
||||
List<OmsStockInfo> rows = orderCode == null
|
||||
? Collections.emptyList() : stockMap.getOrDefault(orderCode, Collections.emptyList());
|
||||
if (rows.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
OmsStockInfo stock = rows.get(0);
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("orderCode", McpFormat.trim(stock.getOrderCode()));
|
||||
item.put("stockStatus", stock.getStockStatus());
|
||||
item.put("stockStatusName", stockStatusName(stock.getStockStatus()));
|
||||
item.put("onceInStock", stock.getOnceInStock());
|
||||
item.put("allQuantity", stock.getAllQuantity());
|
||||
item.put("deliveryTime", McpFormat.date(stock.getDeliveryTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private String outerStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryOuter.OuterStatusEnum value : InventoryOuter.OuterStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String outerDeliveryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryOuter.DeliveryStatusEnum value : InventoryOuter.DeliveryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String deliveryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryDelivery.DeliveryStatusEnum value : InventoryDelivery.DeliveryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String deliveryTypeName(String type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
if ("1".equals(type)) {
|
||||
return "快递";
|
||||
}
|
||||
if ("2".equals(type)) {
|
||||
return "物流";
|
||||
}
|
||||
if ("3".equals(type)) {
|
||||
return "自提";
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private String stockStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
if ("0".equals(status)) {
|
||||
return "未备货";
|
||||
}
|
||||
if ("1".equals(status)) {
|
||||
return "已备货";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String inventoryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryInfo.InventoryStatusEnum value : InventoryInfo.InventoryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private <T> List<T> safeList(List<T> list) {
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键 id 升序稳定排序:保证主调用的截断点与局部游标的续页起点严格衔接(部分子表 SQL 无 order by)。
|
||||
*/
|
||||
private static <T> List<T> sortById(List<T> rows, java.util.function.Function<T, Long> idGetter) {
|
||||
if (rows == null || rows.size() <= 1) {
|
||||
return rows == null ? Collections.emptyList() : rows;
|
||||
}
|
||||
List<T> copy = new ArrayList<>(rows);
|
||||
copy.sort(java.util.Comparator.comparing(idGetter,
|
||||
java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder())));
|
||||
return copy;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata() {
|
||||
Map<String, Object> metadata = metadata("按出库单号/合同编号点查完整单据流转链(不分页,子列表有上界)");
|
||||
metadata.put("query_fields", mapData(
|
||||
"outer_code", "出库单号(与 order_code 二选一)",
|
||||
"order_code", "合同编号(与 outer_code 二选一)",
|
||||
"include_detail", "是否返回明细,默认 true",
|
||||
"sub_list", "可选:子列表名(" + SUB_LIST_NAMES + "),传该参数表示只翻该子列表的一页",
|
||||
"sub_parent", "可选:父实体标识(出库单号),首次翻转子列表时必填",
|
||||
"sub_cursor", "可选:局部游标,原样回传 data.sub_page_info.next_cursor;与 cursor 互斥",
|
||||
"page_size", "可选:子列表每页条数,默认 " + McpSubPage.DEFAULT_PAGE_SIZE + ",上限 " + McpSubPage.MAX_PAGE_SIZE
|
||||
));
|
||||
metadata.put("data_fields", mapData(
|
||||
"total", "命中的出库单数",
|
||||
"items", "每个出库单一条流转链",
|
||||
"truncated_sub_lists", "被子列表上限(" + MAX_SUB_LIST + ")截断的条目:{list, parent, total, returned, next_cursor};"
|
||||
+ "用 sub_list + next_cursor 作为 sub_cursor 继续翻页,直到 has_more=false,否则数据不完整"
|
||||
));
|
||||
metadata.put("item_fields", mapData(
|
||||
"outerCode", "出库单号",
|
||||
"orderCode", "合同编号",
|
||||
"projectCode", "项目编号",
|
||||
"projectName", "项目名称",
|
||||
"productCode", "产品BOM编码",
|
||||
"model", "产品型号",
|
||||
"quantity", "应发数量",
|
||||
"outerStatus", "出库状态编码",
|
||||
"outerStatusName", "出库状态名称(1=待确认,2=已确认,3=已接收,4=已退回)",
|
||||
"deliveryStatus", "发货状态编码",
|
||||
"deliveryStatusName", "发货状态名称(0=未发货,1=部分发货,2=全部发货,3=已撤回)",
|
||||
"deliveryTime", "发货时间",
|
||||
"contactPerson", "联系人",
|
||||
"contactPhone", "联系电话",
|
||||
"contactAddress", "联系地址",
|
||||
"createByName", "创建人",
|
||||
"createTime", "创建时间",
|
||||
"warehouseId", "仓库ID(取自出库明细)",
|
||||
"outerDetails", "出库明细(仓库拆分)",
|
||||
"outerDetails[].warehouseId", "仓库ID",
|
||||
"outerDetails[].warehouseName", "仓库名称",
|
||||
"outerDetails[].quantity", "出库数量",
|
||||
"outerDetails[].outerStatus", "出库状态",
|
||||
"snDetails", "该出库单下的 SN 明细",
|
||||
"snDetails[].productSn", "产品序列号",
|
||||
"snDetails[].inventoryStatus", "库存状态(0=入库,1=出库)",
|
||||
"snDetails[].innerCode", "入库单号",
|
||||
"snDetails[].innerPrice", "入库价(是否含税待确认)",
|
||||
"snDetails[].outerPrice", "出库价(是否含税待确认)",
|
||||
"inners", "入库信息",
|
||||
"inners[].innerCode", "入库单号",
|
||||
"inners[].purchaseNo", "采购单号",
|
||||
"inners[].vendorName", "厂商名称",
|
||||
"inners[].createTime", "入库时间",
|
||||
"deliveries", "发货单列表",
|
||||
"deliveries[].deliveryId", "发货单ID",
|
||||
"deliveries[].logisticsCompany", "物流公司",
|
||||
"deliveries[].logisticsCode", "物流单号",
|
||||
"deliveries[].deliveryStatus", "发货状态编码",
|
||||
"deliveries[].deliveryStatusName", "发货状态名称(0=待发货,1=已发货,2=撤回)",
|
||||
"deliveries[].deliveryTime", "发货时间",
|
||||
"deliveries[].receiverAddress", "收货地址",
|
||||
"deliveries[].productSns", "发货 SN 明细",
|
||||
"stock", "备货状态",
|
||||
"stock.stockStatus", "备货状态编码(0=未备货,1=已备货)",
|
||||
"stock.stockStatusName", "备货状态名称",
|
||||
"stock.onceInStock", "是否一次备齐",
|
||||
"stock.allQuantity", "应备货总量",
|
||||
"stock.deliveryTime", "要求到货时间"
|
||||
));
|
||||
metadata.put("dict_fields", mapData(
|
||||
"outerStatus", "InventoryOuter.OuterStatusEnum",
|
||||
"deliveryStatus(outer)", "InventoryOuter.DeliveryStatusEnum",
|
||||
"deliveryStatus(delivery)", "InventoryDelivery.DeliveryStatusEnum",
|
||||
"stock.stockStatus", "OmsStockInfo(0=未备货,1=已备货)"
|
||||
));
|
||||
metadata.put("notes", "撤回后 inventory_info.outer_code 会被清空,故 SN 明细优先按 outer_code 查询,"
|
||||
+ "查不到时再按发货记录(oms_inventory_delivery + _detail)反查兜底(listByOuterCodeViaDelivery)。"
|
||||
+ "子列表(outerDetails / snDetails / deliveries)单次最多 " + MAX_SUB_LIST + " 条,"
|
||||
+ "超出时用 data.truncated_sub_lists[].next_cursor 配合 sub_list 继续翻页(局部游标,最多 "
|
||||
+ McpSubPage.MAX_PAGES + " 页),不要让结果停留在截断状态。");
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSubListMetadata(String subList) {
|
||||
Map<String, Object> metadata = metadata("inventory_flow 的局部游标翻页:只返回某一个出库单下 " + subList + " 子列表的一页");
|
||||
metadata.put("query_fields", mapData(
|
||||
"sub_list", "要翻页的子列表名,取值 " + SUB_LIST_NAMES,
|
||||
"sub_parent", "父实体标识(出库单号 outer_code),首次翻页必填",
|
||||
"sub_cursor", "局部游标,原样回传上次的 data.sub_page_info.next_cursor;已携带 sub_parent,与 cursor 互斥",
|
||||
"page_size", "每页条数,默认 " + McpSubPage.DEFAULT_PAGE_SIZE + ",上限 " + McpSubPage.MAX_PAGE_SIZE
|
||||
));
|
||||
metadata.put("data_fields", mapData(
|
||||
"sub_list", "本次翻页的子列表名",
|
||||
"sub_parent", "父实体标识(出库单号)",
|
||||
"total", "★该子列表的总条数(不是出库单数)",
|
||||
"items", "本页数据,字段语义与主调用的 items[].<sub_list>[] 完全一致",
|
||||
"sub_page_info", "分页信封,结构同 page_info:returned / page_size / has_more / next_cursor / sort_by / page_no / total_count"
|
||||
));
|
||||
metadata.put("item_fields", subListItemFields(subList));
|
||||
metadata.put("notes", "本工具不分页但有界:子列表在主调用中最多返回 " + MAX_SUB_LIST + " 条,"
|
||||
+ "超出时 data.truncated_sub_lists 会给出 {list, parent, total, returned, next_cursor},"
|
||||
+ "用 sub_list + 该 next_cursor 作为 sub_cursor 继续翻页,直到 sub_page_info.has_more=false,否则数据不完整。"
|
||||
+ "首次翻页(无 sub_cursor)需同时传 sub_parent。局部游标最多翻 " + McpSubPage.MAX_PAGES + " 页。");
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> subListItemFields(String subList) {
|
||||
if ("outerDetails".equals(subList)) {
|
||||
return mapData(
|
||||
"outerCode", "出库单号",
|
||||
"warehouseId", "仓库ID",
|
||||
"warehouseName", "仓库名称",
|
||||
"quantity", "出库数量",
|
||||
"outerStatus", "出库状态",
|
||||
"productCode", "产品BOM编码",
|
||||
"model", "产品型号"
|
||||
);
|
||||
}
|
||||
if ("snDetails".equals(subList)) {
|
||||
return mapData(
|
||||
"id", "SN 记录主键(排序键)",
|
||||
"productSn", "产品序列号",
|
||||
"productCode", "产品BOM编码",
|
||||
"model", "产品型号",
|
||||
"inventoryStatus", "库存状态编码(0=入库,1=出库)",
|
||||
"inventoryStatusName", "库存状态名称",
|
||||
"innerCode", "入库单号",
|
||||
"outerCode", "出库单号",
|
||||
"orderCode", "合同编号",
|
||||
"warehouseId", "仓库ID",
|
||||
"warehouseName", "仓库名称",
|
||||
"innerPrice", "入库价(是否含税待确认)",
|
||||
"outerPrice", "出库价(是否含税待确认)",
|
||||
"taxRate", "税率"
|
||||
);
|
||||
}
|
||||
return mapData(
|
||||
"deliveryId", "发货单ID(排序键)",
|
||||
"outerCode", "出库单号",
|
||||
"warehouseId", "仓库ID",
|
||||
"warehouseName", "仓库名称",
|
||||
"logisticsCompany", "物流公司",
|
||||
"logisticsCode", "物流单号",
|
||||
"deliveryType", "发货方式编码(1=快递,2=物流,3=自提)",
|
||||
"deliveryTypeName", "发货方式名称",
|
||||
"deliveryStatus", "发货状态编码(0=待发货,1=已发货,2=撤回)",
|
||||
"deliveryStatusName", "发货状态名称",
|
||||
"deliveryTime", "发货时间",
|
||||
"receiverAddress", "收货地址",
|
||||
"productSns", "该发货单的 SN 明细"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.domain.InventoryInfo;
|
||||
import com.ruoyi.sip.domain.OmsInventoryInner;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.InventoryDeliveryMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryInfoMapper;
|
||||
import com.ruoyi.sip.mapper.OmsInventoryInnerMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MCP 工具 inventory_sn_trace(标识符点查,不分页,有界)。
|
||||
* <p>权限来源:InventoryInfoController({@code @RequiresPermissions("inventory:info:view")})。</p>
|
||||
*/
|
||||
@Component
|
||||
public class InventorySnTraceToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final int MAX_SN = 50;
|
||||
private static final int MAX_CODE = 20;
|
||||
private static final int DEFAULT_LIMIT = 50;
|
||||
private static final int MAX_LIMIT = 200;
|
||||
|
||||
@Autowired
|
||||
private InventoryInfoMapper inventoryInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsInventoryInnerMapper omsInventoryInnerMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryDeliveryMapper inventoryDeliveryMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "inventory_sn_trace";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "按产品序列号/入库单号/出库单号/合同编号点查库存明细(SN 级):库存状态、入库价/出库价、税率、所属入库单/出库单/合同号、仓库。"
|
||||
+ "本工具不分页但结果有上界(limit,默认 50、上限 200);若返回 truncated=true 请收窄查询范围。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("product_sn_list", arrayProperty("产品序列号/条码列表,最多 " + MAX_SN
|
||||
+ " 个;命中唯一索引 unq_idx_sn,最优先的入口"));
|
||||
properties.put("inner_code_list", arrayProperty("入库单号列表,最多 " + MAX_CODE + " 个"));
|
||||
properties.put("outer_code_list", arrayProperty("出库单号列表,最多 " + MAX_CODE + " 个"));
|
||||
properties.put("order_code", stringProperty("合同编号(仅能查到已出库的 SN;按订单查在库货请改用 inner_code_list)"));
|
||||
properties.put("limit", intProperty("返回条数上限,默认 " + DEFAULT_LIMIT + ",上限 " + MAX_LIMIT));
|
||||
return objectSchema(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
McpAuth.requireAnyPermission("inventory:info:view");
|
||||
|
||||
McpParams.requireAny(params, "product_sn_list", "inner_code_list", "outer_code_list", "order_code");
|
||||
List<String> productSns = McpParams.codeList(params, "product_sn_list", MAX_SN);
|
||||
List<String> innerCodes = McpParams.codeList(params, "inner_code_list", MAX_CODE);
|
||||
List<String> outerCodes = McpParams.codeList(params, "outer_code_list", MAX_CODE);
|
||||
String orderCode = McpParams.str(params, "order_code");
|
||||
int limit = McpParams.intVal(params, "limit", DEFAULT_LIMIT);
|
||||
if (limit <= 0 || limit > MAX_LIMIT) {
|
||||
throw McpToolException.invalidParams("limit 必须在 1 ~ " + MAX_LIMIT + " 之间,当前值:" + limit);
|
||||
}
|
||||
|
||||
// 多入口取并集(按 id 去重,保持首次出现顺序)
|
||||
// 注意:未提供的入口必须跳过查询,否则 `where col in ()` 是非法 SQL(MySQL 语法错误)。
|
||||
Map<Long, InventoryInfo> merged = new LinkedHashMap<>();
|
||||
if (!productSns.isEmpty()) {
|
||||
merge(merged, inventoryInfoMapper.listSnTraceByProductSnList(productSns, limit));
|
||||
}
|
||||
if (!innerCodes.isEmpty()) {
|
||||
merge(merged, inventoryInfoMapper.listSnTraceByInnerCodeList(innerCodes, limit));
|
||||
}
|
||||
if (!outerCodes.isEmpty()) {
|
||||
merge(merged, inventoryInfoMapper.listSnTraceByOuterCodeList(outerCodes, limit));
|
||||
}
|
||||
if (orderCode != null) {
|
||||
merge(merged, inventoryInfoMapper.listSnTraceByOrderCode(Collections.singletonList(orderCode), limit));
|
||||
}
|
||||
|
||||
List<InventoryInfo> rows = new ArrayList<>(merged.values());
|
||||
boolean truncated = false;
|
||||
if (rows.size() > limit) {
|
||||
rows = new ArrayList<>(rows.subList(0, limit));
|
||||
truncated = true;
|
||||
}
|
||||
|
||||
// 采购单号:purchase_no 不是 oms_inventory_info 的列(见方案 2.3/2.4),
|
||||
// 需按 SN 的 inner_code 关联 oms_inventory_inner.purchase_no 批量补齐(一次查询,非逐行)。
|
||||
Map<String, String> purchaseNoMap = loadPurchaseNoMap(rows);
|
||||
|
||||
// 发货/财务审批信息(可选键:为空则不输出)
|
||||
List<String> snValues = new ArrayList<>();
|
||||
for (InventoryInfo row : rows) {
|
||||
String sn = McpFormat.trimToNull(row.getProductSn());
|
||||
if (sn != null) {
|
||||
snValues.add(sn);
|
||||
}
|
||||
}
|
||||
List<Integer> approveStatusList = snValues.isEmpty()
|
||||
? Collections.emptyList()
|
||||
: safeList(inventoryDeliveryMapper.selectApproveStatusBySn(snValues));
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (InventoryInfo row : rows) {
|
||||
items.add(toItem(row, purchaseNoMap, approveStatusList));
|
||||
}
|
||||
|
||||
Map<String, Object> query = mapData(
|
||||
"product_sn_list", productSns,
|
||||
"inner_code_list", innerCodes,
|
||||
"outer_code_list", outerCodes,
|
||||
"order_code", orderCode,
|
||||
"limit", limit
|
||||
);
|
||||
|
||||
Map<String, Object> metadata = buildMetadata();
|
||||
Map<String, Object> data = mapData(
|
||||
"total", items.size(),
|
||||
"items", items,
|
||||
"truncated", truncated
|
||||
);
|
||||
return response(metadata, query, data);
|
||||
}
|
||||
|
||||
private void merge(Map<Long, InventoryInfo> target, List<InventoryInfo> rows) {
|
||||
if (rows == null) {
|
||||
return;
|
||||
}
|
||||
for (InventoryInfo row : rows) {
|
||||
if (row == null || row.getId() == null) {
|
||||
continue;
|
||||
}
|
||||
target.putIfAbsent(row.getId(), row);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> List<T> safeList(List<T> list) {
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
|
||||
private Map<String, String> loadPurchaseNoMap(List<InventoryInfo> rows) {
|
||||
Set<String> innerCodes = new LinkedHashSet<>();
|
||||
for (InventoryInfo row : rows) {
|
||||
String innerCode = McpFormat.trimToNull(row.getInnerCode());
|
||||
if (innerCode != null) {
|
||||
innerCodes.add(innerCode);
|
||||
}
|
||||
}
|
||||
if (innerCodes.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<OmsInventoryInner> inners = omsInventoryInnerMapper
|
||||
.selectOmsInventoryInnerByInnerCodeList(new ArrayList<>(innerCodes));
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
if (inners != null) {
|
||||
for (OmsInventoryInner inner : inners) {
|
||||
if (inner == null) {
|
||||
continue;
|
||||
}
|
||||
String innerCode = McpFormat.trimToNull(inner.getInnerCode());
|
||||
if (innerCode != null) {
|
||||
result.putIfAbsent(innerCode, McpFormat.trimToNull(inner.getPurchaseNo()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> toItem(InventoryInfo row,
|
||||
Map<String, String> purchaseNoMap,
|
||||
List<Integer> approveStatusList) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("productSn", McpFormat.trim(row.getProductSn()));
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("model", row.getModel());
|
||||
item.put("productDesc", row.getProductDesc());
|
||||
item.put("inventoryStatus", row.getInventoryStatus());
|
||||
item.put("inventoryStatusName", inventoryStatusName(row.getInventoryStatus()));
|
||||
item.put("innerCode", McpFormat.trim(row.getInnerCode()));
|
||||
item.put("outerCode", McpFormat.trim(row.getOuterCode()));
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("purchaseNo", purchaseNoMap.get(McpFormat.trim(row.getInnerCode())));
|
||||
item.put("warehouseId", row.getWarehouseId());
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("innerPrice", row.getInnerPrice());
|
||||
item.put("outerPrice", row.getOuterPrice());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
item.put("payableBillCode", McpFormat.trim(row.getPayableBillCode()));
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
item.put("updateTime", McpFormat.dateTime(row.getUpdateTime()));
|
||||
if (!approveStatusList.isEmpty()) {
|
||||
item.put("deliveryInfo", mapData("approveStatusList", approveStatusList));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private String inventoryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryInfo.InventoryStatusEnum value : InventoryInfo.InventoryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata() {
|
||||
Map<String, Object> metadata = metadata("按产品序列号/入库单号/出库单号/合同编号点查库存明细(不分页,有上界)");
|
||||
metadata.put("query_fields", mapData(
|
||||
"product_sn_list", "产品序列号/条码列表,≤" + MAX_SN,
|
||||
"inner_code_list", "入库单号列表,≤" + MAX_CODE,
|
||||
"outer_code_list", "出库单号列表,≤" + MAX_CODE,
|
||||
"order_code", "合同编号",
|
||||
"limit", "返回上限,默认 " + DEFAULT_LIMIT + " / 上限 " + MAX_LIMIT
|
||||
));
|
||||
metadata.put("data_fields", mapData(
|
||||
"total", "返回条数",
|
||||
"items", "库存明细列表",
|
||||
"truncated", "是否因 limit 截断(true 时请收窄查询范围)"
|
||||
));
|
||||
metadata.put("item_fields", mapData(
|
||||
"id", "库存明细主键",
|
||||
"productSn", "产品序列号/条码",
|
||||
"productCode", "产品BOM编码",
|
||||
"model", "产品型号",
|
||||
"productDesc", "产品描述",
|
||||
"inventoryStatus", "库存状态编码",
|
||||
"inventoryStatusName", "库存状态名称(0=入库,1=出库)",
|
||||
"innerCode", "入库单号",
|
||||
"outerCode", "出库单号",
|
||||
"orderCode", "合同编号(SN 未出库时为空)",
|
||||
"purchaseNo", "采购单号(由 inner_code 关联 oms_inventory_inner 得到)",
|
||||
"warehouseId", "仓库ID",
|
||||
"warehouseName", "仓库名称",
|
||||
"innerPrice", "入库价(是否含税待业务确认)",
|
||||
"outerPrice", "出库价(是否含税待业务确认)",
|
||||
"taxRate", "税率(实测存在 NULL,原值输出,未做未税/含税换算)",
|
||||
"payableBillCode", "对应应付单号",
|
||||
"createTime", "创建时间",
|
||||
"updateTime", "更新时间",
|
||||
"deliveryInfo", "该 SN 集合关联的财务单据审批状态(approveStatusList:1=审批中,2=审批通过);为空时不输出该键"
|
||||
));
|
||||
metadata.put("dict_fields", mapData(
|
||||
"inventoryStatus", "InventoryInfo.InventoryStatusEnum(0=入库,1=出库)"
|
||||
));
|
||||
metadata.put("aggregation_rule", "1) orderCode 在 SN 未出库时为空,按订单查在库货请改用 inner_code;"
|
||||
+ "2) innerPrice/outerPrice 是否含税未经业务确认,不做含税/未税换算;"
|
||||
+ "3) inventoryStatus 取值 0=入库、1=出库;"
|
||||
+ "4) 编码匹配不区分大小写(utf8mb4_unicode_ci)。");
|
||||
metadata.put("data_quality", "编码类字段入参/出参统一 trim;tax_rate 实测存在 NULL,原值输出。");
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.domain.InventoryInfo;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpCursor;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpPages;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.InventoryInfoMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MCP 工具 inventory_stock_aggregate(聚合,默认 mode=SUMMARY)。
|
||||
* <p>权限来源:InventoryInfoController({@code @RequiresPermissions("inventory:info:view")})。</p>
|
||||
*/
|
||||
@Component
|
||||
public class InventoryStockAggregateToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final Set<String> MODES = new HashSet<>(Arrays.asList("SUMMARY", "LIST"));
|
||||
private static final Set<String> GROUP_BY = new HashSet<>(Arrays.asList("NONE", "PRODUCT", "WAREHOUSE", "STATUS"));
|
||||
/** LIST 模式允许的 group_by:其余维度缺少索引支撑(方案 15.2),只能走 SUMMARY */
|
||||
private static final Set<String> LIST_GROUP_BY = new HashSet<>(Arrays.asList("NONE", "PRODUCT"));
|
||||
|
||||
private static final int MAX_PRODUCT_CODE = 20;
|
||||
private static final int DEFAULT_TOP_N = 10;
|
||||
private static final int MAX_TOP_N = 100;
|
||||
private static final long COUNT_CAP = 50000L;
|
||||
|
||||
@Autowired
|
||||
private InventoryInfoMapper inventoryInfoMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "inventory_stock_aggregate";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "库存汇总统计:按产品/仓库/状态/整体聚合在库数量、已出库数量、总数量、入库价合计、出库价合计。"
|
||||
+ "默认 mode=SUMMARY(一次算完全量,按数量倒序取 top_n,不翻页);mode=LIST 按产品分页(游标键 product_code,每条产品嵌套仓库拆分)。"
|
||||
+ "分页查询时:若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 再次调用本工具,重复直到 has_more 为 false,"
|
||||
+ "否则结果不完整;不要用其它参数代替 cursor。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("mode", enumProperty("聚合模式,默认 SUMMARY(一次算完,不分页)/ LIST(按产品游标分页)", "SUMMARY", "LIST"));
|
||||
properties.put("group_by", enumProperty("分组的维度:NONE=整体一行(默认)/ PRODUCT=按产品 / WAREHOUSE=按仓库 / STATUS=按库存状态",
|
||||
"NONE", "PRODUCT", "WAREHOUSE", "STATUS"));
|
||||
properties.put("product_code_list", arrayProperty("产品编码列表,最多 " + MAX_PRODUCT_CODE
|
||||
+ " 个;用于收窄范围;mode=LIST 时必填"));
|
||||
properties.put("warehouse_id", intProperty("仓库ID(可选)"));
|
||||
properties.put("top_n", intProperty("SUMMARY 模式下返回的分组数上限,默认 " + DEFAULT_TOP_N + "、上限 " + MAX_TOP_N));
|
||||
properties.put("include_zero", boolProperty("是否包含全部度量均为 0/null 的分组,默认 false"));
|
||||
properties.put("include_total", boolProperty("LIST 模式是否统计产品分组总数,默认 false(受 count_cap=" + COUNT_CAP + " 限制)"));
|
||||
withPagingProperties(properties, McpPages.MAX_AGG_PAGE_SIZE, McpPages.DEFAULT_PAGE_SIZE);
|
||||
return objectSchema(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
McpAuth.requireAnyPermission("inventory:info:view");
|
||||
|
||||
String mode = McpParams.enumVal(params, "mode", MODES, "SUMMARY");
|
||||
String groupBy = McpParams.enumVal(params, "group_by", GROUP_BY, "NONE");
|
||||
List<String> productCodes = McpParams.codeList(params, "product_code_list", MAX_PRODUCT_CODE);
|
||||
Long warehouseId = longVal(params, "warehouse_id");
|
||||
boolean includeZero = McpParams.boolVal(params, "include_zero", false);
|
||||
boolean includeTotal = McpParams.boolVal(params, "include_total", false);
|
||||
|
||||
McpParams.reject("SUMMARY".equals(mode) && (McpParams.has(params, "cursor") || McpParams.has(params, "page_size")),
|
||||
"mode=SUMMARY 不分页,请勿传 cursor / page_size;需要分页请使用 mode=LIST");
|
||||
McpParams.reject("LIST".equals(mode) && McpParams.has(params, "top_n"),
|
||||
"top_n 仅在 mode=SUMMARY 下可用,请移除该参数或改用 mode=SUMMARY");
|
||||
if ("LIST".equals(mode)) {
|
||||
McpParams.reject(productCodes.isEmpty(),
|
||||
"mode=LIST 必须提供 product_code_list(最多 " + MAX_PRODUCT_CODE + " 个),以避免大表全量分页");
|
||||
McpParams.reject(!LIST_GROUP_BY.contains(groupBy),
|
||||
"group_by=" + groupBy + " 在 mode=LIST 下缺少索引支撑,请改用 mode=SUMMARY");
|
||||
}
|
||||
|
||||
if ("SUMMARY".equals(mode)) {
|
||||
return handleSummary(params, groupBy, productCodes, warehouseId, includeZero);
|
||||
}
|
||||
return handleList(params, groupBy, productCodes, warehouseId, includeZero, includeTotal);
|
||||
}
|
||||
|
||||
private Object handleSummary(Map<String, Object> params,
|
||||
String groupBy,
|
||||
List<String> productCodes,
|
||||
Long warehouseId,
|
||||
boolean includeZero) {
|
||||
int topN = McpParams.intVal(params, "top_n", DEFAULT_TOP_N);
|
||||
if (topN <= 0 || topN > MAX_TOP_N) {
|
||||
throw McpToolException.invalidParams("top_n 必须在 1 ~ " + MAX_TOP_N + " 之间,当前值:" + topN);
|
||||
}
|
||||
List<Map<String, Object>> rows = inventoryInfoMapper.selectInventoryStockSummary(
|
||||
groupBy, productCodes.isEmpty() ? null : productCodes, warehouseId, includeZero, topN);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
if (rows != null) {
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
if (!includeZero && isAllZero(row)) {
|
||||
continue;
|
||||
}
|
||||
items.add(toSummaryItem(groupBy, row));
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> query = mapData(
|
||||
"mode", "SUMMARY",
|
||||
"group_by", groupBy,
|
||||
"product_code_list", productCodes,
|
||||
"warehouse_id", warehouseId,
|
||||
"top_n", topN,
|
||||
"include_zero", includeZero
|
||||
);
|
||||
Map<String, Object> data = mapData(
|
||||
"total", items.size(),
|
||||
"items", items
|
||||
);
|
||||
return response(buildMetadata("SUMMARY"), query, data);
|
||||
}
|
||||
|
||||
private Object handleList(Map<String, Object> params,
|
||||
String groupBy,
|
||||
List<String> productCodes,
|
||||
Long warehouseId,
|
||||
boolean includeZero,
|
||||
boolean includeTotal) {
|
||||
int pageSize = McpPages.pageSize(params, McpPages.DEFAULT_PAGE_SIZE, McpPages.MAX_AGG_PAGE_SIZE);
|
||||
String filterHash = McpParams.filterHash(getToolName(), "LIST", groupBy, String.join(",", productCodes),
|
||||
warehouseId == null ? "" : String.valueOf(warehouseId), String.valueOf(includeZero));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), filterHash, McpPages.DEFAULT_AGG_MAX_PAGES);
|
||||
int pageNo = cursor == null ? 1 : cursor.getP();
|
||||
String cursorKey = cursor == null ? null : cursor.key(0);
|
||||
|
||||
List<Map<String, Object>> rows = inventoryInfoMapper.selectStockGroupPage(
|
||||
productCodes, warehouseId, cursorKey, includeZero, pageSize + 1);
|
||||
boolean hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<Map<String, Object>> page = McpPages.trim(rows, pageSize);
|
||||
|
||||
// 先取本页产品码,再批量取仓库拆分(禁止逐行查询)
|
||||
List<String> pageProductCodes = new ArrayList<>();
|
||||
for (Map<String, Object> row : page) {
|
||||
String code = str(row.get("productCode"));
|
||||
if (code != null) {
|
||||
pageProductCodes.add(code);
|
||||
}
|
||||
}
|
||||
Map<String, List<Map<String, Object>>> warehouseMap = loadWarehouseBreakdown(pageProductCodes, warehouseId);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
String lastProductCode = null;
|
||||
for (Map<String, Object> row : page) {
|
||||
String code = str(row.get("productCode"));
|
||||
lastProductCode = code;
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("productCode", code);
|
||||
putMetrics(item, row);
|
||||
item.put("warehouses", warehouseMap.getOrDefault(code, Collections.emptyList()));
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
Long total = null;
|
||||
boolean totalCapped = false;
|
||||
if (includeTotal) {
|
||||
long counted = inventoryInfoMapper.countStockGroups(productCodes, warehouseId);
|
||||
totalCapped = counted > COUNT_CAP;
|
||||
total = totalCapped ? COUNT_CAP : counted;
|
||||
}
|
||||
|
||||
String nextCursor = hasMore
|
||||
? McpCursor.encode(getToolName(), filterHash, pageNo + 1, keysOf(lastProductCode))
|
||||
: null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), pageSize, hasMore, nextCursor,
|
||||
"product_code", pageNo, total, totalCapped, false);
|
||||
|
||||
Map<String, Object> query = mapData(
|
||||
"mode", "LIST",
|
||||
"group_by", groupBy,
|
||||
"product_code_list", productCodes,
|
||||
"warehouse_id", warehouseId,
|
||||
"include_zero", includeZero,
|
||||
"include_total", includeTotal,
|
||||
"page_size", pageSize,
|
||||
"cursor", McpParams.str(params, "cursor")
|
||||
);
|
||||
Map<String, Object> data = listData(items, pageInfo, total);
|
||||
return response(buildMetadata("LIST"), query, data);
|
||||
}
|
||||
|
||||
private Map<String, List<Map<String, Object>>> loadWarehouseBreakdown(List<String> productCodes, Long warehouseId) {
|
||||
Map<String, List<Map<String, Object>>> result = new LinkedHashMap<>();
|
||||
if (productCodes.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<Map<String, Object>> rows = inventoryInfoMapper.selectStockByProductWarehouse(productCodes, warehouseId);
|
||||
if (rows == null) {
|
||||
return result;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (row == null) {
|
||||
continue;
|
||||
}
|
||||
String code = str(row.get("productCode"));
|
||||
if (code == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> warehouse = new LinkedHashMap<>();
|
||||
warehouse.put("warehouseId", row.get("warehouseId"));
|
||||
warehouse.put("warehouseName", row.get("warehouseName"));
|
||||
warehouse.put("inStockQty", toLong(row.get("inStockQty")));
|
||||
warehouse.put("outStockQty", toLong(row.get("outStockQty")));
|
||||
result.computeIfAbsent(code, k -> new ArrayList<>()).add(warehouse);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> toSummaryItem(String groupBy, Map<String, Object> row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
Object groupKey = row.get("groupKey");
|
||||
if ("PRODUCT".equals(groupBy)) {
|
||||
item.put("groupKey", str(groupKey));
|
||||
item.put("productCode", str(groupKey));
|
||||
} else if ("WAREHOUSE".equals(groupBy)) {
|
||||
item.put("groupKey", groupKey);
|
||||
item.put("warehouseId", groupKey);
|
||||
item.put("warehouseName", row.get("groupName"));
|
||||
} else if ("STATUS".equals(groupBy)) {
|
||||
String status = str(groupKey);
|
||||
item.put("groupKey", status);
|
||||
item.put("inventoryStatus", status);
|
||||
item.put("inventoryStatusName", inventoryStatusName(status));
|
||||
}
|
||||
putMetrics(item, row);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void putMetrics(Map<String, Object> item, Map<String, Object> row) {
|
||||
item.put("inStockQty", toLong(row.get("inStockQty")));
|
||||
item.put("outStockQty", toLong(row.get("outStockQty")));
|
||||
item.put("totalQty", toLong(row.get("totalQty")));
|
||||
item.put("innerAmount", amount(row.get("innerAmount")));
|
||||
item.put("outerAmount", amount(row.get("outerAmount")));
|
||||
}
|
||||
|
||||
/** include_zero 判定:本次请求的全部度量均为 0/null。 */
|
||||
private boolean isAllZero(Map<String, Object> row) {
|
||||
return toLong(row.get("inStockQty")) == 0L
|
||||
&& toLong(row.get("outStockQty")) == 0L
|
||||
&& toLong(row.get("totalQty")) == 0L
|
||||
&& BigDecimal.ZERO.compareTo(amount(row.get("innerAmount"))) == 0
|
||||
&& BigDecimal.ZERO.compareTo(amount(row.get("outerAmount"))) == 0;
|
||||
}
|
||||
|
||||
private long toLong(Object value) {
|
||||
if (value == null) {
|
||||
return 0L;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value)).longValue();
|
||||
} catch (NumberFormatException ex) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal amount(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (value instanceof BigDecimal) {
|
||||
return (BigDecimal) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return new BigDecimal(value.toString());
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value == null ? null : McpFormat.trimToNull(String.valueOf(value));
|
||||
}
|
||||
|
||||
private Long longVal(Map<String, Object> params, String key) {
|
||||
String value = McpParams.str(params, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(value);
|
||||
} catch (NumberFormatException ex) {
|
||||
throw McpToolException.invalidParams("参数 " + key + " 必须为整数,当前值:" + value);
|
||||
}
|
||||
}
|
||||
|
||||
private String inventoryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryInfo.InventoryStatusEnum value : InventoryInfo.InventoryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata(String mode) {
|
||||
Map<String, Object> metadata = metadata("库存汇总聚合(mode=" + mode + ")");
|
||||
metadata.put("query_fields", mapData(
|
||||
"mode", "SUMMARY(默认,一次算完不分页)/ LIST(按产品游标分页)",
|
||||
"group_by", "NONE(默认)/ PRODUCT / WAREHOUSE / STATUS",
|
||||
"product_code_list", "产品编码列表,≤" + MAX_PRODUCT_CODE + "(LIST 必填)",
|
||||
"warehouse_id", "仓库ID(可选)",
|
||||
"top_n", "SUMMARY 分组数上限,默认 " + DEFAULT_TOP_N + " / 上限 " + MAX_TOP_N,
|
||||
"include_zero", "是否包含全零分组,默认 false",
|
||||
"include_total", "LIST 是否统计产品分组总数,默认 false",
|
||||
"page_size", "LIST 每页条数,默认 " + McpPages.DEFAULT_PAGE_SIZE + " / 上限 " + McpPages.MAX_AGG_PAGE_SIZE,
|
||||
"cursor", "LIST 分页游标,原样回传 page_info.next_cursor"
|
||||
));
|
||||
metadata.put("data_fields", mapData(
|
||||
"total", "返回分组数",
|
||||
"items", "分组聚合结果;LIST 模式每条产品含 warehouses[] 仓库拆分",
|
||||
"page_info", "LIST 模式分页信息"
|
||||
));
|
||||
metadata.put("item_fields", mapData(
|
||||
"productCode", "产品BOM编码(LIST 及 group_by=PRODUCT)",
|
||||
"warehouseId", "仓库ID(group_by=WAREHOUSE)",
|
||||
"warehouseName", "仓库名称(group_by=WAREHOUSE)",
|
||||
"inventoryStatus", "库存状态编码(group_by=STATUS)",
|
||||
"inventoryStatusName", "库存状态名称(0=入库,1=出库)",
|
||||
"inStockQty", "在库数量(inventory_status='0')",
|
||||
"outStockQty", "已出库数量(inventory_status='1')",
|
||||
"totalQty", "总数量",
|
||||
"innerAmount", "入库价合计(sum(inner_price),NULL 计 0)",
|
||||
"outerAmount", "出库价合计(sum(outer_price),NULL 计 0)",
|
||||
"warehouses", "该产品的仓库拆分数组(LIST)",
|
||||
"warehouses[].warehouseId", "仓库ID",
|
||||
"warehouses[].warehouseName", "仓库名称",
|
||||
"warehouses[].inStockQty", "该仓在库数量",
|
||||
"warehouses[].outStockQty", "该仓已出库数量"
|
||||
));
|
||||
metadata.put("aggregation_rule", "在库 = inventory_status='0'(**未扣除已发货占用**,业务待确认,见方案 15.7);"
|
||||
+ "已出库 = inventory_status='1';innerAmount/outerAmount 为 sum(inner_price)/sum(outer_price),NULL 计 0;"
|
||||
+ "inner_price/outer_price 是否含税未经业务确认,不做换算。"
|
||||
+ "include_zero 判定:按本次请求的 metrics 集合(inStockQty/outStockQty/totalQty/innerAmount/outerAmount)全为 0/null 视为全零行。"
|
||||
+ "SUMMARY 为一次全量聚合(不受翻页影响,upstream 数据变化会导致结果变化)。");
|
||||
metadata.put("notes", "LIST 模式仅支持 group_by=NONE/PRODUCT(按产品游标分页);WAREHOUSE/STATUS 等维度在 LIST 下缺少索引支撑,请改用 mode=SUMMARY(方案 15.2)。");
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,950 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.sip.domain.AgentInfo;
|
||||
import com.ruoyi.sip.domain.CustomerInfo;
|
||||
import com.ruoyi.sip.domain.OmsCompanyInfo;
|
||||
import com.ruoyi.sip.domain.OmsWarehouseInfo;
|
||||
import com.ruoyi.sip.domain.PartnerInfo;
|
||||
import com.ruoyi.sip.domain.ProductInfo;
|
||||
import com.ruoyi.sip.domain.VendorInfo;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpCursor;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpPages;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.AgentInfoMapper;
|
||||
import com.ruoyi.sip.mapper.CustomerInfoMapper;
|
||||
import com.ruoyi.sip.mapper.OmsCompanyInfoMapper;
|
||||
import com.ruoyi.sip.mapper.OmsWarehouseInfoMapper;
|
||||
import com.ruoyi.sip.mapper.PartnerInfoMapper;
|
||||
import com.ruoyi.sip.mapper.ProductInfoMapper;
|
||||
import com.ruoyi.sip.mapper.VendorInfoMapper;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
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.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* MCP 工具 master_data_list(主数据列表 / 批量编码→名称翻译,游标分页,entity 参数化)。
|
||||
*
|
||||
* <p>方案依据:docx/mcp-data-tools-plan.md 第 4.3、5、7(第 10/15/20 条)、8.2、15.12、附录 A.12。</p>
|
||||
*
|
||||
* <p>核心用途:把其它工具返回的 partnerCode / customerCode / agentCode / vendorCode / productCode / userId /
|
||||
* warehouseCode / companyCode 一次批量翻译成名称与属性。</p>
|
||||
*
|
||||
* <p>权限串来源(逐个 entity 从对应 Controller 的 {@code @RequiresPermissions} 抄取):
|
||||
* PARTNER→PartnerInfoController#list、CUSTOMER→CustomerInfoController#list、AGENT→AgentInfoController#list、
|
||||
* VENDOR→VendorInfoController#list、PRODUCT→ProductInfoController#list(VueProductInfoController#list 同串)、
|
||||
* USER→SysUserController#list、WAREHOUSE→OmsWarehouseInfoController#list、COMPANY→OmsCompanyInfoController#list。</p>
|
||||
*
|
||||
* <p>实现约定:PARTNER/CUSTOMER/AGENT/VENDOR/PRODUCT/COMPANY 走新增的 SQL 游标分页(limit+1 探测);
|
||||
* USER(sys_user,约 119 行)与 WAREHOUSE(oms_warehouse_info,14 行)按方案复用既有查询后在内存中做 keyset 分页
|
||||
* (两张表规模极小,方案允许优先复用既有方法)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class MasterDataListToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final Set<String> ENTITIES = new LinkedHashSet<>(Arrays.asList(
|
||||
"PARTNER", "CUSTOMER", "AGENT", "VENDOR", "PRODUCT", "USER", "WAREHOUSE", "COMPANY"));
|
||||
|
||||
private static final long COUNT_CAP = 50000L;
|
||||
|
||||
@Autowired
|
||||
private PartnerInfoMapper partnerInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private CustomerInfoMapper customerInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private AgentInfoMapper agentInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private VendorInfoMapper vendorInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private ProductInfoMapper productInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsWarehouseInfoMapper omsWarehouseInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsCompanyInfoMapper omsCompanyInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private SysUserMapper sysUserMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "master_data_list";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "主数据列表 / 批量编码→名称翻译(只读)。entity=PARTNER 进货商 / CUSTOMER 客户 / AGENT 代表处 / "
|
||||
+ "VENDOR 制造商 / PRODUCT 产品 / USER 系统用户 / WAREHOUSE 仓库 / COMPANY 己方公司主体。"
|
||||
+ "典型用途:把其它工具返回的编码(partnerCode/vendorCode/productCode/userId/warehouseCode 等)"
|
||||
+ "放到 code_list 中一次性取回名称与属性,避免逐个查询;也支持名称模糊、状态过滤。"
|
||||
+ "分页查询工具:若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 "
|
||||
+ "has_more 为 false,否则数据不完整。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("entity", enumProperty("查询实体(必填):PARTNER=进货商 / CUSTOMER=客户 / AGENT=代表处 / "
|
||||
+ "VENDOR=制造商 / PRODUCT=产品 / USER=系统用户 / WAREHOUSE=仓库 / COMPANY=己方公司主体",
|
||||
"PARTNER", "CUSTOMER", "AGENT", "VENDOR", "PRODUCT", "USER", "WAREHOUSE", "COMPANY"));
|
||||
properties.put("code_list", arrayProperty("编码列表,≤" + McpPages.MAX_CODE_LIST
|
||||
+ ":PARTNER→partner_code / CUSTOMER→customer_code / AGENT→agent_code / VENDOR→vendor_code / "
|
||||
+ "PRODUCT→product_code / USER→user_id(数字)/ WAREHOUSE→warehouse_code / COMPANY→company_code。"
|
||||
+ "非空时走批量 in 查询(不逐条查询),这是本工具的核心用途"));
|
||||
properties.put("name_keyword", stringProperty("名称模糊匹配(各 entity 的名称列:partner_name/customer_name/"
|
||||
+ "agent_name/vendor_name/product_name/user_name/warehouse_name/company_name)"));
|
||||
properties.put("status", intProperty("状态过滤:PARTNER/CUSTOMER/AGENT=数据状态(0有效);PRODUCT=产品状态(0有效);"
|
||||
+ "VENDOR=合作状态(0正常合作/1暂停合作,见 VendorStatusEnum);WAREHOUSE=仓库状态(0正常/1停用);"
|
||||
+ "USER=系统用户状态(0正常/1停用)"));
|
||||
properties.put("type", stringProperty("产品一级类型(仅 entity=PRODUCT):1软件/2硬件/3服务/11软件维保/22硬件维保/70省代服务/99其它"
|
||||
+ "(枚举 ProductInfo.ProductTypeEnum)"));
|
||||
properties.put("include_disabled", boolProperty("是否包含停用数据,默认 false(仅 entity=WAREHOUSE 生效:"
|
||||
+ "默认只返回正常仓,true 时含停用仓,走 OmsWarehouseInfoMapper.listAll)"));
|
||||
properties.put("include_total", boolProperty("是否统计总条数,默认 false;true 时受 count_cap=" + COUNT_CAP
|
||||
+ " 限制,超出置 total_count_capped=true"));
|
||||
withPagingProperties(properties, McpPages.MAX_LIST_PAGE_SIZE, McpPages.DEFAULT_PAGE_SIZE);
|
||||
return objectSchema(properties, "entity");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
String entity = McpParams.requireEnum(params, "entity", ENTITIES);
|
||||
McpAuth.requireAnyPermission(permissionsOf(entity));
|
||||
|
||||
int pageSize = McpPages.pageSize(params, McpPages.DEFAULT_PAGE_SIZE, McpPages.MAX_LIST_PAGE_SIZE);
|
||||
List<String> codeList = McpParams.codeList(params, "code_list", McpPages.MAX_CODE_LIST);
|
||||
String nameKeyword = McpParams.str(params, "name_keyword");
|
||||
Integer status = McpParams.intVal(params, "status");
|
||||
String type = "PRODUCT".equals(entity) ? McpParams.str(params, "type") : null;
|
||||
boolean includeDisabled = McpParams.boolVal(params, "include_disabled", false);
|
||||
boolean includeTotal = McpParams.boolVal(params, "include_total", false);
|
||||
|
||||
// 过滤条件指纹:cursor 与当前条件不一致时直接报错,避免串用游标漏数(方案 5.4)
|
||||
String filterHash = McpParams.filterHash(getToolName(), entity, sortedJoin(codeList), nameKeyword,
|
||||
status == null ? null : String.valueOf(status), type, String.valueOf(includeDisabled));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
int pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
EntityResult result = runEntity(entity, codeList, nameKeyword, status, type, includeDisabled, includeTotal,
|
||||
pageSize, cursor);
|
||||
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(result.items.size(), pageSize, result.hasMore,
|
||||
result.hasMore ? McpCursor.encode(getToolName(), filterHash, pageNo + 1, result.lastSortKeys) : null,
|
||||
result.sortBy, pageNo, result.total, result.totalCapped, false);
|
||||
|
||||
Map<String, Object> data = listData(result.items, pageInfo, result.total);
|
||||
return response(buildMetadata(entity),
|
||||
queryEcho(entity, codeList, nameKeyword, status, type, includeDisabled, includeTotal, pageSize, pageNo),
|
||||
data);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ entity 分发
|
||||
|
||||
private EntityResult runEntity(String entity, List<String> codeList, String nameKeyword, Integer status,
|
||||
String type, boolean includeDisabled, boolean includeTotal, int pageSize,
|
||||
McpCursor cursor) {
|
||||
switch (entity) {
|
||||
case "PARTNER":
|
||||
return runPartner(codeList, nameKeyword, status, includeTotal, pageSize, cursor);
|
||||
case "CUSTOMER":
|
||||
return runCustomer(codeList, nameKeyword, status, includeTotal, pageSize, cursor);
|
||||
case "AGENT":
|
||||
return runAgent(codeList, nameKeyword, status, includeTotal, pageSize, cursor);
|
||||
case "VENDOR":
|
||||
return runVendor(codeList, nameKeyword, status, includeTotal, pageSize, cursor);
|
||||
case "PRODUCT":
|
||||
return runProduct(codeList, nameKeyword, status, type, includeTotal, pageSize, cursor);
|
||||
case "USER":
|
||||
return runUser(codeList, nameKeyword, status, includeTotal, pageSize, cursor);
|
||||
case "WAREHOUSE":
|
||||
return runWarehouse(codeList, nameKeyword, status, includeDisabled, includeTotal, pageSize, cursor);
|
||||
case "COMPANY":
|
||||
return runCompany(codeList, nameKeyword, includeTotal, pageSize, cursor);
|
||||
default:
|
||||
throw McpToolException.invalidParams("不支持的 entity:" + entity);
|
||||
}
|
||||
}
|
||||
|
||||
private EntityResult runPartner(List<String> codeList, String nameKeyword, Integer status, boolean includeTotal,
|
||||
int pageSize, McpCursor cursor) {
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("codeList", codeList);
|
||||
q.put("nameKeyword", nameKeyword);
|
||||
q.put("status", status);
|
||||
q.put("limit", pageSize + 1);
|
||||
putCodeCursor(q, cursor);
|
||||
|
||||
List<PartnerInfo> rows = partnerInfoMapper.listPartnerMasterPage(q);
|
||||
EntityResult result = new EntityResult("partner_code,id");
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<PartnerInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (PartnerInfo row : page) {
|
||||
result.items.add(toPartnerItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
PartnerInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(McpFormat.trim(last.getPartnerCode()), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, partnerInfoMapper.listPartnerMasterPage(countQuery(q, "lastCode", "lastId")).size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runCustomer(List<String> codeList, String nameKeyword, Integer status, boolean includeTotal,
|
||||
int pageSize, McpCursor cursor) {
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("codeList", codeList);
|
||||
q.put("nameKeyword", nameKeyword);
|
||||
q.put("status", status);
|
||||
q.put("limit", pageSize + 1);
|
||||
putCodeCursor(q, cursor);
|
||||
|
||||
List<CustomerInfo> rows = customerInfoMapper.listCustomerMasterPage(q);
|
||||
EntityResult result = new EntityResult("customer_code,id");
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<CustomerInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (CustomerInfo row : page) {
|
||||
result.items.add(toCustomerItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
CustomerInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(McpFormat.trim(last.getCustomerCode()), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, customerInfoMapper.listCustomerMasterPage(countQuery(q, "lastCode", "lastId")).size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runAgent(List<String> codeList, String nameKeyword, Integer status, boolean includeTotal,
|
||||
int pageSize, McpCursor cursor) {
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("codeList", codeList);
|
||||
q.put("nameKeyword", nameKeyword);
|
||||
q.put("status", status);
|
||||
q.put("limit", pageSize + 1);
|
||||
putCodeCursor(q, cursor);
|
||||
|
||||
List<AgentInfo> rows = agentInfoMapper.listAgentMasterPage(q);
|
||||
EntityResult result = new EntityResult("agent_code,id");
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<AgentInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (AgentInfo row : page) {
|
||||
result.items.add(toAgentItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
AgentInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(McpFormat.trim(last.getAgentCode()), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, agentInfoMapper.listAgentMasterPage(countQuery(q, "lastCode", "lastId")).size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runVendor(List<String> codeList, String nameKeyword, Integer status, boolean includeTotal,
|
||||
int pageSize, McpCursor cursor) {
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("codeList", codeList);
|
||||
q.put("nameKeyword", nameKeyword);
|
||||
q.put("status", status == null ? null : String.valueOf(status));
|
||||
q.put("limit", pageSize + 1);
|
||||
putCodeCursor(q, cursor);
|
||||
|
||||
List<VendorInfo> rows = vendorInfoMapper.listVendorMasterPage(q);
|
||||
EntityResult result = new EntityResult("vendor_code,vendor_id");
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<VendorInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (VendorInfo row : page) {
|
||||
result.items.add(toVendorItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
VendorInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(McpFormat.trim(last.getVendorCode()), last.getVendorId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, vendorInfoMapper.listVendorMasterPage(countQuery(q, "lastCode", "lastId")).size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runProduct(List<String> codeList, String nameKeyword, Integer status, String type,
|
||||
boolean includeTotal, int pageSize, McpCursor cursor) {
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("codeList", codeList);
|
||||
q.put("nameKeyword", nameKeyword);
|
||||
q.put("status", status);
|
||||
q.put("type", type);
|
||||
q.put("limit", pageSize + 1);
|
||||
putCodeCursor(q, cursor);
|
||||
|
||||
List<ProductInfo> rows = productInfoMapper.listProductMasterPage(q);
|
||||
EntityResult result = new EntityResult("product_code,id");
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<ProductInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (ProductInfo row : page) {
|
||||
result.items.add(toProductItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
ProductInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(McpFormat.trim(last.getProductCode()), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, productInfoMapper.listProductMasterPage(countQuery(q, "lastCode", "lastId")).size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runCompany(List<String> codeList, String nameKeyword, boolean includeTotal, int pageSize,
|
||||
McpCursor cursor) {
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("codeList", codeList);
|
||||
q.put("nameKeyword", nameKeyword);
|
||||
q.put("limit", pageSize + 1);
|
||||
putCodeCursor(q, cursor);
|
||||
|
||||
List<OmsCompanyInfo> rows = omsCompanyInfoMapper.listCompanyMasterPage(q);
|
||||
EntityResult result = new EntityResult("company_code,id");
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<OmsCompanyInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (OmsCompanyInfo row : page) {
|
||||
result.items.add(toCompanyItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
OmsCompanyInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(McpFormat.trim(last.getCompanyCode()), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, omsCompanyInfoMapper.listCompanyMasterPage(countQuery(q, "lastCode", "lastId")).size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* USER:复用 SysUserMapper.selectUserList(sys_user 约 119 行,方案允许全表扫),按 user_id 在内存中 keyset 分页。
|
||||
*/
|
||||
private EntityResult runUser(List<String> codeList, String nameKeyword, Integer status, boolean includeTotal,
|
||||
int pageSize, McpCursor cursor) {
|
||||
SysUser probe = new SysUser();
|
||||
if (!codeList.isEmpty()) {
|
||||
List<Long> userIds = new ArrayList<>();
|
||||
for (String code : codeList) {
|
||||
try {
|
||||
userIds.add(Long.valueOf(code));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw McpToolException.invalidParams("entity=USER 的 code_list 仅支持数字用户ID,当前值:" + code);
|
||||
}
|
||||
}
|
||||
probe.setUserIdList(userIds);
|
||||
}
|
||||
if (nameKeyword != null) {
|
||||
probe.setUserName(nameKeyword);
|
||||
}
|
||||
if (status != null) {
|
||||
probe.setStatus(String.valueOf(status));
|
||||
}
|
||||
List<SysUser> all = sysUserMapper.selectUserList(probe);
|
||||
if (all == null) {
|
||||
all = Collections.emptyList();
|
||||
}
|
||||
List<SysUser> filtered = new ArrayList<>();
|
||||
for (SysUser user : all) {
|
||||
if (user == null || user.getUserId() == null) {
|
||||
continue;
|
||||
}
|
||||
if (cursor != null) {
|
||||
Long lastId = cursor.longKey(0);
|
||||
if (lastId != null && user.getUserId() <= lastId) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
filtered.add(user);
|
||||
}
|
||||
filtered.sort(Comparator.comparing(SysUser::getUserId));
|
||||
|
||||
EntityResult result = new EntityResult("user_id");
|
||||
result.hasMore = McpPages.hasMore(filtered, pageSize);
|
||||
List<SysUser> page = McpPages.trim(filtered, pageSize);
|
||||
for (SysUser user : page) {
|
||||
result.items.add(toUserItem(user));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
result.lastSortKeys = keysOf(page.get(page.size() - 1).getUserId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, filtered.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* WAREHOUSE:include_disabled=false 用 selectOmsWarehouseInfoList(仅正常仓),true 用 listAll(含停用仓);
|
||||
* oms_warehouse_info 仅 14 行,按 warehouse_code,id 在内存中 keyset 分页。
|
||||
*/
|
||||
private EntityResult runWarehouse(List<String> codeList, String nameKeyword, Integer status, boolean includeDisabled,
|
||||
boolean includeTotal, int pageSize, McpCursor cursor) {
|
||||
OmsWarehouseInfo probe = new OmsWarehouseInfo();
|
||||
if (nameKeyword != null) {
|
||||
probe.setWarehouseName(nameKeyword);
|
||||
}
|
||||
List<OmsWarehouseInfo> all = includeDisabled
|
||||
? omsWarehouseInfoMapper.listAll(probe)
|
||||
: omsWarehouseInfoMapper.selectOmsWarehouseInfoList(probe);
|
||||
if (all == null) {
|
||||
all = Collections.emptyList();
|
||||
}
|
||||
String statusText = status == null ? null : String.valueOf(status);
|
||||
String lastCode = cursor == null ? null : cursor.key(0);
|
||||
Long lastId = cursor == null ? null : cursor.longKey(1);
|
||||
|
||||
List<OmsWarehouseInfo> filtered = new ArrayList<>();
|
||||
for (OmsWarehouseInfo warehouse : all) {
|
||||
if (warehouse == null) {
|
||||
continue;
|
||||
}
|
||||
String code = McpFormat.trim(warehouse.getWarehouseCode());
|
||||
if (code == null) {
|
||||
code = "";
|
||||
}
|
||||
if (!codeList.isEmpty() && !codeList.contains(code)) {
|
||||
continue;
|
||||
}
|
||||
if (statusText != null && !statusText.equals(McpFormat.trim(warehouse.getWarehouseStatus()))) {
|
||||
continue;
|
||||
}
|
||||
if (lastId != null) {
|
||||
String lastCodeText = lastCode == null ? "" : lastCode;
|
||||
int compare = code.compareTo(lastCodeText);
|
||||
if (compare < 0 || (compare == 0 && (warehouse.getId() == null || warehouse.getId() <= lastId))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
filtered.add(warehouse);
|
||||
}
|
||||
filtered.sort(Comparator
|
||||
.comparing((OmsWarehouseInfo item) -> item.getWarehouseCode() == null ? ""
|
||||
: item.getWarehouseCode().trim())
|
||||
.thenComparing(item -> item.getId() == null ? 0L : item.getId()));
|
||||
|
||||
EntityResult result = new EntityResult("warehouse_code,id");
|
||||
result.hasMore = McpPages.hasMore(filtered, pageSize);
|
||||
List<OmsWarehouseInfo> page = McpPages.trim(filtered, pageSize);
|
||||
for (OmsWarehouseInfo warehouse : page) {
|
||||
result.items.add(toWarehouseItem(warehouse));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
OmsWarehouseInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(McpFormat.trim(last.getWarehouseCode()), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, filtered.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 行映射
|
||||
|
||||
private Map<String, Object> toPartnerItem(PartnerInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("partnerCode", McpFormat.trim(row.getPartnerCode()));
|
||||
item.put("partnerName", row.getPartnerName());
|
||||
item.put("province", row.getProvince());
|
||||
item.put("city", row.getCity());
|
||||
item.put("address", row.getAddress());
|
||||
item.put("contactPerson", row.getContactPerson());
|
||||
item.put("contactPhone", row.getContactPhone());
|
||||
item.put("contactEmail", row.getContactEmail());
|
||||
item.put("level", row.getLevel());
|
||||
item.put("systemUserId", McpFormat.trim(row.getSystemUserId()));
|
||||
item.put("systemUserName", row.getSystemUserName());
|
||||
item.put("status", row.getStatus());
|
||||
item.put("payName", row.getPayName());
|
||||
item.put("payBankNumber", row.getPayBankNumber());
|
||||
item.put("payBankOpenAddress", row.getPayBankOpenAddress());
|
||||
item.put("bankNumber", row.getBankNumber());
|
||||
item.put("socialCredit", row.getSocialCredit());
|
||||
item.put("createAt", McpFormat.dateTime(row.getCreateAt()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toCustomerItem(CustomerInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("customerCode", McpFormat.trim(row.getCustomerCode()));
|
||||
item.put("customerName", row.getCustomerName());
|
||||
item.put("customerPostcode", row.getCustomerPostcode());
|
||||
item.put("province", row.getProvince());
|
||||
item.put("city", row.getCity());
|
||||
item.put("address", row.getAddress());
|
||||
item.put("contactPerson", row.getContactPerson());
|
||||
item.put("contactPhone", row.getContactPhone());
|
||||
item.put("contactEmail", row.getContactEmail());
|
||||
item.put("bgProperty", row.getBgProperty());
|
||||
item.put("industryType", row.getIndustryType());
|
||||
item.put("status", row.getStatus());
|
||||
item.put("createAt", McpFormat.dateTime(row.getCreateAt()));
|
||||
item.put("updateAt", McpFormat.dateTime(row.getUpdateAt()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toAgentItem(AgentInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("agentCode", McpFormat.trim(row.getAgentCode()));
|
||||
item.put("agentName", row.getAgentName());
|
||||
item.put("province", row.getProvince());
|
||||
item.put("city", row.getCity());
|
||||
item.put("address", row.getAddress());
|
||||
item.put("contactPerson", row.getContactPerson());
|
||||
item.put("contactPhone", row.getContactPhone());
|
||||
item.put("contactEmail", row.getContactEmail());
|
||||
item.put("status", row.getStatus());
|
||||
item.put("createAt", McpFormat.dateTime(row.getCreateAt()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toVendorItem(VendorInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("vendorId", row.getVendorId());
|
||||
item.put("vendorCode", McpFormat.trim(row.getVendorCode()));
|
||||
item.put("vendorName", row.getVendorName());
|
||||
item.put("vendorAddress", row.getVendorAddress());
|
||||
item.put("vendorUser", row.getVendorUser());
|
||||
item.put("vendorEmail", row.getVendorEmail());
|
||||
item.put("vendorPhone", row.getVendorPhone());
|
||||
item.put("vendorStatus", row.getVendorStatus());
|
||||
item.put("vendorStatusName", vendorStatusName(row.getVendorStatus()));
|
||||
item.put("warehouseId", McpFormat.trim(row.getWarehouseId()));
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("ownWarehouseId", row.getOwnWarehouseId());
|
||||
item.put("payType", row.getPayType());
|
||||
item.put("payTypeName", payTypeName(row.getPayType()));
|
||||
item.put("payConfigDay", row.getPayConfigDay());
|
||||
item.put("payName", row.getPayName());
|
||||
item.put("payBankNumber", row.getPayBankNumber());
|
||||
item.put("payBankOpenAddress", row.getPayBankOpenAddress());
|
||||
item.put("bankNumber", row.getBankNumber());
|
||||
item.put("socialCredit", row.getSocialCredit());
|
||||
item.put("province", row.getProvince());
|
||||
item.put("city", row.getCity());
|
||||
item.put("generatedAddress", row.getGeneratedAddress());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toProductItem(ProductInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("hzCode", row.getHzCode());
|
||||
item.put("productName", row.getProductName());
|
||||
item.put("model", row.getModel());
|
||||
item.put("type", row.getType());
|
||||
item.put("typeName", productTypeName(row.getType()));
|
||||
item.put("level2Type", row.getLevel2Type());
|
||||
item.put("level2TypeName", productLevel2TypeName(row.getType(), row.getLevel2Type()));
|
||||
item.put("value", row.getValue());
|
||||
item.put("cataloguePrice", row.getCataloguePrice());
|
||||
item.put("guidanceDiscount", row.getGuidanceDiscount());
|
||||
item.put("vendorCode", McpFormat.trim(row.getVendorCode()));
|
||||
item.put("vendorName", row.getVendorName());
|
||||
item.put("availableCount", row.getAvailableCount());
|
||||
item.put("cumulativeCount", row.getCumulativeCount());
|
||||
item.put("description", row.getDescription());
|
||||
item.put("remark", row.getRemark());
|
||||
item.put("createdAt", McpFormat.dateTime(row.getCreatedAt()));
|
||||
item.put("updatedAt", McpFormat.dateTime(row.getUpdatedAt()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toUserItem(SysUser row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("userId", row.getUserId());
|
||||
item.put("loginName", row.getLoginName());
|
||||
item.put("userName", row.getUserName());
|
||||
item.put("deptId", row.getDeptId());
|
||||
item.put("deptName", row.getDept() == null ? null : row.getDept().getDeptName());
|
||||
item.put("email", row.getEmail());
|
||||
item.put("phonenumber", row.getPhonenumber());
|
||||
item.put("status", row.getStatus());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toWarehouseItem(OmsWarehouseInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("warehouseId", row.getId());
|
||||
item.put("warehouseCode", McpFormat.trim(row.getWarehouseCode()));
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("warehouseType", row.getWarehouseType());
|
||||
item.put("warehouseTypeName", warehouseTypeName(row.getWarehouseType()));
|
||||
item.put("warehouseStatus", row.getWarehouseStatus());
|
||||
item.put("warehouseStatusName", warehouseStatusName(row.getWarehouseStatus()));
|
||||
item.put("address", row.getAddress());
|
||||
item.put("managerName", row.getManagerName());
|
||||
item.put("managerPhone", row.getManagerPhone());
|
||||
item.put("managerEmail", row.getManagerEmail());
|
||||
item.put("remark", row.getRemark());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toCompanyItem(OmsCompanyInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("companyCode", McpFormat.trim(row.getCompanyCode()));
|
||||
item.put("companyName", row.getCompanyName());
|
||||
item.put("companyUser", row.getCompanyUser());
|
||||
item.put("companyEmail", row.getCompanyEmail());
|
||||
item.put("companyPhone", row.getCompanyPhone());
|
||||
item.put("companyAddress", row.getCompanyAddress());
|
||||
item.put("payName", row.getPayName());
|
||||
item.put("payBankNumber", row.getPayBankNumber());
|
||||
item.put("payBankOpenAddress", row.getPayBankOpenAddress());
|
||||
item.put("bankNumber", row.getBankNumber());
|
||||
item.put("socialCredit", row.getSocialCredit());
|
||||
return item;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 字典翻译
|
||||
|
||||
private String productTypeName(String type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
for (ProductInfo.ProductTypeEnum value : ProductInfo.ProductTypeEnum.values()) {
|
||||
if (value.getType().equals(type.trim())) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 二级类型需结合一级类型选择枚举:1/11 软件类、2/22 硬件类、3/70 服务类。
|
||||
*/
|
||||
private String productLevel2TypeName(String type, String level2Type) {
|
||||
if (type == null || level2Type == null) {
|
||||
return level2Type == null ? null : level2Type;
|
||||
}
|
||||
String level = level2Type.trim();
|
||||
if ("1".equals(type.trim()) || "11".equals(type.trim())) {
|
||||
for (ProductInfo.ProductTypeLevel2SoftwareEnum value : ProductInfo.ProductTypeLevel2SoftwareEnum.values()) {
|
||||
if (value.getType().equals(level)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
} else if ("2".equals(type.trim()) || "22".equals(type.trim())) {
|
||||
for (ProductInfo.ProductTypeLevel2HardwareEnum value : ProductInfo.ProductTypeLevel2HardwareEnum.values()) {
|
||||
if (value.getType().equals(level)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
} else if ("3".equals(type.trim()) || "70".equals(type.trim())) {
|
||||
for (ProductInfo.ProductTypeLevel2ServiceEnum value : ProductInfo.ProductTypeLevel2ServiceEnum.values()) {
|
||||
if (value.getType().equals(level)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
private String vendorStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
// 注意:VendorInfo.VendorStatusEnum 的 value 是编码、code 是中文描述(既有类命名如此)
|
||||
for (VendorInfo.VendorStatusEnum value : VendorInfo.VendorStatusEnum.values()) {
|
||||
if (value.getValue().equals(status.trim())) {
|
||||
return value.getCode();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String payTypeName(Integer payType) {
|
||||
if (payType == null) {
|
||||
return null;
|
||||
}
|
||||
for (VendorInfo.PayTypeEnum value : VendorInfo.PayTypeEnum.values()) {
|
||||
if (value.getCode().equals(payType)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return String.valueOf(payType);
|
||||
}
|
||||
|
||||
private String warehouseStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsWarehouseInfo.WarehouseStatusEnum value : OmsWarehouseInfo.WarehouseStatusEnum.values()) {
|
||||
if (value.getValue().equals(status.trim())) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String warehouseTypeName(String type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
// 注意:OmsWarehouseInfo.WarehouseTypeEnum 的 value 是编码、code 是中文描述(既有类命名如此)
|
||||
for (OmsWarehouseInfo.WarehouseTypeEnum value : OmsWarehouseInfo.WarehouseTypeEnum.values()) {
|
||||
if (value.getValue().equals(type.trim())) {
|
||||
return value.getCode();
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private void putCodeCursor(Map<String, Object> q, McpCursor cursor) {
|
||||
if (cursor != null) {
|
||||
q.put("lastCode", cursor.key(0));
|
||||
q.put("lastId", cursor.longKey(1));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> countQuery(Map<String, Object> q, String... cursorKeys) {
|
||||
Map<String, Object> copy = new HashMap<>(q);
|
||||
if (cursorKeys != null) {
|
||||
for (String key : cursorKeys) {
|
||||
copy.remove(key);
|
||||
}
|
||||
}
|
||||
copy.put("limit", COUNT_CAP + 1);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private void applyTotal(EntityResult result, long counted) {
|
||||
result.totalCapped = counted > COUNT_CAP;
|
||||
result.total = result.totalCapped ? COUNT_CAP : counted;
|
||||
}
|
||||
|
||||
private String sortedJoin(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return String.join(",", new TreeSet<>(values));
|
||||
}
|
||||
|
||||
private Map<String, Object> queryEcho(String entity, List<String> codeList, String nameKeyword, Integer status,
|
||||
String type, boolean includeDisabled, boolean includeTotal, int pageSize,
|
||||
int pageNo) {
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("entity", entity);
|
||||
query.put("code_list", codeList);
|
||||
query.put("name_keyword", nameKeyword);
|
||||
query.put("status", status);
|
||||
query.put("type", type);
|
||||
query.put("include_disabled", includeDisabled);
|
||||
query.put("include_total", includeTotal);
|
||||
query.put("page_size", pageSize);
|
||||
query.put("page_no", pageNo);
|
||||
return query;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata(String entity) {
|
||||
Map<String, Object> metadata = metadata("主数据列表 / 批量编码→名称翻译(entity=" + entity
|
||||
+ ");排序键为各 entity 主编码 + 唯一键,游标分页(不重不漏)。");
|
||||
metadata.put("entity_fields", mapData(
|
||||
"PARTNER", "进货商 partner_info(权限来源 PartnerInfoController#list)",
|
||||
"CUSTOMER", "客户 customer_info(CustomerInfoController#list)",
|
||||
"AGENT", "代表处 agent_info(AgentInfoController#list)",
|
||||
"VENDOR", "制造商 oms_vendor_info(VendorInfoController#list;注意不采用冗余表 vendor_info)",
|
||||
"PRODUCT", "产品 product_info(ProductInfoController#list)",
|
||||
"USER", "系统用户 sys_user(SysUserController#list)",
|
||||
"WAREHOUSE", "仓库 oms_warehouse_info(OmsWarehouseInfoController#list)",
|
||||
"COMPANY", "己方公司主体 oms_company_info(OmsCompanyInfoController#list,当前 0 行)"));
|
||||
metadata.put("data_fields", mapData(
|
||||
"total", "include_total=true 时为命中总条数(否则为当前页条数),受 count_cap=" + COUNT_CAP + " 限制",
|
||||
"items", "主数据列表",
|
||||
"page_info", "分页信息;has_more=true 时必须回传 next_cursor"));
|
||||
metadata.put("item_fields", itemFields());
|
||||
metadata.put("enum_fields", mapData(
|
||||
"PRODUCT.type / typeName", "枚举 ProductInfo.ProductTypeEnum(1软件/2硬件/3服务/11软件维保/22硬件维保/70省代服务/99其它)",
|
||||
"PRODUCT.level2Type / level2TypeName", "枚举 ProductInfo.ProductTypeLevel2{Software|Hardware|Service}Enum(按一级类型选择)",
|
||||
"VENDOR.vendorStatus / vendorStatusName", "枚举 VendorInfo.VendorStatusEnum(0正常合作/1暂停合作)",
|
||||
"VENDOR.payType / payTypeName", "枚举 VendorInfo.PayTypeEnum(0入库付款/1出库付款)",
|
||||
"WAREHOUSE.warehouseType / warehouseTypeName", "枚举 OmsWarehouseInfo.WarehouseTypeEnum(0实体仓/1虚拟仓)",
|
||||
"WAREHOUSE.warehouseStatus / warehouseStatusName", "枚举 OmsWarehouseInfo.WarehouseStatusEnum(0正常/1停用)",
|
||||
"PARTNER.level", "字典 identify_level(本工具只回显编码,未做字典翻译,字典值待确认)",
|
||||
"USER.status", "字符状态:0正常/1停用"));
|
||||
metadata.put("typical_usage", "把其它工具返回的编码批量翻译成名称:把 partnerCode/vendorCode/productCode/"
|
||||
+ "userId/warehouseCode/companyCode 放进 code_list 一次取回(走 in 批量查询,不逐条查询)。");
|
||||
metadata.put("notes", "编码类字段入参与出参统一 trim;USER 与 WAREHOUSE 复用既有查询后在内存中做 keyset 分页"
|
||||
+ "(sys_user 约 119 行、oms_warehouse_info 14 行);include_disabled 仅对 WAREHOUSE 生效;"
|
||||
+ "编码匹配不区分大小写(utf8mb4_unicode_ci 排序规则)。");
|
||||
metadata.put("permission_source", permissionSource(entity));
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> itemFields() {
|
||||
return mapData(
|
||||
"id", "主键(PARTNER/CUSTOMER/AGENT/PRODUCT/COMPANY)",
|
||||
"partnerCode", "进货商编码(PARTNER)",
|
||||
"partnerName", "进货商名称(PARTNER)",
|
||||
"customerCode", "客户编码(CUSTOMER)",
|
||||
"customerName", "客户名称(CUSTOMER)",
|
||||
"customerPostcode", "客户邮编(CUSTOMER)",
|
||||
"agentCode", "代表处编码(AGENT)",
|
||||
"agentName", "代表处名称(AGENT)",
|
||||
"vendorId", "制造商ID(VENDOR)",
|
||||
"vendorCode", "制造商编码(VENDOR;PRODUCT 中为所属制造商编码)",
|
||||
"vendorName", "制造商名称(VENDOR;PRODUCT 中为所属制造商名称)",
|
||||
"vendorAddress", "生产地址(VENDOR)",
|
||||
"vendorUser", "制造商联系人(VENDOR)",
|
||||
"vendorEmail", "制造商邮箱(VENDOR)",
|
||||
"vendorPhone", "制造商电话(VENDOR)",
|
||||
"vendorStatus", "合作状态编码(0正常合作/1暂停合作)(VENDOR)",
|
||||
"vendorStatusName", "合作状态名称(VENDOR)",
|
||||
"warehouseId", "绑定仓库ID(VENDOR)/ 仓库ID(WAREHOUSE)",
|
||||
"warehouseName", "绑定仓库名称(VENDOR)/ 仓库名称(WAREHOUSE)",
|
||||
"ownWarehouseId", "自有仓库ID(逗号分隔字符串)(VENDOR)",
|
||||
"payType", "付款方式编码(0入库付款/1出库付款)(VENDOR)",
|
||||
"payTypeName", "付款方式名称(VENDOR)",
|
||||
"payConfigDay", "付款账期天数(VENDOR)",
|
||||
"generatedAddress", "生成地址(VENDOR)",
|
||||
"productCode", "产品编码(PRODUCT)",
|
||||
"hzCode", "华智编码(PRODUCT)",
|
||||
"productName", "产品名称(PRODUCT)",
|
||||
"model", "产品型号(PRODUCT)",
|
||||
"type", "产品一级类型编码(PRODUCT)",
|
||||
"typeName", "产品一级类型名称(PRODUCT)",
|
||||
"level2Type", "产品二级类型编码(PRODUCT)",
|
||||
"level2TypeName", "产品二级类型名称(PRODUCT)",
|
||||
"value", "产品属性值(PRODUCT)",
|
||||
"cataloguePrice", "目录单价(PRODUCT)",
|
||||
"guidanceDiscount", "指导折扣(PRODUCT)",
|
||||
"availableCount", "实时可用库存(PRODUCT)",
|
||||
"cumulativeCount", "累计发货(PRODUCT)",
|
||||
"description", "产品描述(PRODUCT)",
|
||||
"userId", "用户ID(USER)",
|
||||
"loginName", "登录名(USER)",
|
||||
"userName", "姓名(USER)",
|
||||
"deptId", "部门ID(USER)",
|
||||
"deptName", "部门名称(USER)",
|
||||
"email", "邮箱(USER)",
|
||||
"phonenumber", "手机号(USER)",
|
||||
"warehouseCode", "仓库编码(WAREHOUSE)",
|
||||
"warehouseType", "仓库类型编码(0实体仓/1虚拟仓)(WAREHOUSE)",
|
||||
"warehouseTypeName", "仓库类型名称(WAREHOUSE)",
|
||||
"warehouseStatus", "仓库状态编码(0正常/1停用)(WAREHOUSE)",
|
||||
"warehouseStatusName", "仓库状态名称(WAREHOUSE)",
|
||||
"managerName", "仓库管理员(WAREHOUSE)",
|
||||
"managerPhone", "仓库管理员电话(WAREHOUSE)",
|
||||
"managerEmail", "仓库管理员邮箱(WAREHOUSE)",
|
||||
"companyCode", "公司编码(COMPANY)",
|
||||
"companyName", "公司名称(COMPANY)",
|
||||
"companyUser", "联系用户(COMPANY)",
|
||||
"companyEmail", "联系邮箱(COMPANY)",
|
||||
"companyPhone", "联系电话(COMPANY)",
|
||||
"companyAddress", "公司地址(COMPANY)",
|
||||
"province", "所在省",
|
||||
"city", "所在市",
|
||||
"address", "详细地址",
|
||||
"contactPerson", "联系人",
|
||||
"contactPhone", "联系电话",
|
||||
"contactEmail", "联系邮箱",
|
||||
"bgProperty", "BG属性(CUSTOMER)",
|
||||
"industryType", "所属行业(CUSTOMER)",
|
||||
"level", "进货商认证级别编码(PARTNER,字典 identify_level)",
|
||||
"systemUserId", "绑定系统用户ID(PARTNER)",
|
||||
"systemUserName", "绑定系统用户姓名(PARTNER)",
|
||||
"status", "数据状态(PARTNER/CUSTOMER/AGENT:0有效;USER:0正常/1停用;PRODUCT 的 status 仅作入参过滤,不回显)",
|
||||
"payName", "账户名称(PARTNER/VENDOR/COMPANY 己方账户信息)",
|
||||
"payBankNumber", "银行卡号(PARTNER/VENDOR/COMPANY)",
|
||||
"payBankOpenAddress", "银行开户行(PARTNER/VENDOR/COMPANY)",
|
||||
"bankNumber", "银行行号(PARTNER/VENDOR/COMPANY)",
|
||||
"socialCredit", "统一社会信用代码(PARTNER/VENDOR/COMPANY)",
|
||||
"remark", "备注",
|
||||
"createAt", "创建时间",
|
||||
"updateAt", "更新时间",
|
||||
"createdAt", "创建时间(PRODUCT)",
|
||||
"updatedAt", "更新时间(PRODUCT)");
|
||||
}
|
||||
|
||||
private String permissionSource(String entity) {
|
||||
switch (entity) {
|
||||
case "PARTNER":
|
||||
return "PartnerInfoController#list:system:partner:list(已抄取)";
|
||||
case "CUSTOMER":
|
||||
return "CustomerInfoController#list:system:customer:list(已抄取)";
|
||||
case "AGENT":
|
||||
return "AgentInfoController#list:system:agent:list(已抄取)";
|
||||
case "VENDOR":
|
||||
return "VendorInfoController#list:system:vendor:list(已抄取)";
|
||||
case "PRODUCT":
|
||||
return "ProductInfoController#list / VueProductInfoController#list:system:product:list(已抄取)";
|
||||
case "USER":
|
||||
return "SysUserController#list:system:user:list(已抄取)";
|
||||
case "WAREHOUSE":
|
||||
return "OmsWarehouseInfoController#list:warehouse:info:list(已抄取)";
|
||||
case "COMPANY":
|
||||
return "OmsCompanyInfoController#list:sip:companyInfo:list(已抄取)";
|
||||
default:
|
||||
return "未确定(待确认)";
|
||||
}
|
||||
}
|
||||
|
||||
private String[] permissionsOf(String entity) {
|
||||
switch (entity) {
|
||||
case "PARTNER":
|
||||
return new String[]{"system:partner:list"};
|
||||
case "CUSTOMER":
|
||||
return new String[]{"system:customer:list"};
|
||||
case "AGENT":
|
||||
return new String[]{"system:agent:list"};
|
||||
case "VENDOR":
|
||||
return new String[]{"system:vendor:list"};
|
||||
case "PRODUCT":
|
||||
return new String[]{"system:product:list"};
|
||||
case "USER":
|
||||
return new String[]{"system:user:list"};
|
||||
case "WAREHOUSE":
|
||||
return new String[]{"warehouse:info:list"};
|
||||
case "COMPANY":
|
||||
return new String[]{"sip:companyInfo:list"};
|
||||
default:
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
|
||||
/** entity 查询结果载体 */
|
||||
private static class EntityResult {
|
||||
private final List<Map<String, Object>> items = new ArrayList<>();
|
||||
private final String sortBy;
|
||||
private boolean hasMore;
|
||||
private List<String> lastSortKeys = new ArrayList<>();
|
||||
private Long total;
|
||||
private boolean totalCapped;
|
||||
|
||||
private EntityResult(String sortBy) {
|
||||
this.sortBy = sortBy;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,456 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrder;
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrderItem;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpCursor;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpPages;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpTimeRange;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.OmsPurchaseOrderMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* 采购到货汇总 MCP 工具(只读):mode=SUMMARY(默认,一次算完)或 mode=LIST(按 purchase_no 游标分页)。
|
||||
*
|
||||
* <p>方案依据:docx/mcp-data-tools-plan.md 第 4.2 / 5 / 9 / 13 / 15 章与附录 A.5。</p>
|
||||
* <p>权限来源:{@code OmsPurchaseOrderController#list} 的 {@code @RequiresPermissions("sip:purchaseorder:list")}(已实测抄取)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class PurchaseArrivalAggregateToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final Set<String> MODES = new LinkedHashSet<>(Arrays.asList("SUMMARY", "LIST"));
|
||||
private static final Set<String> GROUP_BYS = new LinkedHashSet<>(
|
||||
Arrays.asList("NONE", "ORDER", "VENDOR", "PRODUCT", "STATUS", "TIME_MONTH"));
|
||||
private static final int DEFAULT_TIME_MONTHS = 12;
|
||||
private static final int MAX_TIME_MONTHS = 36;
|
||||
private static final int DEFAULT_TOP_N = 10;
|
||||
private static final int MAX_TOP_N = 100;
|
||||
private static final int COUNT_CAP = 50000;
|
||||
|
||||
@Autowired
|
||||
private OmsPurchaseOrderMapper omsPurchaseOrderMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "purchase_arrival_aggregate";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "采购到货汇总(只读)。mode=SUMMARY(默认)按 group_by 分组一次算完,mode=LIST 按 purchase_no 游标分页。"
|
||||
+ "返回固定度量:purchaseQty 采购数量、innerQty 已入库数量、pendingQty 未入库数量、arrivalRate 到货率(%)、amountWithTax 含税金额、taxTotal 税额。"
|
||||
+ "分页查询工具:若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 has_more 为 false,否则数据不完整。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("mode", enumProperty("模式:SUMMARY 汇总(默认,不分页)/ LIST 明细(按 purchase_no 分页)", "SUMMARY", "LIST"));
|
||||
properties.put("group_by", enumProperty("SUMMARY 分组维度:NONE 总计(默认)/ ORDER 采购单号 / VENDOR 供应商 / PRODUCT 产品 / STATUS 采购状态 / TIME_MONTH 月份",
|
||||
"NONE", "ORDER", "VENDOR", "PRODUCT", "STATUS", "TIME_MONTH"));
|
||||
properties.put("purchase_no_list", arrayProperty("采购单号列表(≤200);mode=LIST 时与 vendor_code_list 至少提供一个"));
|
||||
properties.put("vendor_code_list", arrayProperty("制造商编码列表(≤200)"));
|
||||
properties.put("time_range", stringProperty("时间区间,格式 start~end(yyyy-MM-dd)。基于 purchase_date;默认近 12 个月,跨度上限 36 个月"));
|
||||
properties.put("top_n", intProperty("SUMMARY 返回前 N 组(默认 10,上限 100);仅 SUMMARY 可用"));
|
||||
properties.put("include_total", boolProperty("是否统计总条数(默认 false);LIST 时受 count_cap=50000 限制"));
|
||||
return withPagingProperties(properties, McpPages.MAX_AGG_PAGE_SIZE, McpPages.DEFAULT_PAGE_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
// 权限串来源:OmsPurchaseOrderController#list 的 @RequiresPermissions("sip:purchaseorder:list")
|
||||
McpAuth.requireAnyPermission("sip:purchaseorder:list");
|
||||
|
||||
String mode = McpParams.enumVal(params, "mode", MODES, "SUMMARY");
|
||||
String groupBy = McpParams.enumVal(params, "group_by", GROUP_BYS, "NONE");
|
||||
List<String> purchaseNoList = McpParams.codeList(params, "purchase_no_list", McpPages.MAX_CODE_LIST);
|
||||
List<String> vendorCodeList = McpParams.codeList(params, "vendor_code_list", McpPages.MAX_CODE_LIST);
|
||||
McpTimeRange timeRange = McpParams.timeRange(params, "time_range", DEFAULT_TIME_MONTHS, MAX_TIME_MONTHS);
|
||||
boolean includeTotal = McpParams.boolVal(params, "include_total", false);
|
||||
|
||||
boolean scopeRestricted = !inventoryAuthAll();
|
||||
List<String> authVendorCodes = scopeRestricted ? inventoryAuthVendorCodes() : Collections.emptyList();
|
||||
|
||||
if ("SUMMARY".equals(mode)) {
|
||||
McpParams.reject(McpParams.has(params, "cursor"), "mode=SUMMARY 不支持 cursor(SUMMARY 不分页)");
|
||||
McpParams.reject(McpParams.has(params, "page_size"), "mode=SUMMARY 不支持 page_size,请改用 top_n");
|
||||
} else {
|
||||
McpParams.reject(McpParams.has(params, "top_n"), "mode=LIST 不支持 top_n(top_n 仅 SUMMARY 可用)");
|
||||
if (purchaseNoList.isEmpty() && vendorCodeList.isEmpty()) {
|
||||
throw McpToolException.invalidParams("mode=LIST 需要提供 purchase_no_list 或 vendor_code_list 以收窄范围,避免全表扫描;"
|
||||
+ "如需全局统计请使用 mode=SUMMARY");
|
||||
}
|
||||
}
|
||||
|
||||
Integer topN = McpParams.intVal(params, "top_n", DEFAULT_TOP_N);
|
||||
if (topN < 1 || topN > MAX_TOP_N) {
|
||||
throw McpToolException.invalidParams("top_n 取值非法:" + topN + ",允许范围 1~" + MAX_TOP_N);
|
||||
}
|
||||
|
||||
String filterHash = McpParams.filterHash(
|
||||
mode,
|
||||
groupBy,
|
||||
sortedJoin(purchaseNoList),
|
||||
sortedJoin(vendorCodeList),
|
||||
McpParams.describe(timeRange),
|
||||
String.valueOf(topN),
|
||||
sortedJoin(authVendorCodes));
|
||||
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("mode", mode);
|
||||
query.put("group_by", groupBy);
|
||||
query.put("purchase_no_list", purchaseNoList);
|
||||
query.put("vendor_code_list", vendorCodeList);
|
||||
query.put("time_range", McpParams.describe(timeRange));
|
||||
query.put("top_n", "SUMMARY".equals(mode) ? topN : null);
|
||||
query.put("include_total", includeTotal);
|
||||
|
||||
if (scopeRestricted && authVendorCodes.isEmpty()) {
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(0, McpPages.DEFAULT_PAGE_SIZE, false, null,
|
||||
sortByFor(mode, groupBy), 1, null, false, false);
|
||||
return response(buildMetadata(), query, listData(Collections.emptyList(), pageInfo, null));
|
||||
}
|
||||
|
||||
return "SUMMARY".equals(mode)
|
||||
? handleSummary(groupBy, purchaseNoList, vendorCodeList, authVendorCodes, timeRange, topN, includeTotal, query)
|
||||
: handleList(params, purchaseNoList, vendorCodeList, authVendorCodes, timeRange, includeTotal, filterHash, query);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ SUMMARY
|
||||
|
||||
private Object handleSummary(String groupBy, List<String> purchaseNoList,
|
||||
List<String> vendorCodeList, List<String> authVendorCodes, McpTimeRange timeRange,
|
||||
Integer topN, boolean includeTotal, Map<String, Object> query) {
|
||||
int limit = "NONE".equals(groupBy) ? 1 : topN;
|
||||
List<Map<String, Object>> rows = omsPurchaseOrderMapper.selectPurchaseArrivalSummary(groupBy, purchaseNoList,
|
||||
vendorCodeList, authVendorCodes, timeRange.getStart(), timeRange.getEnd(), limit);
|
||||
if (rows == null) {
|
||||
rows = Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
items.add(toSummaryItem(groupBy, row));
|
||||
}
|
||||
|
||||
Long total = includeTotal ? (long) items.size() : null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), limit, false, null,
|
||||
sortByFor("SUMMARY", groupBy), 1, total, false, false);
|
||||
return response(buildMetadata(), query, listData(items, pageInfo, total));
|
||||
}
|
||||
|
||||
private Map<String, Object> toSummaryItem(String groupBy, Map<String, Object> row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
switch (groupBy) {
|
||||
case "ORDER":
|
||||
item.put("purchaseNo", McpFormat.trimToNull(stringValue(row.get("groupKey"))));
|
||||
break;
|
||||
case "VENDOR":
|
||||
item.put("vendorId", row.get("groupKey"));
|
||||
item.put("vendorCode", McpFormat.trimToNull(stringValue(row.get("vendorCode"))));
|
||||
item.put("vendorName", stringValue(row.get("vendorName")));
|
||||
break;
|
||||
case "PRODUCT":
|
||||
item.put("productCode", McpFormat.trimToNull(stringValue(row.get("groupKey"))));
|
||||
break;
|
||||
case "STATUS":
|
||||
Integer status = intValue(row.get("groupKey"));
|
||||
item.put("status", status);
|
||||
item.put("statusName", statusName(status));
|
||||
break;
|
||||
case "TIME_MONTH":
|
||||
item.put("timeBucket", stringValue(row.get("groupKey")));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
appendMetrics(item, row);
|
||||
return item;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ LIST
|
||||
|
||||
private Object handleList(Map<String, Object> params, List<String> purchaseNoList, List<String> vendorCodeList,
|
||||
List<String> authVendorCodes, McpTimeRange timeRange, boolean includeTotal,
|
||||
String filterHash, Map<String, Object> query) {
|
||||
int pageSize = McpPages.pageSize(params, McpPages.DEFAULT_PAGE_SIZE, McpPages.MAX_AGG_PAGE_SIZE);
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), filterHash, McpPages.DEFAULT_AGG_MAX_PAGES);
|
||||
int pageNo = cursor == null ? 1 : cursor.getP();
|
||||
query.put("page_size", pageSize);
|
||||
query.put("page_no", pageNo);
|
||||
|
||||
String lastPurchaseNo = cursor == null ? null : cursor.key(0);
|
||||
Long lastId = cursor == null ? null : cursor.longKey(1);
|
||||
|
||||
List<OmsPurchaseOrder> rows = omsPurchaseOrderMapper.selectPurchaseOrderPage(purchaseNoList, vendorCodeList,
|
||||
authVendorCodes, null, null, null, null, Collections.emptyList(),
|
||||
timeRange.getStart(), timeRange.getEnd(), lastPurchaseNo, lastId, pageSize + 1);
|
||||
boolean hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<OmsPurchaseOrder> page = McpPages.trim(rows, pageSize);
|
||||
|
||||
Map<String, BigDecimal[]> metricsMap = loadMetrics(page);
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (OmsPurchaseOrder order : page) {
|
||||
items.add(toListItem(order, metricsMap));
|
||||
}
|
||||
|
||||
OmsPurchaseOrder last = page.isEmpty() ? null : page.get(page.size() - 1);
|
||||
String nextCursor = hasMore
|
||||
? McpCursor.encode(getToolName(), filterHash, pageNo + 1,
|
||||
keysOf(last == null ? null : McpFormat.trim(last.getPurchaseNo()), last == null ? null : last.getId()))
|
||||
: null;
|
||||
|
||||
Long total = null;
|
||||
boolean capped = false;
|
||||
if (includeTotal) {
|
||||
List<OmsPurchaseOrder> all = omsPurchaseOrderMapper.selectPurchaseOrderPage(purchaseNoList, vendorCodeList,
|
||||
authVendorCodes, null, null, null, null, Collections.emptyList(),
|
||||
timeRange.getStart(), timeRange.getEnd(), null, null, COUNT_CAP + 1);
|
||||
capped = all.size() > COUNT_CAP;
|
||||
total = capped ? (long) COUNT_CAP : (long) all.size();
|
||||
}
|
||||
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), pageSize, hasMore, nextCursor,
|
||||
"purchase_no,id", pageNo, total, capped, false);
|
||||
return response(buildMetadata(), query, listData(items, pageInfo, total));
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal[]> loadMetrics(List<OmsPurchaseOrder> page) {
|
||||
List<String> purchaseNos = new ArrayList<>();
|
||||
for (OmsPurchaseOrder order : page) {
|
||||
String no = McpFormat.trim(order.getPurchaseNo());
|
||||
if (no != null) {
|
||||
purchaseNos.add(no);
|
||||
}
|
||||
}
|
||||
Map<String, BigDecimal[]> result = new LinkedHashMap<>();
|
||||
if (purchaseNos.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
List<OmsPurchaseOrderItem> itemList = omsPurchaseOrderMapper.listItemByCodeList(purchaseNos);
|
||||
if (itemList == null) {
|
||||
return result;
|
||||
}
|
||||
for (OmsPurchaseOrderItem detail : itemList) {
|
||||
String no = McpFormat.trim(detail.getPurchaseNo());
|
||||
if (no == null) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal[] sums = result.computeIfAbsent(no, key -> new BigDecimal[]{
|
||||
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO});
|
||||
sums[0] = sums[0].add(zeroIfNull(detail.getQuantity()));
|
||||
sums[1] = sums[1].add(zeroIfNull(detail.getInnerQuantity()));
|
||||
sums[2] = sums[2].add(zeroIfNull(detail.getAmountTotal()));
|
||||
sums[3] = sums[3].add(zeroIfNull(detail.getTaxTotal()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> toListItem(OmsPurchaseOrder order, Map<String, BigDecimal[]> metricsMap) {
|
||||
String no = McpFormat.trim(order.getPurchaseNo());
|
||||
BigDecimal[] sums = metricsMap.get(no);
|
||||
if (sums == null) {
|
||||
sums = new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO};
|
||||
}
|
||||
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("purchaseNo", no);
|
||||
item.put("buyerName", order.getBuyerName());
|
||||
item.put("vendorId", order.getVendorId());
|
||||
item.put("vendorCode", McpFormat.trim(order.getVendorCode()));
|
||||
item.put("vendorName", order.getVendorName());
|
||||
item.put("warehouseId", order.getWarehouseId());
|
||||
item.put("warehouseName", order.getWarehouseName());
|
||||
item.put("purchaserName", order.getPurchaserName());
|
||||
item.put("ownerName", order.getOwnerName());
|
||||
item.put("purchaseDate", McpFormat.date(order.getPurchaseDate()));
|
||||
item.put("status", order.getStatus());
|
||||
item.put("statusName", statusName(order.getStatus()));
|
||||
appendMetrics(item, sums[0], sums[1], sums[2], sums[3]);
|
||||
return item;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 度量
|
||||
|
||||
private void appendMetrics(Map<String, Object> item, Map<String, Object> row) {
|
||||
appendMetrics(item,
|
||||
toBigDecimal(row.get("purchaseQty")),
|
||||
toBigDecimal(row.get("innerQty")),
|
||||
toBigDecimal(row.get("amountWithTax")),
|
||||
toBigDecimal(row.get("taxTotal")));
|
||||
}
|
||||
|
||||
private void appendMetrics(Map<String, Object> item, BigDecimal purchaseQty, BigDecimal innerQty,
|
||||
BigDecimal amountWithTax, BigDecimal taxTotal) {
|
||||
BigDecimal pending = purchaseQty.subtract(innerQty);
|
||||
if (pending.compareTo(BigDecimal.ZERO) < 0) {
|
||||
pending = BigDecimal.ZERO;
|
||||
}
|
||||
item.put("purchaseQty", purchaseQty);
|
||||
item.put("innerQty", innerQty);
|
||||
item.put("pendingQty", pending);
|
||||
item.put("arrivalRate", arrivalRate(innerQty, purchaseQty));
|
||||
item.put("amountWithTax", amountWithTax);
|
||||
item.put("taxTotal", taxTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 到货率(%):innerQty / purchaseQty,HALF_UP 保留 2 位;分母为 0 返回 0。
|
||||
*/
|
||||
private BigDecimal arrivalRate(BigDecimal innerQty, BigDecimal purchaseQty) {
|
||||
if (purchaseQty == null || purchaseQty.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
return innerQty.multiply(new BigDecimal("100"))
|
||||
.divide(purchaseQty, 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private String sortByFor(String mode, String groupBy) {
|
||||
if ("LIST".equals(mode)) {
|
||||
return "purchase_no,id";
|
||||
}
|
||||
switch (groupBy) {
|
||||
case "TIME_MONTH":
|
||||
return "timeBucket asc";
|
||||
case "NONE":
|
||||
return "groupKey asc";
|
||||
case "ORDER":
|
||||
return "purchase_no (purchaseQty desc)";
|
||||
case "VENDOR":
|
||||
return "vendor_id (purchaseQty desc)";
|
||||
case "PRODUCT":
|
||||
return "product_code (purchaseQty desc)";
|
||||
case "STATUS":
|
||||
return "status (purchaseQty desc)";
|
||||
default:
|
||||
return "purchaseQty desc";
|
||||
}
|
||||
}
|
||||
|
||||
private String sortedJoin(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
List<String> sorted = new ArrayList<>(new TreeSet<>(values));
|
||||
return String.join(",", sorted);
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (value instanceof BigDecimal) {
|
||||
return (BigDecimal) value;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return new BigDecimal(value.toString());
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(String.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal zeroIfNull(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
|
||||
private Integer intValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(String.valueOf(value).trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private String statusName(Integer status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsPurchaseOrder.StatusEnum value : OmsPurchaseOrder.StatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getValue();
|
||||
}
|
||||
}
|
||||
return String.valueOf(status);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata() {
|
||||
Map<String, Object> metadata = metadata("采购到货汇总:oms_purchase_order ⋈ oms_purchase_order_item,按 group_by 动态分组(SUMMARY)或按 purchase_no 游标分页(LIST)。");
|
||||
metadata.put("query_fields", mapOf(
|
||||
"mode", "SUMMARY 汇总(默认)/ LIST 明细分页",
|
||||
"group_by", "SUMMARY 分组维度:NONE/ORDER/VENDOR/PRODUCT/STATUS/TIME_MONTH",
|
||||
"purchase_no_list", "采购单号列表(≤200)",
|
||||
"vendor_code_list", "制造商编码列表(≤200)",
|
||||
"time_range", "时间区间 start~end,基于 purchase_date;默认近12个月、上限36个月",
|
||||
"top_n", "SUMMARY 前 N 组(默认10,上限100)",
|
||||
"include_total", "是否统计总数(默认false,上限50000)",
|
||||
"page_size", "LIST 每页条数(默认20,上限200)",
|
||||
"cursor", "LIST 游标:回传上次的 page_info.next_cursor"));
|
||||
metadata.put("data_fields", mapOf(
|
||||
"total", "命中条数(include_total=true 时为真实值)",
|
||||
"items", "分组结果列表",
|
||||
"page_info", "分页信息:returned/page_size/has_more/next_cursor/sort_by/page_no/total_count"));
|
||||
metadata.put("item_fields", mapOf(
|
||||
"purchaseNo", "采购单号(group_by=ORDER 或 LIST)",
|
||||
"vendorId", "制造商ID(group_by=VENDOR 或 LIST)",
|
||||
"vendorCode", "制造商编码",
|
||||
"vendorName", "供应商名称",
|
||||
"productCode", "产品BOM编码(group_by=PRODUCT)",
|
||||
"status", "采购状态编码(group_by=STATUS 或 LIST;0待入库/1部分入库/2已完成)",
|
||||
"statusName", "采购状态名称",
|
||||
"timeBucket", "时间桶 yyyy-MM(group_by=TIME_MONTH)",
|
||||
"buyerName", "采购方名称(LIST)",
|
||||
"warehouseId", "入库仓库ID(LIST)",
|
||||
"warehouseName", "入库仓库名称(LIST)",
|
||||
"purchaserName", "采购员(LIST)",
|
||||
"ownerName", "汇智负责人(LIST)",
|
||||
"purchaseDate", "采购日期(LIST)",
|
||||
"purchaseQty", "采购数量(Σ item.quantity)",
|
||||
"innerQty", "已入库数量(Σ item.inner_quantity)",
|
||||
"pendingQty", "未入库数量(purchaseQty - innerQty,下限0)",
|
||||
"arrivalRate", "到货率(%,innerQty/purchaseQty,HALF_UP 保留2位;分母0返回0)",
|
||||
"amountWithTax", "含税金额(Σ item.amount_total)",
|
||||
"taxTotal", "税额(Σ item.tax_total)"));
|
||||
metadata.put("aggregation_rule", mapOf(
|
||||
"arrivalRate", "arrivalRate = innerQty / purchaseQty * 100,HALF_UP 保留 2 位百分比数值;分母为 0 时返回 0",
|
||||
"partial_inner", "“部分入库”按 oms_purchase_order.status = 1 判定(0待入库/1部分入库/2已完成)",
|
||||
"pendingQty", "未入库数量 = 采购数量 - 已入库数量(下限 0)",
|
||||
"time_field", "时间维度字段为 oms_purchase_order.purchase_date;时间条件一律区间下推(>= start and < end),不使用 date_format",
|
||||
"group_scope", "度量在采购明细粒度求和;group_by=NONE 返回单行总计;TIME_MONTH 以 purchase_date 月份为桶",
|
||||
"currency", "金额按原币返回,不自动换算"));
|
||||
metadata.put("data_scope", "回填 authVendorCodeList(= IInventoryAuthService.currentVendor() 的 vendor_code,与 OmsPurchaseOrderController 拼法一致)");
|
||||
metadata.put("notes", "mode=LIST 必须给出 purchase_no_list 或 vendor_code_list;若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 has_more 为 false,否则数据不完整。");
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,841 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.common.enums.ApproveStatusEnum;
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrder;
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrderHistory;
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrderItem;
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrderItemHistory;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpCursor;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpPages;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpTimeRange;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.OmsPurchaseOrderHistoryMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPurchaseOrderMapMapper;
|
||||
import com.ruoyi.sip.mapper.OmsPurchaseOrderMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* 采购域列表 / 范围查询 MCP 工具(只读,游标分页)。
|
||||
*
|
||||
* <p>entity 参数化:ORDER(采购单主表)/ ITEM(采购明细)/ HISTORY(采购单历史)/ ORDER_BIND(采购-订单绑定)。
|
||||
* 方案依据:docx/mcp-data-tools-plan.md 第 4.3 / 5 / 7 / 8 / 9 / 15 章与附录 A.7 / A.10。</p>
|
||||
*
|
||||
* <p>权限来源:{@code OmsPurchaseOrderController#list} 的 {@code @RequiresPermissions("sip:purchaseorder:list")}(已实测抄取)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class PurchaseListToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final Set<String> ENTITIES = new LinkedHashSet<>(
|
||||
Arrays.asList("ORDER", "ITEM", "HISTORY", "ORDER_BIND"));
|
||||
private static final Set<String> APPROVE_STATUS_VALUES = new LinkedHashSet<>(
|
||||
Arrays.asList("0", "1", "2", "3"));
|
||||
private static final Set<String> CONFIRM_STATUS_VALUES = new LinkedHashSet<>(
|
||||
Arrays.asList("0", "1", "2"));
|
||||
private static final Set<String> FLOW_TYPE_VALUES = new LinkedHashSet<>(
|
||||
Arrays.asList("online", "offline"));
|
||||
private static final int DEFAULT_TIME_MONTHS = 12;
|
||||
private static final int MAX_TIME_MONTHS = 36;
|
||||
private static final int COUNT_CAP = 50000;
|
||||
private static final int MAX_STATUS = 2;
|
||||
|
||||
@Autowired
|
||||
private OmsPurchaseOrderMapper omsPurchaseOrderMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsPurchaseOrderMapMapper omsPurchaseOrderMapMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsPurchaseOrderHistoryMapper omsPurchaseOrderHistoryMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "purchase_list";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "采购域列表/范围查询(只读)。entity=ORDER 采购单、ITEM 采购明细、HISTORY 采购单历史、ORDER_BIND 采购-订单绑定。"
|
||||
+ "支持按单号/供应商/状态/审批状态/确认状态/流程类型/产品/采购日期范围过滤,游标分页。"
|
||||
+ "分页查询工具:若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 has_more 为 false,否则数据不完整。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("entity", enumProperty("查询对象:ORDER 采购单 / ITEM 采购明细 / HISTORY 采购单历史 / ORDER_BIND 采购-订单绑定",
|
||||
"ORDER", "ITEM", "HISTORY", "ORDER_BIND"));
|
||||
properties.put("code_list", arrayProperty("单号/编码列表(≤200)。ORDER/ITEM/HISTORY 传采购单号 purchase_no;ORDER_BIND 传订单号 order_code"));
|
||||
properties.put("order_code_list", arrayProperty("订单号(合同编号)列表(≤200),仅 entity=ORDER_BIND 支持;entity=ORDER 请改用 entity=ORDER_BIND"));
|
||||
properties.put("vendor_code_list", arrayProperty("制造商编码列表(≤200),用于收窄供应商范围"));
|
||||
properties.put("status", intProperty("采购状态:0=待入库,1=部分入库,2=已完成"));
|
||||
properties.put("approve_status", stringProperty("审批状态:0=草稿/待提交,1=审批中/待审核,2=已通过,3=驳回"));
|
||||
properties.put("confirm_status", stringProperty("供应商确认状态:0=待确认,1=已确认,2=已驳回"));
|
||||
properties.put("flow_type", stringProperty("流程类型:online 线上 / offline 线下"));
|
||||
properties.put("product_code_list", arrayProperty("产品编码列表(≤200)"));
|
||||
properties.put("time_range", stringProperty("时间区间,格式 start~end(yyyy-MM-dd)。基于 purchase_date(HISTORY 基于 create_time);默认近 12 个月,跨度上限 36 个月"));
|
||||
properties.put("include_detail", boolProperty("是否返回明细行(默认 false)。ORDER 附采购明细,HISTORY 附历史明细"));
|
||||
properties.put("include_total", boolProperty("是否统计总条数(默认 false);true 时受 count_cap=50000 限制,超出置 total_count_capped=true"));
|
||||
return withPagingProperties(properties, McpPages.MAX_LIST_PAGE_SIZE, McpPages.DEFAULT_PAGE_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
// 权限串来源:OmsPurchaseOrderController#list / item/list / vendor/list 的 @RequiresPermissions("sip:purchaseorder:list")
|
||||
McpAuth.requireAnyPermission("sip:purchaseorder:list");
|
||||
|
||||
String entity = McpParams.requireEnum(params, "entity", ENTITIES);
|
||||
List<String> codeList = McpParams.codeList(params, "code_list", McpPages.MAX_CODE_LIST);
|
||||
List<String> orderCodeList = McpParams.codeList(params, "order_code_list", McpPages.MAX_CODE_LIST);
|
||||
List<String> vendorCodeList = McpParams.codeList(params, "vendor_code_list", McpPages.MAX_CODE_LIST);
|
||||
List<String> productCodeList = McpParams.codeList(params, "product_code_list", McpPages.MAX_CODE_LIST);
|
||||
|
||||
if (!"ORDER_BIND".equals(entity) && !orderCodeList.isEmpty()) {
|
||||
throw McpToolException.invalidParams("order_code_list 仅支持 entity=ORDER_BIND;"
|
||||
+ "order_id/purchase_id 无索引,禁止以订单号作为 entity=ORDER 的过滤入口,请改用 entity=ORDER_BIND");
|
||||
}
|
||||
|
||||
Integer status = McpParams.intVal(params, "status");
|
||||
if (status != null && (status < 0 || status > MAX_STATUS)) {
|
||||
throw McpToolException.invalidParams("status 取值非法:" + status + ",允许值:0=待入库,1=部分入库,2=已完成");
|
||||
}
|
||||
String approveStatus = McpParams.enumVal(params, "approve_status", APPROVE_STATUS_VALUES, null);
|
||||
String confirmStatus = McpParams.enumVal(params, "confirm_status", CONFIRM_STATUS_VALUES, null);
|
||||
String flowType = resolveFlowType(params);
|
||||
McpTimeRange timeRange = McpParams.timeRange(params, "time_range", DEFAULT_TIME_MONTHS, MAX_TIME_MONTHS);
|
||||
|
||||
int pageSize = McpPages.pageSize(params, McpPages.DEFAULT_PAGE_SIZE, McpPages.MAX_LIST_PAGE_SIZE);
|
||||
boolean includeDetail = McpParams.boolVal(params, "include_detail", false);
|
||||
boolean includeTotal = McpParams.boolVal(params, "include_total", false);
|
||||
|
||||
boolean scopeRestricted = !inventoryAuthAll();
|
||||
List<String> authVendorCodes = scopeRestricted ? inventoryAuthVendorCodes() : Collections.emptyList();
|
||||
|
||||
String filterHash = McpParams.filterHash(
|
||||
entity,
|
||||
sortedJoin(codeList),
|
||||
sortedJoin(orderCodeList),
|
||||
sortedJoin(vendorCodeList),
|
||||
sortedJoin(productCodeList),
|
||||
status == null ? null : String.valueOf(status),
|
||||
approveStatus,
|
||||
confirmStatus,
|
||||
flowType,
|
||||
McpParams.describe(timeRange),
|
||||
String.valueOf(includeDetail),
|
||||
sortedJoin(authVendorCodes));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
int pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> query = buildQuery(entity, codeList, orderCodeList, vendorCodeList, productCodeList,
|
||||
status, approveStatus, confirmStatus, flowType, timeRange, pageSize, includeDetail, includeTotal, pageNo);
|
||||
|
||||
// 数据范围收窄为"无任何授权供应商":与 OmsPurchaseOrderController 一致,直接返回空集(不泄露全量数据)
|
||||
if (scopeRestricted && authVendorCodes.isEmpty() && !"ORDER_BIND".equals(entity)) {
|
||||
return emptyResponse(query);
|
||||
}
|
||||
|
||||
switch (entity) {
|
||||
case "ORDER":
|
||||
return handleOrder(codeList, vendorCodeList, authVendorCodes, status, approveStatus, confirmStatus,
|
||||
flowType, productCodeList, timeRange, pageSize, includeDetail, includeTotal, cursor, pageNo, filterHash, query);
|
||||
case "ITEM":
|
||||
return handleItem(codeList, vendorCodeList, authVendorCodes, status, approveStatus, confirmStatus,
|
||||
flowType, productCodeList, timeRange, pageSize, includeTotal, cursor, pageNo, filterHash, query);
|
||||
case "HISTORY":
|
||||
return handleHistory(codeList, status, approveStatus, confirmStatus, flowType, timeRange,
|
||||
pageSize, includeDetail, includeTotal, cursor, pageNo, filterHash, query);
|
||||
default:
|
||||
return handleOrderBind(codeList, orderCodeList, timeRange, pageSize, includeTotal, cursor, pageNo, filterHash, query);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ entity 分支
|
||||
|
||||
private Object handleOrder(List<String> codeList, List<String> vendorCodeList, List<String> authVendorCodes,
|
||||
Integer status, String approveStatus, String confirmStatus, String flowType,
|
||||
List<String> productCodeList, McpTimeRange timeRange, int pageSize, boolean includeDetail,
|
||||
boolean includeTotal, McpCursor cursor, int pageNo, String filterHash, Map<String, Object> query) {
|
||||
String lastPurchaseNo = cursor == null ? null : cursor.key(0);
|
||||
Long lastId = cursor == null ? null : cursor.longKey(1);
|
||||
|
||||
List<OmsPurchaseOrder> rows = omsPurchaseOrderMapper.selectPurchaseOrderPage(codeList, vendorCodeList,
|
||||
authVendorCodes, status, approveStatus, confirmStatus, flowType, productCodeList,
|
||||
timeRange.getStart(), timeRange.getEnd(), lastPurchaseNo, lastId, pageSize + 1);
|
||||
boolean hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<OmsPurchaseOrder> page = McpPages.trim(rows, pageSize);
|
||||
|
||||
Map<String, List<OmsPurchaseOrderItem>> detailMap = includeDetail ? loadOrderDetails(page) : Collections.emptyMap();
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (OmsPurchaseOrder order : page) {
|
||||
List<OmsPurchaseOrderItem> details = includeDetail
|
||||
? detailMap.getOrDefault(McpFormat.trim(order.getPurchaseNo()), Collections.emptyList())
|
||||
: null;
|
||||
items.add(toOrderItem(order, details));
|
||||
}
|
||||
|
||||
String nextCursor = nextCursor(hasMore, page.isEmpty() ? null : page.get(page.size() - 1).getPurchaseNo(),
|
||||
page.isEmpty() ? null : page.get(page.size() - 1).getId(), pageNo, filterHash);
|
||||
CountResult count = includeTotal
|
||||
? countOrders(codeList, vendorCodeList, authVendorCodes, status, approveStatus, confirmStatus, flowType, productCodeList, timeRange)
|
||||
: null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), pageSize, hasMore, nextCursor,
|
||||
"purchase_no,id", pageNo, count == null ? null : count.total, count != null && count.capped, false);
|
||||
return response(buildMetadata(), query, listData(items, pageInfo, count == null ? null : count.total));
|
||||
}
|
||||
|
||||
private Object handleItem(List<String> codeList, List<String> vendorCodeList, List<String> authVendorCodes,
|
||||
Integer status, String approveStatus, String confirmStatus, String flowType,
|
||||
List<String> productCodeList, McpTimeRange timeRange, int pageSize, boolean includeTotal,
|
||||
McpCursor cursor, int pageNo, String filterHash, Map<String, Object> query) {
|
||||
String lastPurchaseNo = cursor == null ? null : cursor.key(0);
|
||||
Long lastId = cursor == null ? null : cursor.longKey(1);
|
||||
|
||||
List<OmsPurchaseOrderItem> rows = omsPurchaseOrderMapper.selectPurchaseOrderItemPage(codeList, vendorCodeList,
|
||||
authVendorCodes, status, approveStatus, confirmStatus, flowType, productCodeList,
|
||||
timeRange.getStart(), timeRange.getEnd(), lastPurchaseNo, lastId, pageSize + 1);
|
||||
boolean hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<OmsPurchaseOrderItem> page = McpPages.trim(rows, pageSize);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (OmsPurchaseOrderItem row : page) {
|
||||
items.add(toItemRow(row));
|
||||
}
|
||||
|
||||
OmsPurchaseOrderItem last = page.isEmpty() ? null : page.get(page.size() - 1);
|
||||
String nextCursor = nextCursor(hasMore, last == null ? null : last.getPurchaseNo(),
|
||||
last == null ? null : last.getId(), pageNo, filterHash);
|
||||
CountResult count = includeTotal
|
||||
? countItems(codeList, vendorCodeList, authVendorCodes, status, approveStatus, confirmStatus, flowType, productCodeList, timeRange)
|
||||
: null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), pageSize, hasMore, nextCursor,
|
||||
"purchase_no,id", pageNo, count == null ? null : count.total, count != null && count.capped, false);
|
||||
return response(buildMetadata(), query, listData(items, pageInfo, count == null ? null : count.total));
|
||||
}
|
||||
|
||||
private Object handleHistory(List<String> codeList, Integer status, String approveStatus, String confirmStatus,
|
||||
String flowType, McpTimeRange timeRange, int pageSize, boolean includeDetail,
|
||||
boolean includeTotal, McpCursor cursor, int pageNo, String filterHash, Map<String, Object> query) {
|
||||
String lastPurchaseNo = cursor == null ? null : cursor.key(0);
|
||||
Long lastId = cursor == null ? null : cursor.longKey(1);
|
||||
|
||||
List<OmsPurchaseOrderHistory> rows = omsPurchaseOrderHistoryMapper.listByPurchaseNoList(codeList, status,
|
||||
approveStatus, confirmStatus, flowType, timeRange.getStart(), timeRange.getEnd(),
|
||||
lastPurchaseNo, lastId, pageSize + 1);
|
||||
boolean hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<OmsPurchaseOrderHistory> page = McpPages.trim(rows, pageSize);
|
||||
|
||||
Map<Long, List<OmsPurchaseOrderItemHistory>> detailMap = includeDetail
|
||||
? loadHistoryDetails(page) : Collections.emptyMap();
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (OmsPurchaseOrderHistory history : page) {
|
||||
List<OmsPurchaseOrderItemHistory> details = includeDetail
|
||||
? detailMap.getOrDefault(history.getId(), Collections.emptyList()) : null;
|
||||
items.add(toHistoryItem(history, details));
|
||||
}
|
||||
|
||||
OmsPurchaseOrderHistory last = page.isEmpty() ? null : page.get(page.size() - 1);
|
||||
String nextCursor = nextCursor(hasMore, last == null ? null : last.getPurchaseNo(),
|
||||
last == null ? null : last.getId(), pageNo, filterHash);
|
||||
CountResult count = includeTotal
|
||||
? countHistory(codeList, status, approveStatus, confirmStatus, flowType, timeRange) : null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), pageSize, hasMore, nextCursor,
|
||||
"purchase_no,id", pageNo, count == null ? null : count.total, count != null && count.capped, false);
|
||||
return response(buildMetadata(), query, listData(items, pageInfo, count == null ? null : count.total));
|
||||
}
|
||||
|
||||
private Object handleOrderBind(List<String> codeList, List<String> orderCodeList, McpTimeRange timeRange,
|
||||
int pageSize, boolean includeTotal, McpCursor cursor, int pageNo,
|
||||
String filterHash, Map<String, Object> query) {
|
||||
// ORDER_BIND:code_list 与 order_code_list 均按"订单号"处理
|
||||
Set<String> merged = new LinkedHashSet<>();
|
||||
merged.addAll(orderCodeList);
|
||||
merged.addAll(codeList);
|
||||
List<String> orderCodes = new ArrayList<>(merged);
|
||||
|
||||
Long lastOrderId = cursor == null ? null : cursor.longKey(0);
|
||||
Long lastPurchaseId = cursor == null ? null : cursor.longKey(1);
|
||||
Long lastId = cursor == null ? null : cursor.longKey(2);
|
||||
|
||||
List<Map<String, Object>> rows = omsPurchaseOrderMapMapper.listByOrderCodeList(orderCodes,
|
||||
lastOrderId, lastPurchaseId, lastId, pageSize + 1);
|
||||
boolean hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<Map<String, Object>> page = McpPages.trim(rows, pageSize);
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (Map<String, Object> row : page) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("orderId", row.get("orderId"));
|
||||
item.put("orderCode", McpFormat.trimToNull(row.get("orderCode") == null ? null : String.valueOf(row.get("orderCode"))));
|
||||
item.put("purchaseId", row.get("purchaseId"));
|
||||
item.put("purchaseNo", McpFormat.trimToNull(row.get("purchaseNo") == null ? null : String.valueOf(row.get("purchaseNo"))));
|
||||
item.put("productCode", McpFormat.trimToNull(row.get("productCode") == null ? null : String.valueOf(row.get("productCode"))));
|
||||
item.put("bindNum", row.get("bindNum"));
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
Map<String, Object> last = page.isEmpty() ? null : page.get(page.size() - 1);
|
||||
List<String> keys = last == null ? null : keysOf(last.get("orderId"), last.get("purchaseId"), last.get("id"));
|
||||
String nextCursor = hasMore ? McpCursor.encode(getToolName(), filterHash, pageNo + 1, keys) : null;
|
||||
CountResult count = includeTotal ? countOrderBind(orderCodes) : null;
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(items.size(), pageSize, hasMore, nextCursor,
|
||||
"order_id,purchase_id,id", pageNo, count == null ? null : count.total, count != null && count.capped, false);
|
||||
return response(buildMetadata(), query, listData(items, pageInfo, count == null ? null : count.total));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 明细批量装载
|
||||
|
||||
private Map<String, List<OmsPurchaseOrderItem>> loadOrderDetails(List<OmsPurchaseOrder> page) {
|
||||
List<String> purchaseNos = new ArrayList<>();
|
||||
for (OmsPurchaseOrder order : page) {
|
||||
String no = McpFormat.trim(order.getPurchaseNo());
|
||||
if (no != null) {
|
||||
purchaseNos.add(no);
|
||||
}
|
||||
}
|
||||
if (purchaseNos.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<OmsPurchaseOrderItem> itemList = omsPurchaseOrderMapper.listItemByCodeList(purchaseNos);
|
||||
if (itemList == null || itemList.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, List<OmsPurchaseOrderItem>> result = new LinkedHashMap<>();
|
||||
for (OmsPurchaseOrderItem item : itemList) {
|
||||
String no = McpFormat.trim(item.getPurchaseNo());
|
||||
if (no == null) {
|
||||
continue;
|
||||
}
|
||||
result.computeIfAbsent(no, key -> new ArrayList<>()).add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Long, List<OmsPurchaseOrderItemHistory>> loadHistoryDetails(List<OmsPurchaseOrderHistory> page) {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (OmsPurchaseOrderHistory history : page) {
|
||||
if (history.getId() != null) {
|
||||
ids.add(history.getId());
|
||||
}
|
||||
}
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<OmsPurchaseOrderItemHistory> itemList = omsPurchaseOrderHistoryMapper.listItemByPurchaseHistoryIdList(ids);
|
||||
if (itemList == null || itemList.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, List<OmsPurchaseOrderItemHistory>> result = new LinkedHashMap<>();
|
||||
for (OmsPurchaseOrderItemHistory item : itemList) {
|
||||
if (item.getPurchaseHistoryId() == null) {
|
||||
continue;
|
||||
}
|
||||
result.computeIfAbsent(item.getPurchaseHistoryId(), key -> new ArrayList<>()).add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 行 → 输出
|
||||
|
||||
private Map<String, Object> toOrderItem(OmsPurchaseOrder order, List<OmsPurchaseOrderItem> details) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", order.getId());
|
||||
item.put("purchaseNo", McpFormat.trim(order.getPurchaseNo()));
|
||||
item.put("buyerName", order.getBuyerName());
|
||||
item.put("buyerAddress", order.getBuyerAddress());
|
||||
item.put("vendorId", order.getVendorId());
|
||||
item.put("vendorCode", McpFormat.trim(order.getVendorCode()));
|
||||
item.put("vendorName", order.getVendorName());
|
||||
item.put("vendorAddress", order.getVendorAddress());
|
||||
item.put("vendorUser", order.getVendorUser());
|
||||
item.put("vendorPhone", order.getVendorPhone());
|
||||
item.put("taxRate", order.getTaxRate());
|
||||
item.put("currency", order.getCurrency());
|
||||
item.put("purchaserId", order.getPurchaserId());
|
||||
item.put("purchaserName", order.getPurchaserName());
|
||||
item.put("purchaserMobile", order.getPurchaserMobile());
|
||||
item.put("purchaserEmail", order.getPurchaserEmail());
|
||||
item.put("warehouseId", order.getWarehouseId());
|
||||
item.put("warehouseName", order.getWarehouseName());
|
||||
item.put("payMethod", order.getPayMethod());
|
||||
item.put("payMethodName", payMethodName(order.getPayMethod()));
|
||||
item.put("ownerId", order.getOwnerId());
|
||||
item.put("ownerName", order.getOwnerName());
|
||||
item.put("totalAmount", order.getTotalAmount());
|
||||
item.put("status", order.getStatus());
|
||||
item.put("statusName", statusName(order.getStatus()));
|
||||
item.put("approveStatus", order.getApproveStatus());
|
||||
item.put("approveStatusName", approveStatusName(order.getApproveStatus()));
|
||||
item.put("approveTime", McpFormat.dateTime(order.getApproveTime()));
|
||||
item.put("approveNode", order.getApproveNode());
|
||||
item.put("confirmStatus", order.getConfirmStatus());
|
||||
item.put("confirmStatusName", confirmStatusName(order.getConfirmStatus()));
|
||||
item.put("purchaseDate", McpFormat.date(order.getPurchaseDate()));
|
||||
item.put("flowType", order.getFlowType());
|
||||
item.put("flowTypeName", flowTypeName(order.getFlowType()));
|
||||
item.put("isVirtual", order.getIsVirtual());
|
||||
item.put("version", order.getVersion());
|
||||
item.put("remark", order.getRemark());
|
||||
if (details != null) {
|
||||
List<Map<String, Object>> detailItems = new ArrayList<>();
|
||||
for (OmsPurchaseOrderItem detail : details) {
|
||||
detailItems.add(toItemRow(detail));
|
||||
}
|
||||
item.put("items", detailItems);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toItemRow(OmsPurchaseOrderItem row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("purchaseId", row.getPurchaseId());
|
||||
item.put("purchaseNo", McpFormat.trim(row.getPurchaseNo()));
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("productModel", row.getProductModel());
|
||||
item.put("productType", row.getProductType());
|
||||
item.put("productDescription", row.getProductDescription());
|
||||
item.put("quantity", row.getQuantity());
|
||||
item.put("innerQuantity", row.getInnerQuantity());
|
||||
item.put("pendingQuantity", pendingQuantity(row.getQuantity(), row.getInnerQuantity()));
|
||||
item.put("price", row.getPrice());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
item.put("taxTotal", row.getTaxTotal());
|
||||
item.put("amountTotal", row.getAmountTotal());
|
||||
item.put("deliveryDate", McpFormat.date(row.getDeliveryDate()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toHistoryItem(OmsPurchaseOrderHistory history, List<OmsPurchaseOrderItemHistory> details) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", history.getId());
|
||||
item.put("purchaseId", history.getPurchaseId());
|
||||
item.put("purchaseNo", McpFormat.trim(history.getPurchaseNo()));
|
||||
item.put("version", history.getVersion());
|
||||
item.put("buyerName", history.getBuyerName());
|
||||
item.put("buyerAddress", history.getBuyerAddress());
|
||||
item.put("vendorId", history.getVendorId());
|
||||
item.put("vendorName", history.getVendorName());
|
||||
item.put("vendorUser", history.getVendorUser());
|
||||
item.put("vendorPhone", history.getVendorPhone());
|
||||
item.put("currency", history.getCurrency());
|
||||
item.put("purchaserId", history.getPurchaserId());
|
||||
item.put("purchaserName", history.getPurchaserName());
|
||||
item.put("purchaserMobile", history.getPurchaserMobile());
|
||||
item.put("purchaserEmail", history.getPurchaserEmail());
|
||||
item.put("warehouseId", history.getWarehouseId());
|
||||
item.put("payMethod", history.getPayMethod());
|
||||
item.put("payMethodName", payMethodName(history.getPayMethod()));
|
||||
item.put("ownerId", history.getOwnerId());
|
||||
item.put("ownerName", history.getOwnerName());
|
||||
item.put("totalAmount", history.getTotalAmount());
|
||||
item.put("status", history.getStatus());
|
||||
item.put("statusName", statusName(history.getStatus()));
|
||||
String approveStatus = history.getApproveStatus() == null ? null : String.valueOf(history.getApproveStatus());
|
||||
item.put("approveStatus", approveStatus);
|
||||
item.put("approveStatusName", approveStatusName(approveStatus));
|
||||
item.put("approveTime", McpFormat.dateTime(history.getApproveTime()));
|
||||
item.put("approveNode", history.getApproveNode());
|
||||
item.put("confirmStatus", history.getConfirmStatus());
|
||||
item.put("confirmStatusName", confirmStatusName(history.getConfirmStatus()));
|
||||
item.put("flowType", history.getFlowType());
|
||||
item.put("flowTypeName", flowTypeName(history.getFlowType()));
|
||||
item.put("isVirtual", history.getIsVirtual());
|
||||
item.put("createTime", McpFormat.dateTime(history.getCreateTime()));
|
||||
item.put("updateTime", McpFormat.dateTime(history.getUpdateTime()));
|
||||
item.put("remark", history.getRemark());
|
||||
if (details != null) {
|
||||
List<Map<String, Object>> detailItems = new ArrayList<>();
|
||||
for (OmsPurchaseOrderItemHistory detail : details) {
|
||||
detailItems.add(toHistoryDetailRow(detail));
|
||||
}
|
||||
item.put("items", detailItems);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toHistoryDetailRow(OmsPurchaseOrderItemHistory row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("purchaseHistoryId", row.getPurchaseHistoryId());
|
||||
item.put("originalPurchaseId", row.getOriginalPurchaseId());
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("productModel", row.getProductModel());
|
||||
item.put("productType", row.getProductType());
|
||||
item.put("productDescription", row.getProductDescription());
|
||||
item.put("quantity", row.getQuantity());
|
||||
item.put("price", row.getPrice());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
item.put("taxTotal", row.getTaxTotal());
|
||||
item.put("amountTotal", row.getAmountTotal());
|
||||
item.put("deliveryDate", McpFormat.date(row.getDeliveryDate()));
|
||||
item.put("innerStatus", row.getInnerStatus());
|
||||
return item;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 计数(include_total,上限 count_cap)
|
||||
|
||||
private CountResult countOrders(List<String> codeList, List<String> vendorCodeList, List<String> authVendorCodes,
|
||||
Integer status, String approveStatus, String confirmStatus, String flowType,
|
||||
List<String> productCodeList, McpTimeRange timeRange) {
|
||||
List<OmsPurchaseOrder> rows = omsPurchaseOrderMapper.selectPurchaseOrderPage(codeList, vendorCodeList,
|
||||
authVendorCodes, status, approveStatus, confirmStatus, flowType, productCodeList,
|
||||
timeRange.getStart(), timeRange.getEnd(), null, null, COUNT_CAP + 1);
|
||||
return CountResult.of(rows.size());
|
||||
}
|
||||
|
||||
private CountResult countItems(List<String> codeList, List<String> vendorCodeList, List<String> authVendorCodes,
|
||||
Integer status, String approveStatus, String confirmStatus, String flowType,
|
||||
List<String> productCodeList, McpTimeRange timeRange) {
|
||||
List<OmsPurchaseOrderItem> rows = omsPurchaseOrderMapper.selectPurchaseOrderItemPage(codeList, vendorCodeList,
|
||||
authVendorCodes, status, approveStatus, confirmStatus, flowType, productCodeList,
|
||||
timeRange.getStart(), timeRange.getEnd(), null, null, COUNT_CAP + 1);
|
||||
return CountResult.of(rows.size());
|
||||
}
|
||||
|
||||
private CountResult countHistory(List<String> codeList, Integer status, String approveStatus, String confirmStatus,
|
||||
String flowType, McpTimeRange timeRange) {
|
||||
List<OmsPurchaseOrderHistory> rows = omsPurchaseOrderHistoryMapper.listByPurchaseNoList(codeList, status,
|
||||
approveStatus, confirmStatus, flowType, timeRange.getStart(), timeRange.getEnd(), null, null, COUNT_CAP + 1);
|
||||
return CountResult.of(rows.size());
|
||||
}
|
||||
|
||||
private CountResult countOrderBind(List<String> orderCodes) {
|
||||
List<Map<String, Object>> rows = omsPurchaseOrderMapMapper.listByOrderCodeList(orderCodes, null, null, null, COUNT_CAP + 1);
|
||||
return CountResult.of(rows.size());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private String resolveFlowType(Map<String, Object> params) {
|
||||
String value = McpParams.str(params, "flow_type");
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = value.toLowerCase();
|
||||
if (!FLOW_TYPE_VALUES.contains(normalized)) {
|
||||
throw McpToolException.invalidParams("flow_type 取值非法:" + value + ",允许值:" + FLOW_TYPE_VALUES);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String nextCursor(boolean hasMore, String purchaseNo, Long id, int pageNo, String filterHash) {
|
||||
if (!hasMore) {
|
||||
return null;
|
||||
}
|
||||
return McpCursor.encode(getToolName(), filterHash, pageNo + 1, keysOf(McpFormat.trim(purchaseNo), id));
|
||||
}
|
||||
|
||||
private String sortedJoin(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
List<String> sorted = new ArrayList<>(new TreeSet<>(values));
|
||||
return String.join(",", sorted);
|
||||
}
|
||||
|
||||
private BigDecimal pendingQuantity(BigDecimal quantity, BigDecimal innerQuantity) {
|
||||
BigDecimal purchase = quantity == null ? BigDecimal.ZERO : quantity;
|
||||
BigDecimal inner = innerQuantity == null ? BigDecimal.ZERO : innerQuantity;
|
||||
BigDecimal pending = purchase.subtract(inner);
|
||||
return pending.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : pending;
|
||||
}
|
||||
|
||||
private String statusName(Integer status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsPurchaseOrder.StatusEnum value : OmsPurchaseOrder.StatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getValue();
|
||||
}
|
||||
}
|
||||
return String.valueOf(status);
|
||||
}
|
||||
|
||||
private String approveStatusName(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (ApproveStatusEnum value : ApproveStatusEnum.values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value.getValue();
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private String confirmStatusName(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsPurchaseOrder.ConfirmStatusEnum value : OmsPurchaseOrder.ConfirmStatusEnum.values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value.getValue();
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private String flowTypeName(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
for (OmsPurchaseOrder.FlowTypeEnum value : OmsPurchaseOrder.FlowTypeEnum.values()) {
|
||||
if (value.getCode().equals(code)) {
|
||||
return value.getValue();
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private String payMethodName(String code) {
|
||||
if (code == null) {
|
||||
return null;
|
||||
}
|
||||
if ("1".equals(code)) {
|
||||
return "出库付款";
|
||||
}
|
||||
if ("0".equals(code) || "2".equals(code)) {
|
||||
return "入库付款";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildQuery(String entity, List<String> codeList, List<String> orderCodeList,
|
||||
List<String> vendorCodeList, List<String> productCodeList, Integer status,
|
||||
String approveStatus, String confirmStatus, String flowType,
|
||||
McpTimeRange timeRange, int pageSize, boolean includeDetail,
|
||||
boolean includeTotal, int pageNo) {
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("entity", entity);
|
||||
query.put("code_list", codeList);
|
||||
query.put("order_code_list", orderCodeList);
|
||||
query.put("vendor_code_list", vendorCodeList);
|
||||
query.put("product_code_list", productCodeList);
|
||||
query.put("status", status);
|
||||
query.put("approve_status", approveStatus);
|
||||
query.put("confirm_status", confirmStatus);
|
||||
query.put("flow_type", flowType);
|
||||
query.put("time_range", McpParams.describe(timeRange));
|
||||
query.put("page_size", pageSize);
|
||||
query.put("page_no", pageNo);
|
||||
query.put("include_detail", includeDetail);
|
||||
query.put("include_total", includeTotal);
|
||||
return query;
|
||||
}
|
||||
|
||||
private Object emptyResponse(Map<String, Object> query) {
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(0, McpPages.DEFAULT_PAGE_SIZE, false, null, "purchase_no,id", 1, null, false, false);
|
||||
return response(buildMetadata(), query, listData(Collections.emptyList(), pageInfo, null));
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata() {
|
||||
Map<String, Object> metadata = metadata("采购域列表/范围查询:entity=ORDER 采购单、ITEM 采购明细、HISTORY 采购单历史、ORDER_BIND 采购-订单绑定;"
|
||||
+ "按 purchase_no,id(ORDER/ITEM/HISTORY)或 order_id,purchase_id,id(ORDER_BIND)游标分页。");
|
||||
metadata.put("query_fields", mapOf(
|
||||
"entity", "查询对象:ORDER/ITEM/HISTORY/ORDER_BIND",
|
||||
"code_list", "单号列表:ORDER/ITEM/HISTORY 传 purchase_no,ORDER_BIND 传 order_code",
|
||||
"order_code_list", "订单号(合同编号)列表,仅 entity=ORDER_BIND 支持",
|
||||
"vendor_code_list", "制造商编码列表",
|
||||
"status", "采购状态(0待入库/1部分入库/2已完成)",
|
||||
"approve_status", "审批状态(0草稿/1审批中/2已通过/3驳回)",
|
||||
"confirm_status", "供应商确认状态(0待确认/1已确认/2已驳回)",
|
||||
"flow_type", "流程类型(online线上/offline线下)",
|
||||
"product_code_list", "产品编码列表",
|
||||
"time_range", "时间区间 start~end;ORDER/ITEM 基于 purchase_date,HISTORY 基于 create_time;默认近12个月、上限36个月",
|
||||
"include_detail", "是否返回明细行(默认false)",
|
||||
"include_total", "是否统计总数(默认false,上限50000)",
|
||||
"page_size", "每页条数(默认20,上限100)",
|
||||
"cursor", "游标:回传上次的 page_info.next_cursor"));
|
||||
metadata.put("data_fields", mapOf(
|
||||
"total", "命中条数(include_total=true 时为真实值,否则为本页条数)",
|
||||
"items", "结果列表",
|
||||
"page_info", "分页信息:returned/page_size/has_more/next_cursor/sort_by/page_no/total_count/total_count_capped"));
|
||||
metadata.put("item_fields", buildItemFieldMetadata());
|
||||
metadata.put("dict_fields", mapOf(
|
||||
"statusName", "枚举 OmsPurchaseOrder.StatusEnum(0待入库/1部分入库/2已完成)",
|
||||
"approveStatusName", "枚举 com.ruoyi.common.enums.ApproveStatusEnum(0待提交/1待审核/2审批完成/3已驳回;本工具入参文档写作 草稿/审批中/已通过/驳回)",
|
||||
"confirmStatusName", "枚举 OmsPurchaseOrder.ConfirmStatusEnum(0待确认/1已确认/2已驳回)",
|
||||
"flowTypeName", "枚举 OmsPurchaseOrder.FlowTypeEnum(online线上/offline线下)",
|
||||
"payMethodName", "字典编码映射(0/2=入库付款,1=出库付款,来源 OmsPurchaseOrder#payMethod 的 readConverterExp)"));
|
||||
metadata.put("sort_by", mapOf(
|
||||
"ORDER", "purchase_no,id",
|
||||
"ITEM", "purchase_no,id",
|
||||
"HISTORY", "purchase_no,id",
|
||||
"ORDER_BIND", "order_id,purchase_id,id"));
|
||||
metadata.put("aggregation_rule", mapOf(
|
||||
"order_bind_scope", "entity=ORDER_BIND 的表 oms_purchase_order_map 无 order_id 索引,仅适用于小数据量(实测 0/少量行)",
|
||||
"order_filter_guard", "禁止用 order_code_list 过滤 entity=ORDER(order_id/purchase_id 无索引会全表扫描),请改用 entity=ORDER_BIND",
|
||||
"del_flag", "按 del_flag 过滤逻辑删除(del_flag=0 或 is null 视为有效)",
|
||||
"time_pushdown", "时间条件一律区间下推(>= start and < end),不使用 date_format",
|
||||
"case_insensitive", "编码匹配不区分大小写(utf8mb4_unicode_ci),返回编码统一 trim"));
|
||||
metadata.put("data_scope", mapOf(
|
||||
"order_item_history", "回填 authVendorCodeList(= IInventoryAuthService.currentVendor() 的 vendor_code,与 OmsPurchaseOrderController 拼法一致)",
|
||||
"no_scope", "ORDER_BIND 无供应商维度数据范围"));
|
||||
metadata.put("notes", "若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 继续调用直到 has_more 为 false,否则数据不完整。");
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildItemFieldMetadata() {
|
||||
Map<String, Object> fields = new LinkedHashMap<>();
|
||||
|
||||
Map<String, Object> orderFields = new LinkedHashMap<>();
|
||||
orderFields.put("id", "采购单ID");
|
||||
orderFields.put("purchaseNo", "采购单号");
|
||||
orderFields.put("buyerName", "采购方名称");
|
||||
orderFields.put("buyerAddress", "采购方地址");
|
||||
orderFields.put("vendorId", "制造商ID");
|
||||
orderFields.put("vendorCode", "制造商编码");
|
||||
orderFields.put("vendorName", "供应商名称");
|
||||
orderFields.put("vendorAddress", "供应商地址");
|
||||
orderFields.put("vendorUser", "供应商联系人");
|
||||
orderFields.put("vendorPhone", "供应商电话");
|
||||
orderFields.put("taxRate", "税率(%,取明细税率最大值)");
|
||||
orderFields.put("currency", "币别");
|
||||
orderFields.put("purchaserId", "采购员ID");
|
||||
orderFields.put("purchaserName", "采购员姓名");
|
||||
orderFields.put("purchaserMobile", "采购员电话");
|
||||
orderFields.put("purchaserEmail", "采购员邮箱");
|
||||
orderFields.put("warehouseId", "入库仓库ID");
|
||||
orderFields.put("warehouseName", "入库仓库名称");
|
||||
orderFields.put("payMethod", "付款方式编码(0/2入库付款,1出库付款)");
|
||||
orderFields.put("payMethodName", "付款方式名称");
|
||||
orderFields.put("ownerId", "汇智负责人ID");
|
||||
orderFields.put("ownerName", "汇智负责人姓名");
|
||||
orderFields.put("totalAmount", "含税总金额");
|
||||
orderFields.put("status", "采购状态编码(0待入库/1部分入库/2已完成)");
|
||||
orderFields.put("statusName", "采购状态名称");
|
||||
orderFields.put("approveStatus", "审批状态编码(0草稿/1审批中/2已通过/3驳回)");
|
||||
orderFields.put("approveStatusName", "审批状态名称");
|
||||
orderFields.put("approveTime", "审批时间");
|
||||
orderFields.put("approveNode", "当前审批节点");
|
||||
orderFields.put("confirmStatus", "供应商确认状态编码(0待确认/1已确认/2已驳回)");
|
||||
orderFields.put("confirmStatusName", "供应商确认状态名称");
|
||||
orderFields.put("purchaseDate", "采购日期");
|
||||
orderFields.put("flowType", "流程类型(online/offline)");
|
||||
orderFields.put("flowTypeName", "流程类型名称");
|
||||
orderFields.put("isVirtual", "是否虚拟采购单");
|
||||
orderFields.put("version", "版本号");
|
||||
orderFields.put("remark", "备注");
|
||||
orderFields.put("items", "include_detail=true 时的采购明细行数组(见 ITEM 字段)");
|
||||
fields.put("ORDER", orderFields);
|
||||
|
||||
Map<String, Object> itemFields = new LinkedHashMap<>();
|
||||
itemFields.put("id", "明细ID");
|
||||
itemFields.put("purchaseId", "采购单ID");
|
||||
itemFields.put("purchaseNo", "采购单号");
|
||||
itemFields.put("productCode", "产品BOM编码");
|
||||
itemFields.put("productModel", "产品型号");
|
||||
itemFields.put("productType", "产品类型");
|
||||
itemFields.put("productDescription", "产品描述");
|
||||
itemFields.put("quantity", "采购数量");
|
||||
itemFields.put("innerQuantity", "已入库数量");
|
||||
itemFields.put("pendingQuantity", "未入库数量(quantity - inner_quantity,下限0)");
|
||||
itemFields.put("price", "单价");
|
||||
itemFields.put("taxRate", "税率(%)");
|
||||
itemFields.put("taxTotal", "税额");
|
||||
itemFields.put("amountTotal", "含税金额");
|
||||
itemFields.put("deliveryDate", "交货日期");
|
||||
fields.put("ITEM", itemFields);
|
||||
|
||||
Map<String, Object> bindFields = new LinkedHashMap<>();
|
||||
bindFields.put("orderId", "订单ID(project_order_info.id)");
|
||||
bindFields.put("orderCode", "订单号/合同编号");
|
||||
bindFields.put("purchaseId", "采购单ID");
|
||||
bindFields.put("purchaseNo", "采购单号");
|
||||
bindFields.put("productCode", "产品编码");
|
||||
bindFields.put("bindNum", "绑定数量");
|
||||
fields.put("ORDER_BIND", bindFields);
|
||||
|
||||
Map<String, Object> historyFields = new LinkedHashMap<>();
|
||||
historyFields.put("id", "历史记录ID");
|
||||
historyFields.put("purchaseId", "原始采购单ID");
|
||||
historyFields.put("purchaseNo", "采购单号");
|
||||
historyFields.put("version", "版本号");
|
||||
historyFields.put("buyerName", "采购方名称");
|
||||
historyFields.put("buyerAddress", "采购方地址");
|
||||
historyFields.put("vendorId", "制造商ID");
|
||||
historyFields.put("vendorName", "供应商名称");
|
||||
historyFields.put("vendorUser", "供应商联系人");
|
||||
historyFields.put("vendorPhone", "供应商电话");
|
||||
historyFields.put("currency", "币别");
|
||||
historyFields.put("purchaserId", "采购员ID");
|
||||
historyFields.put("purchaserName", "采购员姓名");
|
||||
historyFields.put("purchaserMobile", "采购员电话");
|
||||
historyFields.put("purchaserEmail", "采购员邮箱");
|
||||
historyFields.put("warehouseId", "入库仓库ID");
|
||||
historyFields.put("payMethod", "付款方式编码");
|
||||
historyFields.put("payMethodName", "付款方式名称");
|
||||
historyFields.put("ownerId", "汇智负责人ID");
|
||||
historyFields.put("ownerName", "汇智负责人姓名");
|
||||
historyFields.put("totalAmount", "含税总金额");
|
||||
historyFields.put("status", "采购状态编码");
|
||||
historyFields.put("statusName", "采购状态名称");
|
||||
historyFields.put("approveStatus", "审批状态编码");
|
||||
historyFields.put("approveStatusName", "审批状态名称");
|
||||
historyFields.put("approveTime", "审批时间");
|
||||
historyFields.put("approveNode", "当前审批节点");
|
||||
historyFields.put("confirmStatus", "确认状态编码");
|
||||
historyFields.put("confirmStatusName", "确认状态名称");
|
||||
historyFields.put("flowType", "流程类型");
|
||||
historyFields.put("flowTypeName", "流程类型名称");
|
||||
historyFields.put("isVirtual", "是否虚拟采购单");
|
||||
historyFields.put("createTime", "创建时间(HISTORY 的时间维度字段)");
|
||||
historyFields.put("updateTime", "更新时间");
|
||||
historyFields.put("remark", "备注");
|
||||
historyFields.put("items", "include_detail=true 时的历史明细行数组");
|
||||
fields.put("HISTORY", historyFields);
|
||||
|
||||
Map<String, Object> historyDetailFields = new LinkedHashMap<>();
|
||||
historyDetailFields.put("id", "历史明细ID");
|
||||
historyDetailFields.put("purchaseHistoryId", "关联采购单历史ID");
|
||||
historyDetailFields.put("originalPurchaseId", "原始采购单ID");
|
||||
historyDetailFields.put("productCode", "产品BOM编码");
|
||||
historyDetailFields.put("productModel", "产品型号");
|
||||
historyDetailFields.put("productType", "产品类型");
|
||||
historyDetailFields.put("productDescription", "产品描述");
|
||||
historyDetailFields.put("quantity", "采购数量");
|
||||
historyDetailFields.put("price", "单价");
|
||||
historyDetailFields.put("taxRate", "税率(%)");
|
||||
historyDetailFields.put("taxTotal", "税额");
|
||||
historyDetailFields.put("amountTotal", "含税金额");
|
||||
historyDetailFields.put("deliveryDate", "交货日期");
|
||||
historyDetailFields.put("innerStatus", "入库状态(0待入库,1已完成)");
|
||||
fields.put("HISTORY_DETAIL", historyDetailFields);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static class CountResult {
|
||||
private final long total;
|
||||
private final boolean capped;
|
||||
|
||||
private CountResult(long total, boolean capped) {
|
||||
this.total = total;
|
||||
this.capped = capped;
|
||||
}
|
||||
|
||||
private static CountResult of(int size) {
|
||||
if (size > COUNT_CAP) {
|
||||
return new CountResult(COUNT_CAP, true);
|
||||
}
|
||||
return new CountResult(size, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,950 @@
|
|||
package com.ruoyi.sip.llm.tools;
|
||||
|
||||
import com.ruoyi.sip.domain.DeliveryList;
|
||||
import com.ruoyi.sip.domain.InventoryDelivery;
|
||||
import com.ruoyi.sip.domain.InventoryInfo;
|
||||
import com.ruoyi.sip.domain.InventoryOuter;
|
||||
import com.ruoyi.sip.domain.InventoryOuterDetail;
|
||||
import com.ruoyi.sip.domain.OmsInventoryDeliveryDetail;
|
||||
import com.ruoyi.sip.domain.OmsInventoryInner;
|
||||
import com.ruoyi.sip.domain.OmsStockInfo;
|
||||
import com.ruoyi.sip.domain.ProjectOrderInfoRecall;
|
||||
import com.ruoyi.sip.llm.tools.support.AbstractMcpQueryToolProvider;
|
||||
import com.ruoyi.sip.llm.tools.support.McpAuth;
|
||||
import com.ruoyi.sip.llm.tools.support.McpCursor;
|
||||
import com.ruoyi.sip.llm.tools.support.McpFormat;
|
||||
import com.ruoyi.sip.llm.tools.support.McpPages;
|
||||
import com.ruoyi.sip.llm.tools.support.McpParams;
|
||||
import com.ruoyi.sip.llm.tools.support.McpTimeRange;
|
||||
import com.ruoyi.sip.llm.tools.support.McpToolException;
|
||||
import com.ruoyi.sip.mapper.DeliveryListMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryDeliveryMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryInfoMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryOuterDetailMapper;
|
||||
import com.ruoyi.sip.mapper.InventoryOuterMapper;
|
||||
import com.ruoyi.sip.mapper.OmsInventoryDeliveryDetailMapper;
|
||||
import com.ruoyi.sip.mapper.OmsInventoryInnerMapper;
|
||||
import com.ruoyi.sip.mapper.OmsStockInfoMapper;
|
||||
import com.ruoyi.sip.mapper.OrderDeliveryMapper;
|
||||
import com.ruoyi.sip.mapper.ProjectOrderInfoRecallMapper;
|
||||
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.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MCP 工具 warehouse_list(列表 / 范围查询,游标分页,entity 参数化)。
|
||||
* <p>权限来源(逐个 entity 从对应 Controller 的 {@code @RequiresPermissions} 抄取):
|
||||
* INNER→OmsInventoryInnerController、OUTER→InventoryOuterController、DELIVERY→InventoryDeliveryController(VueDeliveryController)、
|
||||
* ORDER_DELIVERY→OrderDeliveryController、STOCK→OmsStockInfoController、SN→InventoryInfoController、
|
||||
* RECALL→ProjectOrderInfoController(待确认)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class WarehouseListToolProvider extends AbstractMcpQueryToolProvider {
|
||||
|
||||
private static final Set<String> ENTITIES = new HashSet<>(Arrays.asList(
|
||||
"INNER", "OUTER", "DELIVERY", "ORDER_DELIVERY", "STOCK", "SN", "RECALL"));
|
||||
|
||||
private static final int MAX_SN = 50;
|
||||
private static final int MAX_CODE = 20;
|
||||
private static final long COUNT_CAP = 50000L;
|
||||
private static final int DEFAULT_MONTHS = 12;
|
||||
private static final int MAX_MONTHS = 36;
|
||||
|
||||
@Autowired
|
||||
private OmsInventoryInnerMapper omsInventoryInnerMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryOuterMapper inventoryOuterMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryOuterDetailMapper inventoryOuterDetailMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryDeliveryMapper inventoryDeliveryMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsInventoryDeliveryDetailMapper omsInventoryDeliveryDetailMapper;
|
||||
|
||||
@Autowired
|
||||
private OrderDeliveryMapper orderDeliveryMapper;
|
||||
|
||||
@Autowired
|
||||
private DeliveryListMapper deliveryListMapper;
|
||||
|
||||
@Autowired
|
||||
private OmsStockInfoMapper omsStockInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private InventoryInfoMapper inventoryInfoMapper;
|
||||
|
||||
@Autowired
|
||||
private ProjectOrderInfoRecallMapper projectOrderInfoRecallMapper;
|
||||
|
||||
@Override
|
||||
protected String getToolName() {
|
||||
return "warehouse_list";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getToolDescription() {
|
||||
return "仓储域列表 / 范围查询(entity 参数化):入库单(INNER)、出库单(OUTER)、仓储发货单(DELIVERY)、"
|
||||
+ "manage 域发货单(ORDER_DELIVERY)、备货(STOCK)、SN 明细(SN)、撤回历史(RECALL)。支持按单号/状态/时间范围等过滤。"
|
||||
+ "分页查询时:若返回 page_info.has_more 为 true,必须携带 page_info.next_cursor 再次调用本工具,"
|
||||
+ "重复直到 has_more 为 false,否则结果不完整;不要用其它参数代替 cursor。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildInputSchema() {
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("entity", enumProperty("查询实体(必填):INNER=入库单 / OUTER=出库单 / DELIVERY=仓储发货单 / "
|
||||
+ "ORDER_DELIVERY=manage 域发货单 / STOCK=备货 / SN=SN 明细 / RECALL=撤回历史",
|
||||
"INNER", "OUTER", "DELIVERY", "ORDER_DELIVERY", "STOCK", "SN", "RECALL"));
|
||||
properties.put("inner_code_list", arrayProperty("入库单号列表,≤" + MAX_CODE + "(entity=INNER)"));
|
||||
properties.put("outer_code_list", arrayProperty("出库单号列表,≤" + MAX_CODE + "(entity=OUTER)"));
|
||||
properties.put("delivery_code_list", arrayProperty("发货单号列表,≤" + MAX_CODE + "(entity=ORDER_DELIVERY)"));
|
||||
properties.put("order_code_list", arrayProperty("合同编号列表,≤" + MAX_CODE + "(entity=OUTER/ORDER_DELIVERY)"));
|
||||
properties.put("product_code_list", arrayProperty("产品编码列表,≤" + MAX_CODE
|
||||
+ "(entity=OUTER/DELIVERY/SN)"));
|
||||
properties.put("product_sn_list", arrayProperty("产品序列号列表,≤" + MAX_SN + "(entity=SN)"));
|
||||
properties.put("outer_status_list", arrayProperty("出库状态列表(entity=OUTER):1待确认/2已确认/3已接收/4已退回"));
|
||||
properties.put("purchase_no", stringProperty("采购单号(entity=INNER)"));
|
||||
properties.put("order_code", stringProperty("合同编号(entity=DELIVERY/STOCK/SN/RECALL)"));
|
||||
properties.put("outer_code", stringProperty("出库单号(entity=DELIVERY/SN)"));
|
||||
properties.put("inner_code", stringProperty("入库单号(entity=SN)"));
|
||||
properties.put("delivery_status", stringProperty("发货状态(entity=DELIVERY:0待发货/1已发货/2撤回;entity=ORDER_DELIVERY:实测取值为拼音缩写 qs=已签收、yf=已发货,同值数字 1/2/3 亦兼容)"));
|
||||
properties.put("stock_status", stringProperty("备货状态(entity=STOCK):0=未备货,1=已备货"));
|
||||
properties.put("inventory_status", stringProperty("库存状态(entity=SN):0=入库,1=出库"));
|
||||
properties.put("time_range", stringProperty("时间范围,格式 \"开始~结束\"(yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss);"
|
||||
+ "各 entity 的时间字段:INNER/OUTER/STOCK/RECALL=create_time、DELIVERY=delivery_time、ORDER_DELIVERY=delivery_date;"
|
||||
+ "未传时默认近 " + DEFAULT_MONTHS + " 个月,跨度上限 " + MAX_MONTHS + " 个月"));
|
||||
properties.put("time_range_start", stringProperty("时间范围开始(可选,与 time_range 二选一)"));
|
||||
properties.put("time_range_end", stringProperty("时间范围结束(可选,与 time_range 二选一)"));
|
||||
properties.put("include_detail", boolProperty("是否返回明细,默认 false(明细来源见 description/metadata)"));
|
||||
properties.put("include_total", boolProperty("是否统计总条数,默认 false;true 时受 count_cap=" + COUNT_CAP + " 限制"));
|
||||
withPagingProperties(properties, McpPages.MAX_LIST_PAGE_SIZE, McpPages.DEFAULT_PAGE_SIZE);
|
||||
return objectSchema(properties, "entity");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handle(Map<String, Object> params) {
|
||||
String entity = McpParams.requireEnum(params, "entity", ENTITIES);
|
||||
McpAuth.requireAnyPermission(permissionsOf(entity));
|
||||
|
||||
int pageSize = McpPages.pageSize(params, McpPages.DEFAULT_PAGE_SIZE, McpPages.MAX_LIST_PAGE_SIZE);
|
||||
boolean includeDetail = McpParams.boolVal(params, "include_detail", false);
|
||||
boolean includeTotal = McpParams.boolVal(params, "include_total", false);
|
||||
|
||||
EntityResult result = runEntity(entity, params, pageSize, includeDetail, includeTotal);
|
||||
|
||||
Map<String, Object> pageInfo = McpPages.pageInfo(result.items.size(), pageSize, result.hasMore,
|
||||
result.hasMore ? McpCursor.encode(getToolName(), result.filterHash, result.pageNo + 1, result.lastSortKeys) : null,
|
||||
result.sortBy, result.pageNo, result.total, result.totalCapped, false);
|
||||
|
||||
Map<String, Object> data = listData(result.items, pageInfo, result.total);
|
||||
return response(buildMetadata(entity), queryEcho(entity, params, pageSize, includeDetail, includeTotal), data);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ entity 分发
|
||||
|
||||
private EntityResult runEntity(String entity,
|
||||
Map<String, Object> params,
|
||||
int pageSize,
|
||||
boolean includeDetail,
|
||||
boolean includeTotal) {
|
||||
switch (entity) {
|
||||
case "INNER":
|
||||
return runInner(params, pageSize, includeDetail, includeTotal);
|
||||
case "OUTER":
|
||||
return runOuter(params, pageSize, includeDetail, includeTotal);
|
||||
case "DELIVERY":
|
||||
return runDelivery(params, pageSize, includeDetail, includeTotal);
|
||||
case "ORDER_DELIVERY":
|
||||
return runOrderDelivery(params, pageSize, includeDetail, includeTotal);
|
||||
case "STOCK":
|
||||
return runStock(params, pageSize, includeTotal);
|
||||
case "SN":
|
||||
return runSn(params, pageSize, includeTotal);
|
||||
case "RECALL":
|
||||
return runRecall(params, pageSize, includeTotal);
|
||||
default:
|
||||
throw McpToolException.invalidParams("不支持的 entity:" + entity);
|
||||
}
|
||||
}
|
||||
|
||||
private EntityResult runInner(Map<String, Object> params, int pageSize, boolean includeDetail, boolean includeTotal) {
|
||||
List<String> innerCodes = McpParams.codeList(params, "inner_code_list", MAX_CODE);
|
||||
String purchaseNo = McpParams.str(params, "purchase_no");
|
||||
String orderCode = McpParams.str(params, "order_code");
|
||||
McpTimeRange range = McpParams.timeRange(params, "time_range", DEFAULT_MONTHS, MAX_MONTHS);
|
||||
EntityResult result = new EntityResult("inner_code,id");
|
||||
result.filterHash = McpParams.filterHash(getToolName(), "INNER", String.join(",", innerCodes), purchaseNo,
|
||||
orderCode, McpParams.describe(range));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), result.filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
result.pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("innerCodeList", innerCodes);
|
||||
q.put("purchaseNo", purchaseNo);
|
||||
q.put("orderCode", orderCode);
|
||||
q.put("startTime", range.getStart());
|
||||
q.put("endTime", range.getEnd());
|
||||
q.put("limit", pageSize + 1);
|
||||
putCursor(q, cursor);
|
||||
|
||||
List<OmsInventoryInner> rows = omsInventoryInnerMapper.listInnerPage(q);
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<OmsInventoryInner> page = McpPages.trim(rows, pageSize);
|
||||
List<String> pageInnerCodes = new ArrayList<>();
|
||||
for (OmsInventoryInner row : page) {
|
||||
result.items.add(toInnerItem(row));
|
||||
String code = McpFormat.trimToNull(row.getInnerCode());
|
||||
if (code != null) {
|
||||
pageInnerCodes.add(code);
|
||||
}
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
OmsInventoryInner last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(last.getInnerCode(), last.getId());
|
||||
}
|
||||
if (includeDetail && !pageInnerCodes.isEmpty()) {
|
||||
// 明细来源:oms_inventory_info(按 inner_code)——oms_inventory_inner_detail 实测仅 1 行、不采用
|
||||
Map<String, List<Map<String, Object>>> detailMap = new LinkedHashMap<>();
|
||||
List<InventoryInfo> sns = inventoryInfoMapper.selectInventoryInfoByInnerCodeList(pageInnerCodes);
|
||||
if (sns != null) {
|
||||
for (InventoryInfo sn : sns) {
|
||||
String code = McpFormat.trimToNull(sn.getInnerCode());
|
||||
if (code != null) {
|
||||
detailMap.computeIfAbsent(code, k -> new ArrayList<>()).add(toSnItem(sn));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map<String, Object> item : result.items) {
|
||||
item.put("snDetails", detailMap.getOrDefault(String.valueOf(item.get("innerCode")), Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, omsInventoryInnerMapper.countInnerPage(q));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runOuter(Map<String, Object> params, int pageSize, boolean includeDetail, boolean includeTotal) {
|
||||
List<String> outerCodes = McpParams.codeList(params, "outer_code_list", MAX_CODE);
|
||||
List<String> orderCodes = McpParams.codeList(params, "order_code_list", MAX_CODE);
|
||||
List<String> productCodes = McpParams.codeList(params, "product_code_list", MAX_CODE);
|
||||
List<String> outerStatusList = McpParams.codeList(params, "outer_status_list", MAX_CODE);
|
||||
McpTimeRange range = McpParams.timeRange(params, "time_range", DEFAULT_MONTHS, MAX_MONTHS);
|
||||
EntityResult result = new EntityResult("outer_code,id");
|
||||
result.filterHash = McpParams.filterHash(getToolName(), "OUTER", String.join(",", outerCodes),
|
||||
String.join(",", orderCodes), String.join(",", productCodes), String.join(",", outerStatusList),
|
||||
McpParams.describe(range));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), result.filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
result.pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("outerCodeList", outerCodes);
|
||||
q.put("orderCodeList", orderCodes);
|
||||
q.put("productCodeList", productCodes);
|
||||
q.put("outerStatusList", outerStatusList);
|
||||
q.put("startTime", range.getStart());
|
||||
q.put("endTime", range.getEnd());
|
||||
q.put("limit", pageSize + 1);
|
||||
putCursor(q, cursor);
|
||||
|
||||
List<InventoryOuter> rows = inventoryOuterMapper.listOuterPage(q);
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<InventoryOuter> page = McpPages.trim(rows, pageSize);
|
||||
List<String> pageOuterCodes = new ArrayList<>();
|
||||
for (InventoryOuter row : page) {
|
||||
result.items.add(toOuterItem(row));
|
||||
String code = McpFormat.trimToNull(row.getOuterCode());
|
||||
if (code != null) {
|
||||
pageOuterCodes.add(code);
|
||||
}
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
InventoryOuter last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(last.getOuterCode(), last.getId());
|
||||
}
|
||||
if (includeDetail && !pageOuterCodes.isEmpty()) {
|
||||
Map<String, List<Map<String, Object>>> detailMap = new LinkedHashMap<>();
|
||||
List<InventoryOuterDetail> details = inventoryOuterDetailMapper.listByOuterCodeList(pageOuterCodes);
|
||||
if (details != null) {
|
||||
for (InventoryOuterDetail detail : details) {
|
||||
String code = McpFormat.trimToNull(detail.getOuterCode());
|
||||
if (code != null) {
|
||||
detailMap.computeIfAbsent(code, k -> new ArrayList<>()).add(toOuterDetailItem(detail));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map<String, Object> item : result.items) {
|
||||
item.put("outerDetails", detailMap.getOrDefault(String.valueOf(item.get("outerCode")), Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, inventoryOuterMapper.countOuterPage(q));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runDelivery(Map<String, Object> params, int pageSize, boolean includeDetail, boolean includeTotal) {
|
||||
String outerCode = McpParams.str(params, "outer_code");
|
||||
String orderCode = McpParams.str(params, "order_code");
|
||||
List<String> productCodes = McpParams.codeList(params, "product_code_list", MAX_CODE);
|
||||
String deliveryStatus = McpParams.str(params, "delivery_status");
|
||||
McpTimeRange range = McpParams.timeRange(params, "time_range", DEFAULT_MONTHS, MAX_MONTHS);
|
||||
EntityResult result = new EntityResult("outer_code,id");
|
||||
result.filterHash = McpParams.filterHash(getToolName(), "DELIVERY", outerCode, orderCode,
|
||||
String.join(",", productCodes), deliveryStatus, McpParams.describe(range));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), result.filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
result.pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("outerCode", outerCode);
|
||||
q.put("orderCode", orderCode);
|
||||
q.put("productCodeList", productCodes);
|
||||
q.put("deliveryStatus", deliveryStatus);
|
||||
q.put("startTime", range.getStart());
|
||||
q.put("endTime", range.getEnd());
|
||||
q.put("limit", pageSize + 1);
|
||||
putCursor(q, cursor);
|
||||
|
||||
List<InventoryDelivery> rows = inventoryDeliveryMapper.listDeliveryPage(q);
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<InventoryDelivery> page = McpPages.trim(rows, pageSize);
|
||||
List<Long> deliveryIds = new ArrayList<>();
|
||||
for (InventoryDelivery row : page) {
|
||||
result.items.add(toDeliveryItem(row));
|
||||
if (row.getId() != null) {
|
||||
deliveryIds.add(row.getId());
|
||||
}
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
InventoryDelivery last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(last.getOuterCode(), last.getId());
|
||||
}
|
||||
if (includeDetail && !deliveryIds.isEmpty()) {
|
||||
Map<Long, List<String>> snMap = new LinkedHashMap<>();
|
||||
List<OmsInventoryDeliveryDetail> details = omsInventoryDeliveryDetailMapper.listByDeliveryIdList(deliveryIds);
|
||||
if (details != null) {
|
||||
for (OmsInventoryDeliveryDetail detail : details) {
|
||||
if (detail.getDeliveryId() != null) {
|
||||
snMap.computeIfAbsent(detail.getDeliveryId(), k -> new ArrayList<>())
|
||||
.add(McpFormat.trim(detail.getProductSn()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map<String, Object> item : result.items) {
|
||||
Object id = item.get("deliveryId");
|
||||
Long key = id instanceof Number ? ((Number) id).longValue() : null;
|
||||
item.put("productSns", key == null ? Collections.emptyList()
|
||||
: snMap.getOrDefault(key, Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, inventoryDeliveryMapper.countDeliveryPage(q));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runOrderDelivery(Map<String, Object> params, int pageSize, boolean includeDetail, boolean includeTotal) {
|
||||
List<String> deliveryCodes = McpParams.codeList(params, "delivery_code_list", MAX_CODE);
|
||||
List<String> orderCodes = McpParams.codeList(params, "order_code_list", MAX_CODE);
|
||||
String deliveryStatus = McpParams.str(params, "delivery_status");
|
||||
McpTimeRange range = McpParams.timeRange(params, "time_range", DEFAULT_MONTHS, MAX_MONTHS);
|
||||
EntityResult result = new EntityResult("delivery_code,id");
|
||||
result.filterHash = McpParams.filterHash(getToolName(), "ORDER_DELIVERY", String.join(",", deliveryCodes),
|
||||
String.join(",", orderCodes), deliveryStatus, McpParams.describe(range));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), result.filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
result.pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("deliveryCodeList", deliveryCodes);
|
||||
q.put("orderCodeList", orderCodes);
|
||||
q.put("deliveryStatus", deliveryStatus);
|
||||
q.put("startTime", range.getStart());
|
||||
q.put("endTime", range.getEnd());
|
||||
q.put("limit", pageSize + 1);
|
||||
putCursor(q, cursor);
|
||||
|
||||
List<Map<String, Object>> rows = orderDeliveryMapper.listOrderDeliveryPage(q);
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<Map<String, Object>> page = McpPages.trim(rows, pageSize);
|
||||
List<Long> deliveryIds = new ArrayList<>();
|
||||
for (Map<String, Object> row : page) {
|
||||
Object id = row.get("id");
|
||||
row.put("deliveryStatusName", orderDeliveryStatusName(str(row.get("deliveryStatus"))));
|
||||
row.put("deliveryTypeName", deliveryTypeName(str(row.get("deliveryType"))));
|
||||
result.items.add(row);
|
||||
if (id instanceof Number) {
|
||||
deliveryIds.add(((Number) id).longValue());
|
||||
}
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
Map<String, Object> last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(str(last.get("deliveryCode")), last.get("id"));
|
||||
}
|
||||
if (includeDetail && !deliveryIds.isEmpty()) {
|
||||
Map<Long, List<Map<String, Object>>> detailMap = new LinkedHashMap<>();
|
||||
List<DeliveryList> details = deliveryListMapper.listByDeliveryIdList(deliveryIds);
|
||||
if (details != null) {
|
||||
for (DeliveryList detail : details) {
|
||||
if (detail.getDeliveryId() != null) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("productCode", McpFormat.trim(detail.getProductCode()));
|
||||
item.put("serialNumber", McpFormat.trim(detail.getSerialNumber()));
|
||||
item.put("remark", detail.getRemark());
|
||||
detailMap.computeIfAbsent(detail.getDeliveryId(), k -> new ArrayList<>()).add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map<String, Object> item : result.items) {
|
||||
Object id = item.get("id");
|
||||
Long key = id instanceof Number ? ((Number) id).longValue() : null;
|
||||
item.put("deliveryItems", key == null ? Collections.emptyList()
|
||||
: detailMap.getOrDefault(key, Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, orderDeliveryMapper.countOrderDeliveryPage(q));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runStock(Map<String, Object> params, int pageSize, boolean includeTotal) {
|
||||
String orderCode = McpParams.str(params, "order_code");
|
||||
String stockStatus = McpParams.str(params, "stock_status");
|
||||
McpTimeRange range = McpParams.timeRange(params, "time_range", DEFAULT_MONTHS, MAX_MONTHS);
|
||||
EntityResult result = new EntityResult("order_code,id");
|
||||
result.filterHash = McpParams.filterHash(getToolName(), "STOCK", orderCode, stockStatus, McpParams.describe(range));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), result.filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
result.pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("orderCode", orderCode);
|
||||
q.put("stockStatus", stockStatus);
|
||||
q.put("startTime", range.getStart());
|
||||
q.put("endTime", range.getEnd());
|
||||
q.put("limit", pageSize + 1);
|
||||
putCursor(q, cursor);
|
||||
|
||||
List<OmsStockInfo> rows = omsStockInfoMapper.listStockPage(q);
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<OmsStockInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (OmsStockInfo row : page) {
|
||||
result.items.add(toStockItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
OmsStockInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(last.getOrderCode(), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, omsStockInfoMapper.countStockPage(q));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runSn(Map<String, Object> params, int pageSize, boolean includeTotal) {
|
||||
// 大表护栏:必须给出索引键之一,禁止无界全表分页(方案 4.3 护栏)
|
||||
McpParams.requireAny(params, "product_sn_list", "product_code_list", "inner_code", "outer_code");
|
||||
List<String> productSns = McpParams.codeList(params, "product_sn_list", MAX_SN);
|
||||
List<String> productCodes = McpParams.codeList(params, "product_code_list", MAX_CODE);
|
||||
String inventoryStatus = McpParams.str(params, "inventory_status");
|
||||
String innerCode = McpParams.str(params, "inner_code");
|
||||
String outerCode = McpParams.str(params, "outer_code");
|
||||
EntityResult result = new EntityResult("product_sn,id");
|
||||
result.filterHash = McpParams.filterHash(getToolName(), "SN", String.join(",", productSns),
|
||||
String.join(",", productCodes), inventoryStatus, innerCode, outerCode);
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), result.filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
result.pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("productSnList", productSns);
|
||||
q.put("productCodeList", productCodes);
|
||||
q.put("inventoryStatus", inventoryStatus);
|
||||
q.put("innerCode", innerCode);
|
||||
q.put("outerCode", outerCode);
|
||||
q.put("limit", pageSize + 1);
|
||||
putCursor(q, cursor);
|
||||
|
||||
List<InventoryInfo> rows = inventoryInfoMapper.listSnPage(q);
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<InventoryInfo> page = McpPages.trim(rows, pageSize);
|
||||
for (InventoryInfo row : page) {
|
||||
result.items.add(toSnItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
InventoryInfo last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(last.getProductSn(), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, inventoryInfoMapper.countSnPage(q));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private EntityResult runRecall(Map<String, Object> params, int pageSize, boolean includeTotal) {
|
||||
String orderCode = McpParams.str(params, "order_code");
|
||||
McpTimeRange range = McpParams.timeRange(params, "time_range", DEFAULT_MONTHS, MAX_MONTHS);
|
||||
EntityResult result = new EntityResult("order_code,create_time desc,id");
|
||||
result.filterHash = McpParams.filterHash(getToolName(), "RECALL", orderCode, McpParams.describe(range));
|
||||
McpCursor cursor = McpPages.cursor(params, getToolName(), result.filterHash, McpPages.DEFAULT_LIST_MAX_PAGES);
|
||||
result.pageNo = cursor == null ? 1 : cursor.getP();
|
||||
|
||||
Map<String, Object> q = new HashMap<>();
|
||||
q.put("orderCode", orderCode);
|
||||
q.put("startTime", range.getStart());
|
||||
q.put("endTime", range.getEnd());
|
||||
q.put("limit", pageSize + 1);
|
||||
if (cursor != null) {
|
||||
q.put("cursorKey", cursor.key(0));
|
||||
q.put("cursorTime", McpParams.parseDate(cursor.key(1), "cursor"));
|
||||
q.put("cursorId", cursor.longKey(2));
|
||||
}
|
||||
|
||||
List<ProjectOrderInfoRecall> rows = projectOrderInfoRecallMapper.listRecallPage(q);
|
||||
result.hasMore = McpPages.hasMore(rows, pageSize);
|
||||
List<ProjectOrderInfoRecall> page = McpPages.trim(rows, pageSize);
|
||||
for (ProjectOrderInfoRecall row : page) {
|
||||
result.items.add(toRecallItem(row));
|
||||
}
|
||||
if (!page.isEmpty()) {
|
||||
ProjectOrderInfoRecall last = page.get(page.size() - 1);
|
||||
result.lastSortKeys = keysOf(last.getOrderCode(), McpFormat.dateTime(last.getCreateTime()), last.getId());
|
||||
}
|
||||
if (includeTotal) {
|
||||
applyTotal(result, projectOrderInfoRecallMapper.countRecallPage(q));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 行映射
|
||||
|
||||
private Map<String, Object> toInnerItem(OmsInventoryInner row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("innerCode", McpFormat.trim(row.getInnerCode()));
|
||||
item.put("purchaseNo", McpFormat.trim(row.getPurchaseNo()));
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("productType", row.getProductType());
|
||||
item.put("model", row.getModel());
|
||||
item.put("quantity", row.getQuantity());
|
||||
item.put("vendorCode", McpFormat.trim(row.getVendorCode()));
|
||||
item.put("vendorName", row.getVendorName());
|
||||
item.put("warehouseId", row.getWarehouseId());
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("warehouseType", row.getWarehouseType());
|
||||
item.put("totalAmount", row.getTotalAmount());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
item.put("taxTotal", row.getTaxTotal());
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("remark", row.getRemark());
|
||||
item.put("createByName", row.getCreateByName());
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toOuterItem(InventoryOuter row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("outerCode", McpFormat.trim(row.getOuterCode()));
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("projectCode", row.getProjectCode());
|
||||
item.put("projectName", row.getProjectName());
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("model", row.getModel());
|
||||
item.put("quantity", row.getQuantity());
|
||||
item.put("outerStatus", row.getOuterStatus());
|
||||
item.put("outerStatusName", outerStatusName(row.getOuterStatus()));
|
||||
item.put("deliveryStatus", row.getDeliveryStatus());
|
||||
item.put("deliveryStatusName", outerDeliveryStatusName(row.getDeliveryStatus()));
|
||||
item.put("deliveryTime", McpFormat.date(row.getDeliveryTime()));
|
||||
item.put("contactPerson", row.getContactPerson());
|
||||
item.put("contactPhone", row.getContactPhone());
|
||||
item.put("contactAddress", row.getContactAddress());
|
||||
item.put("deliveryTimeType", row.getDeliveryTimeType());
|
||||
item.put("versionCode", row.getVersionCode());
|
||||
item.put("receivableBillCode", McpFormat.trim(row.getReceivableBillCode()));
|
||||
item.put("createByName", row.getCreateByName());
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toOuterDetailItem(InventoryOuterDetail row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("outerCode", McpFormat.trim(row.getOuterCode()));
|
||||
item.put("warehouseId", row.getWarehouseId());
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("quantity", row.getQuantity());
|
||||
item.put("outerStatus", row.getOuterStatus());
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("model", row.getModel());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toDeliveryItem(InventoryDelivery row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("deliveryId", row.getId());
|
||||
item.put("outerCode", McpFormat.trim(row.getOuterCode()));
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("projectCode", row.getProjectCode());
|
||||
item.put("projectName", row.getProjectName());
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("model", row.getModel());
|
||||
item.put("quantity", row.getQuantity());
|
||||
item.put("warehouseId", row.getWarehouseId());
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("logisticsCompany", row.getLogisticsCompany());
|
||||
item.put("logisticsCode", row.getLogisticsCode());
|
||||
item.put("deliveryType", row.getDeliveryType());
|
||||
item.put("deliveryTypeName", deliveryTypeName(row.getDeliveryType()));
|
||||
item.put("deliveryStatus", row.getDeliveryStatus());
|
||||
item.put("deliveryStatusName", deliveryStatusName(row.getDeliveryStatus()));
|
||||
item.put("approveStatus", row.getApproveStatus());
|
||||
item.put("deliveryTime", McpFormat.date(row.getDeliveryTime()));
|
||||
item.put("receiverAddress", row.getNotifierAddress());
|
||||
item.put("versionCode", row.getVersionCode());
|
||||
item.put("createByName", row.getCreateByName());
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toStockItem(OmsStockInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("stockStatus", row.getStockStatus());
|
||||
item.put("stockStatusName", stockStatusName(row.getStockStatus()));
|
||||
item.put("onceInStock", row.getOnceInStock());
|
||||
item.put("allQuantity", row.getAllQuantity());
|
||||
item.put("deliveryTime", McpFormat.date(row.getDeliveryTime()));
|
||||
item.put("projectCode", row.getProjectCode());
|
||||
item.put("projectName", row.getProjectName());
|
||||
item.put("notifier", row.getNotifier());
|
||||
item.put("notifierPhone", row.getNotifierPhone());
|
||||
item.put("notifierAddress", row.getNotifierAddress());
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toSnItem(InventoryInfo row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("productSn", McpFormat.trim(row.getProductSn()));
|
||||
item.put("productCode", McpFormat.trim(row.getProductCode()));
|
||||
item.put("model", row.getModel());
|
||||
item.put("productDesc", row.getProductDesc());
|
||||
item.put("inventoryStatus", row.getInventoryStatus());
|
||||
item.put("inventoryStatusName", inventoryStatusName(row.getInventoryStatus()));
|
||||
item.put("innerCode", McpFormat.trim(row.getInnerCode()));
|
||||
item.put("outerCode", McpFormat.trim(row.getOuterCode()));
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("warehouseId", row.getWarehouseId());
|
||||
item.put("warehouseName", row.getWarehouseName());
|
||||
item.put("innerPrice", row.getInnerPrice());
|
||||
item.put("outerPrice", row.getOuterPrice());
|
||||
item.put("taxRate", row.getTaxRate());
|
||||
item.put("payableBillCode", McpFormat.trim(row.getPayableBillCode()));
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
item.put("updateTime", McpFormat.dateTime(row.getUpdateTime()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> toRecallItem(ProjectOrderInfoRecall row) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", row.getId());
|
||||
item.put("orderCode", McpFormat.trim(row.getOrderCode()));
|
||||
item.put("versionCode", row.getVersionCode());
|
||||
item.put("operationVersion", row.getOperationVersion());
|
||||
item.put("createTime", McpFormat.dateTime(row.getCreateTime()));
|
||||
item.put("createBy", row.getCreateBy());
|
||||
return item;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
private void putCursor(Map<String, Object> target, McpCursor cursor) {
|
||||
if (cursor != null) {
|
||||
target.put("cursorKey", cursor.key(0));
|
||||
target.put("cursorId", cursor.longKey(1));
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTotal(EntityResult result, long counted) {
|
||||
result.totalCapped = counted > COUNT_CAP;
|
||||
result.total = result.totalCapped ? COUNT_CAP : counted;
|
||||
}
|
||||
|
||||
private Map<String, Object> queryEcho(String entity, Map<String, Object> params, int pageSize,
|
||||
boolean includeDetail, boolean includeTotal) {
|
||||
Map<String, Object> query = new LinkedHashMap<>();
|
||||
query.put("entity", entity);
|
||||
query.put("page_size", pageSize);
|
||||
query.put("cursor", McpParams.str(params, "cursor"));
|
||||
query.put("include_detail", includeDetail);
|
||||
query.put("include_total", includeTotal);
|
||||
return query;
|
||||
}
|
||||
|
||||
private String str(Object value) {
|
||||
return value == null ? null : McpFormat.trimToNull(String.valueOf(value));
|
||||
}
|
||||
|
||||
private String outerStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryOuter.OuterStatusEnum value : InventoryOuter.OuterStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String outerDeliveryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryOuter.DeliveryStatusEnum value : InventoryOuter.DeliveryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String deliveryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryDelivery.DeliveryStatusEnum value : InventoryDelivery.DeliveryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String orderDeliveryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
// 实测该表取值是拼音缩写 qs=已签收、yf=已发货(与列注释的 1/2/3 不一致);
|
||||
// 同时保留数字取值兜底,避免其它环境按注释存值时无法翻译。
|
||||
if ("qs".equalsIgnoreCase(status) || "3".equals(status)) {
|
||||
return "已签收";
|
||||
}
|
||||
if ("yf".equalsIgnoreCase(status) || "2".equals(status)) {
|
||||
return "已发货";
|
||||
}
|
||||
if ("wf".equalsIgnoreCase(status) || "1".equals(status)) {
|
||||
return "未发货";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String deliveryTypeName(String type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
if ("1".equals(type)) {
|
||||
return "快递";
|
||||
}
|
||||
if ("2".equals(type)) {
|
||||
return "物流";
|
||||
}
|
||||
if ("3".equals(type)) {
|
||||
return "自提";
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private String stockStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
if ("0".equals(status)) {
|
||||
return "未备货";
|
||||
}
|
||||
if ("1".equals(status)) {
|
||||
return "已备货";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private String inventoryStatusName(String status) {
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
for (InventoryInfo.InventoryStatusEnum value : InventoryInfo.InventoryStatusEnum.values()) {
|
||||
if (value.getCode().equals(status)) {
|
||||
return value.getDesc();
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMetadata(String entity) {
|
||||
Map<String, Object> metadata = metadata("仓储域列表 / 范围查询(entity=" + entity + ")");
|
||||
metadata.put("entity", entity);
|
||||
metadata.put("entity_fields", mapData(
|
||||
"INNER", "入库单 oms_inventory_inner(明细来源:oms_inventory_info 按 inner_code)",
|
||||
"OUTER", "出库单 oms_inventory_outer(明细:oms_inventory_outer_detail)",
|
||||
"DELIVERY", "仓储发货单 oms_inventory_delivery(明细:oms_inventory_delivery_detail)",
|
||||
"ORDER_DELIVERY", "manage 域发货单 order_delivery(明细:delivery_list,过滤 deleted_at is null)",
|
||||
"STOCK", "备货 oms_stock_info",
|
||||
"SN", "SN 明细 oms_inventory_info",
|
||||
"RECALL", "撤回历史 project_order_info_recall"
|
||||
));
|
||||
metadata.put("data_fields", mapData(
|
||||
"total", "include_total=true 时的总条数(否则为当前页条数),受 count_cap=" + COUNT_CAP + " 限制",
|
||||
"items", "列表数据",
|
||||
"page_info", "分页信息;has_more=true 时必须回传 next_cursor"
|
||||
));
|
||||
metadata.put("item_fields", itemFields());
|
||||
metadata.put("notes", "时间维度默认近 " + DEFAULT_MONTHS + " 个月,跨度上限 " + MAX_MONTHS + " 个月,采用区间下推(>= start and < end);"
|
||||
+ "编码匹配不区分大小写(utf8mb4_unicode_ci);软删除表(order_delivery / delivery_list)统一过滤 deleted_at is null。");
|
||||
metadata.put("permission_source", permissionSource(entity));
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private Map<String, Object> itemFields() {
|
||||
return mapData(
|
||||
"id", "主键",
|
||||
"innerCode", "入库单号",
|
||||
"outerCode", "出库单号",
|
||||
"deliveryId", "发货单ID(DELIVERY)",
|
||||
"deliveryCode", "发货单号(ORDER_DELIVERY)",
|
||||
"orderCode", "合同编号",
|
||||
"orderName", "合同名称(ORDER_DELIVERY)",
|
||||
"customerCode", "客户编码(ORDER_DELIVERY)",
|
||||
"customerName", "客户名称(ORDER_DELIVERY)",
|
||||
"orderAgentCode", "代表处编码(ORDER_DELIVERY)",
|
||||
"orderPartnerCode", "代理商编码(ORDER_DELIVERY)",
|
||||
"projectCode", "项目编号",
|
||||
"projectName", "项目名称",
|
||||
"productCode", "产品BOM编码",
|
||||
"productSn", "产品序列号(SN)",
|
||||
"model", "产品型号",
|
||||
"productDesc", "产品描述(SN)",
|
||||
"productType", "产品类型(INNER)",
|
||||
"quantity", "数量",
|
||||
"purchaseNo", "采购单号(INNER)",
|
||||
"vendorCode", "厂商编码(INNER)",
|
||||
"vendorName", "厂商名称(INNER)",
|
||||
"warehouseId", "仓库ID",
|
||||
"warehouseName", "仓库名称",
|
||||
"warehouseType", "仓库类型(INNER)",
|
||||
"totalAmount", "入库含税总额(INNER)",
|
||||
"taxRate", "税率(INNER 为入库税率;SN 为明细税率,实测存在 NULL)",
|
||||
"taxTotal", "税额(INNER)",
|
||||
"outerStatus", "出库状态编码(1待确认/2已确认/3已接收/4已退回)",
|
||||
"outerStatusName", "出库状态名称",
|
||||
"deliveryStatus", "发货状态编码(OUTER:0未发货/1部分发货/2全部发货/3已撤回;DELIVERY:0待发货/1已发货/2撤回;ORDER_DELIVERY:1待发货/2已发货/3已签收)",
|
||||
"deliveryStatusName", "发货状态名称",
|
||||
"approveStatus", "撤回审批状态(DELIVERY)",
|
||||
"deliveryType", "发货方式(1快递/2物流/3自提)",
|
||||
"deliveryTypeName", "发货方式名称",
|
||||
"deliveryTime", "发货时间 / 要求到货时间",
|
||||
"signTime", "签收时间(ORDER_DELIVERY)",
|
||||
"logisticsCompany", "物流公司",
|
||||
"logisticsCode", "物流单号",
|
||||
"receiverName", "收货人(ORDER_DELIVERY)",
|
||||
"receiverPhone", "收货人电话(ORDER_DELIVERY)",
|
||||
"receiverAddress", "收货地址(ORDER_DELIVERY)",
|
||||
"contactPerson", "联系人(OUTER)",
|
||||
"contactPhone", "联系电话(OUTER)",
|
||||
"contactAddress", "联系地址(OUTER)",
|
||||
"receivableBillCodeNote", "出库单无“应收单号”列(该字段在库中不存在),如需关联应收请用 finance_list 按 order_code 查询",
|
||||
"versionCode", "版本号",
|
||||
"inventoryStatus", "库存状态编码(0=入库,1=出库)(SN)",
|
||||
"inventoryStatusName", "库存状态名称(SN)",
|
||||
"innerPrice", "入库价(SN,是否含税待确认)",
|
||||
"outerPrice", "出库价(SN,是否含税待确认)",
|
||||
"payableBillCode", "对应应付单号(SN)",
|
||||
"stockStatus", "备货状态编码(0=未备货,1=已备货)(STOCK)",
|
||||
"stockStatusName", "备货状态名称(STOCK)",
|
||||
"onceInStock", "是否一次备齐(STOCK)",
|
||||
"allQuantity", "应备货总量(STOCK)",
|
||||
"notifier", "通知人(STOCK)",
|
||||
"notifierPhone", "通知人电话(STOCK)",
|
||||
"notifierAddress", "通知人地址(STOCK)",
|
||||
"operationVersion", "操作版本(RECALL)",
|
||||
"createByName", "创建人",
|
||||
"createBy", "创建人ID(RECALL)",
|
||||
"createTime", "创建时间",
|
||||
"deliveryItems", "发货 SN 明细(ORDER_DELIVERY,include_detail=true)",
|
||||
"snDetails", "SN 明细(INNER,include_detail=true)",
|
||||
"outerDetails", "出库明细(OUTER,include_detail=true)",
|
||||
"productSns", "发货 SN 列表(DELIVERY,include_detail=true)"
|
||||
);
|
||||
}
|
||||
|
||||
private String permissionSource(String entity) {
|
||||
switch (entity) {
|
||||
case "INNER":
|
||||
return "OmsInventoryInnerController:inventory:inner:list / inventory:inner:view(已抄取)";
|
||||
case "OUTER":
|
||||
return "InventoryOuterController:inventory:outer:list / inventory:outer:view(已抄取)";
|
||||
case "DELIVERY":
|
||||
return "InventoryDeliveryController / VueDeliveryController:inventory:delivery:list / inventory:delivery:view(已抄取)";
|
||||
case "ORDER_DELIVERY":
|
||||
return "OrderDeliveryController:manage:delivery:list / manage:delivery:view(已抄取)";
|
||||
case "STOCK":
|
||||
return "OmsStockInfoController:stock:index:view(该 Controller 的 /list 未加权限注解,取视图权限;待确认)";
|
||||
case "SN":
|
||||
return "InventoryInfoController:inventory:info:view(该 Controller 的 /list 未加权限注解,取视图权限;待确认)";
|
||||
case "RECALL":
|
||||
return "project_order_info_recall 无独立 Controller,取 ProjectOrderInfoController:project:order:list / project:order:view(待确认)";
|
||||
default:
|
||||
return "未确定(待确认)";
|
||||
}
|
||||
}
|
||||
|
||||
private String[] permissionsOf(String entity) {
|
||||
switch (entity) {
|
||||
case "INNER":
|
||||
return new String[]{"inventory:inner:list", "inventory:inner:view"};
|
||||
case "OUTER":
|
||||
return new String[]{"inventory:outer:list", "inventory:outer:view"};
|
||||
case "DELIVERY":
|
||||
return new String[]{"inventory:delivery:list", "inventory:delivery:view"};
|
||||
case "ORDER_DELIVERY":
|
||||
return new String[]{"manage:delivery:list", "manage:delivery:view"};
|
||||
case "STOCK":
|
||||
return new String[]{"stock:index:view"};
|
||||
case "SN":
|
||||
return new String[]{"inventory:info:view"};
|
||||
case "RECALL":
|
||||
return new String[]{"project:order:list", "project:order:view"};
|
||||
default:
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
|
||||
/** entity 查询结果载体 */
|
||||
private static class EntityResult {
|
||||
private final List<Map<String, Object>> items = new ArrayList<>();
|
||||
private final String sortBy;
|
||||
private String filterHash;
|
||||
private int pageNo = 1;
|
||||
private boolean hasMore;
|
||||
private List<String> lastSortKeys;
|
||||
private Long total;
|
||||
private boolean totalCapped;
|
||||
|
||||
private EntityResult(String sortBy) {
|
||||
this.sortBy = sortBy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import com.ruoyi.sip.domain.VendorInfo;
|
||||
import com.ruoyi.sip.service.IInventoryAuthService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 查询类 MCP 工具的公共基类:统一 schema 属性、分页入参、响应信封、仓储行级权限作用域。
|
||||
* <p>继承 {@link AbstractMcpToolProvider},与现有工具保持同一注册方式(@Component 自动注册)。</p>
|
||||
*/
|
||||
public abstract class AbstractMcpQueryToolProvider extends AbstractMcpToolProvider {
|
||||
|
||||
protected static final int SCHEMA_VERSION = 1;
|
||||
|
||||
@Autowired(required = false)
|
||||
protected IInventoryAuthService inventoryAuthService;
|
||||
|
||||
// ------------------------------------------------------------------ schema 属性
|
||||
|
||||
protected Map<String, Object> intProperty(String description) {
|
||||
Map<String, Object> property = new HashMap<>();
|
||||
property.put("type", "integer");
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
protected Map<String, Object> boolProperty(String description) {
|
||||
Map<String, Object> property = new HashMap<>();
|
||||
property.put("type", "boolean");
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
protected Map<String, Object> enumProperty(String description, String... values) {
|
||||
Map<String, Object> property = new HashMap<>();
|
||||
property.put("type", "string");
|
||||
property.put("description", description);
|
||||
property.put("enum", Arrays.asList(values));
|
||||
return property;
|
||||
}
|
||||
|
||||
protected Map<String, Object> arrayProperty(String description) {
|
||||
Map<String, Object> property = new HashMap<>();
|
||||
property.put("type", "array");
|
||||
property.put("description", description);
|
||||
Map<String, Object> items = new HashMap<>();
|
||||
items.put("type", "string");
|
||||
property.put("items", items);
|
||||
return property;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为列表类工具统一追加 page_size / cursor 参数说明。
|
||||
*/
|
||||
protected Map<String, Object> withPagingProperties(Map<String, Object> properties, int maxPageSize, int defaultPageSize) {
|
||||
properties.put("page_size", intProperty("每页条数,默认 " + defaultPageSize + ",上限 " + maxPageSize));
|
||||
properties.put("cursor", stringProperty("分页游标:若上次返回 page_info.has_more=true,必须原样回传 page_info.next_cursor 继续取下一页,直到 has_more=false"));
|
||||
return properties;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 响应
|
||||
|
||||
protected Map<String, Object> metadata(String description) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put("tool", getToolName());
|
||||
metadata.put("schema_version", SCHEMA_VERSION);
|
||||
metadata.put("description", description);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表/分页工具的统一 data 结构:保留与现有工具一致的 total + items,并新增 page_info。
|
||||
*/
|
||||
protected Map<String, Object> listData(List<?> items, Map<String, Object> pageInfo, Long total) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("total", total != null ? total : (items == null ? 0 : items.size()));
|
||||
data.put("items", items == null ? Collections.emptyList() : items);
|
||||
data.put("page_info", pageInfo);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected Map<String, Object> mapData(Object... keyValues) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
if (keyValues != null) {
|
||||
for (int i = 0; i + 1 < keyValues.length; i += 2) {
|
||||
data.put(String.valueOf(keyValues[i]), keyValues[i + 1]);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 仓储行级权限作用域
|
||||
|
||||
protected boolean inventoryAuthAll() {
|
||||
return inventoryAuthService == null || inventoryAuthService.authAll();
|
||||
}
|
||||
|
||||
protected List<String> inventoryAuthProductCodes() {
|
||||
if (inventoryAuthService == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> codes = inventoryAuthService.authProductCode();
|
||||
return codes == null ? Collections.emptyList() : codes;
|
||||
}
|
||||
|
||||
protected List<Long> inventoryAuthWarehouseIds() {
|
||||
if (inventoryAuthService == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Long> ids = inventoryAuthService.authWarehouse();
|
||||
return ids == null ? Collections.emptyList() : ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户可访问的制造商编码(用于采购侧数据范围)。
|
||||
*/
|
||||
protected List<String> inventoryAuthVendorCodes() {
|
||||
if (inventoryAuthService == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<VendorInfo> vendors = inventoryAuthService.currentVendor();
|
||||
if (vendors == null || vendors.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> codes = new ArrayList<>();
|
||||
for (VendorInfo vendor : vendors) {
|
||||
if (vendor != null && vendor.getVendorCode() != null && !vendor.getVendorCode().trim().isEmpty()) {
|
||||
codes.add(vendor.getVendorCode().trim());
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并裁剪仓储数据范围:若非全量权限且请求的产品编码超出授权范围,直接拒绝(不返回空数据)。
|
||||
*/
|
||||
protected void assertInventoryProductScope(List<String> requestedProductCodes) {
|
||||
if (inventoryAuthAll() || requestedProductCodes == null || requestedProductCodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> allowed = inventoryAuthProductCodes();
|
||||
if (allowed.isEmpty()) {
|
||||
throw McpToolException.authError("当前机器人绑定用户未授权任何产品数据范围");
|
||||
}
|
||||
for (String code : requestedProductCodes) {
|
||||
if (!allowed.contains(code)) {
|
||||
throw McpToolException.authError("产品编码 " + code + " 超出当前用户的数据范围");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 辅助
|
||||
|
||||
protected String joinKeys(Object... keys) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (keys != null) {
|
||||
for (Object key : keys) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(key == null ? "" : String.valueOf(key));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
protected List<String> keysOf(Object... keys) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (keys != null) {
|
||||
for (Object key : keys) {
|
||||
result.add(key == null ? null : String.valueOf(key));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.utils.ShiroUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
|
||||
/**
|
||||
* 鉴权与身份:基于 MCP 机器人凭证绑定用户所属的 Shiro 主体。
|
||||
* <p>权限不足时抛 {@link McpToolException#authError(String)},由 McpController 转成 AUTH_ERROR,
|
||||
* 不返回空列表,避免 Agent 误判为"无数据"。</p>
|
||||
*/
|
||||
public final class McpAuth {
|
||||
|
||||
private McpAuth() {
|
||||
}
|
||||
|
||||
public static SysUser currentUser() {
|
||||
try {
|
||||
return ShiroUtils.getSysUser();
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Long currentUserId() {
|
||||
SysUser user = currentUser();
|
||||
return user == null ? null : user.getUserId();
|
||||
}
|
||||
|
||||
public static String currentUserName() {
|
||||
SysUser user = currentUser();
|
||||
return user == null ? null : user.getUserName();
|
||||
}
|
||||
|
||||
public static boolean hasPermission(String permission) {
|
||||
if (permission == null || permission.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
return subject != null && subject.isPermitted(permission.trim());
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 具备任意一个权限即放行(用于"一个工具对应多种页面权限"的场景)。
|
||||
*/
|
||||
public static void requireAnyPermission(String... permissions) {
|
||||
if (permissions == null || permissions.length == 0) {
|
||||
return;
|
||||
}
|
||||
for (String permission : permissions) {
|
||||
if (hasPermission(permission)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw McpToolException.authError("当前机器人绑定用户缺少访问该数据的权限,需要以下任一权限:"
|
||||
+ String.join(" / ", permissions));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 游标分页的游标载体(无状态、可校验)。
|
||||
* <p>编码:base64url(JSON) = { "v":1, "t":工具名, "f":过滤条件哈希, "p":页码, "k":[排序键值...] }</p>
|
||||
*/
|
||||
public class McpCursor {
|
||||
|
||||
public static final int VERSION = 1;
|
||||
|
||||
private int v = VERSION;
|
||||
private String t;
|
||||
private String f;
|
||||
private int p = 1;
|
||||
private List<String> k = new ArrayList<>();
|
||||
|
||||
public int getV() {
|
||||
return v;
|
||||
}
|
||||
|
||||
public void setV(int v) {
|
||||
this.v = v;
|
||||
}
|
||||
|
||||
public String getT() {
|
||||
return t;
|
||||
}
|
||||
|
||||
public void setT(String t) {
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
public String getF() {
|
||||
return f;
|
||||
}
|
||||
|
||||
public void setF(String f) {
|
||||
this.f = f;
|
||||
}
|
||||
|
||||
public int getP() {
|
||||
return p;
|
||||
}
|
||||
|
||||
public void setP(int p) {
|
||||
this.p = p;
|
||||
}
|
||||
|
||||
public List<String> getK() {
|
||||
return k;
|
||||
}
|
||||
|
||||
public void setK(List<String> k) {
|
||||
this.k = k;
|
||||
}
|
||||
|
||||
public static String encode(String toolName, String filterHash, int pageNo, List<String> sortKeys) {
|
||||
McpCursor cursor = new McpCursor();
|
||||
cursor.setT(toolName);
|
||||
cursor.setF(filterHash);
|
||||
cursor.setP(pageNo);
|
||||
cursor.setK(sortKeys == null ? Collections.emptyList() : sortKeys);
|
||||
String json = JSON.toJSONString(cursor);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(json.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static McpCursor decode(String encoded) {
|
||||
if (encoded == null || encoded.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
byte[] raw = Base64.getUrlDecoder().decode(encoded.trim());
|
||||
JSONObject object = JSON.parseObject(new String(raw, StandardCharsets.UTF_8));
|
||||
McpCursor cursor = new McpCursor();
|
||||
cursor.setV(object.getIntValue("v"));
|
||||
cursor.setT(object.getString("t"));
|
||||
cursor.setF(object.getString("f"));
|
||||
cursor.setP(object.getIntValue("p"));
|
||||
List<String> keys = new ArrayList<>();
|
||||
if (object.getJSONArray("k") != null) {
|
||||
for (Object item : object.getJSONArray("k")) {
|
||||
keys.add(item == null ? null : String.valueOf(item));
|
||||
}
|
||||
}
|
||||
cursor.setK(keys);
|
||||
return cursor;
|
||||
} catch (Exception ex) {
|
||||
throw McpToolException.invalidParams("cursor 无法解析,请从第一页重新开始(不要手工拼接 cursor)");
|
||||
}
|
||||
}
|
||||
|
||||
public String key(int index) {
|
||||
if (k == null || index >= k.size()) {
|
||||
return null;
|
||||
}
|
||||
return k.get(index);
|
||||
}
|
||||
|
||||
public Long longKey(int index) {
|
||||
String value = key(index);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(value.trim());
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
|
||||
/**
|
||||
* 统一的出参格式化:日期/时间/金额/字符串裁剪。
|
||||
* <p>与现有工具保持一致:日期 yyyy-MM-dd,时间 yyyy-MM-dd HH:mm:ss,金额保留原精度。</p>
|
||||
*/
|
||||
public final class McpFormat {
|
||||
|
||||
private McpFormat() {
|
||||
}
|
||||
|
||||
public static String date(Date value) {
|
||||
return value == null ? null : DateUtil.format(value, "yyyy-MM-dd");
|
||||
}
|
||||
|
||||
public static String dateTime(Date value) {
|
||||
return value == null ? null : DateUtil.format(value, "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
public static BigDecimal decimal(BigDecimal value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一 trim:库中编码字段存在前导制表符等脏数据,入参与出参一律裁剪。
|
||||
*/
|
||||
public static String trim(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* trim 后为空则返回 null,避免输出 "" 造成歧义。
|
||||
*/
|
||||
public static String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 游标分页的统一实现:page_size、cursor 编解码与校验、page_info 信封、limit+1 探测。
|
||||
*/
|
||||
public final class McpPages {
|
||||
|
||||
public static final int DEFAULT_PAGE_SIZE = 20;
|
||||
/** 聚合类(SUMMARY 一般不翻页)上限 */
|
||||
public static final int MAX_AGG_PAGE_SIZE = 200;
|
||||
/** 明细/列表类上限 */
|
||||
public static final int MAX_LIST_PAGE_SIZE = 100;
|
||||
/** 聚合默认最多翻页数 */
|
||||
public static final int DEFAULT_AGG_MAX_PAGES = 20;
|
||||
/** 明细默认最多翻页数 */
|
||||
public static final int DEFAULT_LIST_MAX_PAGES = 50;
|
||||
/** 编码列表单次上限 */
|
||||
public static final int MAX_CODE_LIST = 200;
|
||||
|
||||
private McpPages() {
|
||||
}
|
||||
|
||||
public static int pageSize(Map<String, Object> params, int defaultValue, int max) {
|
||||
Integer value = McpParams.intVal(params, "page_size");
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value <= 0) {
|
||||
throw McpToolException.invalidParams("page_size 必须为正整数");
|
||||
}
|
||||
if (value > max) {
|
||||
throw McpToolException.invalidParams("page_size 超过上限 " + max + ",当前值:" + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并校验游标:工具名、过滤条件指纹必须匹配,且未超过最大页数。
|
||||
*
|
||||
* @return 当前游标(首页返回 null)
|
||||
*/
|
||||
public static McpCursor cursor(Map<String, Object> params, String toolName, String filterHash, int maxPages) {
|
||||
String encoded = McpParams.str(params, "cursor");
|
||||
if (encoded == null) {
|
||||
return null;
|
||||
}
|
||||
McpCursor cursor = McpCursor.decode(encoded);
|
||||
if (cursor == null) {
|
||||
return null;
|
||||
}
|
||||
if (!toolName.equals(cursor.getT())) {
|
||||
throw McpToolException.invalidParams("cursor 与本工具不匹配(cursor 属于 " + cursor.getT()
|
||||
+ "),请从第一页重新开始");
|
||||
}
|
||||
if (cursor.getF() != null && filterHash != null && !cursor.getF().equals(filterHash)) {
|
||||
throw McpToolException.invalidParams("cursor 与当前过滤条件不匹配,请从第一页重新开始");
|
||||
}
|
||||
if (maxPages > 0 && cursor.getP() > maxPages) {
|
||||
throw McpToolException.invalidParams("翻页次数超过上限 " + maxPages
|
||||
+ " 页,请收窄过滤条件,或改用聚合工具(mode=SUMMARY)一次性统计");
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public static Map<String, Object> pageInfo(int returned,
|
||||
int pageSize,
|
||||
boolean hasMore,
|
||||
String nextCursor,
|
||||
String sortBy,
|
||||
int pageNo,
|
||||
Long total,
|
||||
boolean totalCapped,
|
||||
boolean truncatedByBytes) {
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("returned", returned);
|
||||
info.put("page_size", pageSize);
|
||||
info.put("has_more", hasMore);
|
||||
info.put("next_cursor", hasMore ? nextCursor : null);
|
||||
info.put("sort_by", sortBy);
|
||||
info.put("page_no", pageNo);
|
||||
info.put("total_count", total);
|
||||
info.put("total_count_capped", totalCapped);
|
||||
info.put("truncated_by_bytes", truncatedByBytes);
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 探测是否还有下一页:SQL 层取 pageSize + 1 条。
|
||||
*/
|
||||
public static boolean hasMore(List<?> rows, int pageSize) {
|
||||
return rows != null && rows.size() > pageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断到 pageSize 条(配合 limit+1 探测使用)。
|
||||
*/
|
||||
public static <T> List<T> trim(List<T> rows, int pageSize) {
|
||||
if (rows == null) {
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
if (rows.size() <= pageSize) {
|
||||
return rows;
|
||||
}
|
||||
return new java.util.ArrayList<>(rows.subList(0, pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单页内容过大时下调页大小(仅提示,不丢数据)。
|
||||
*/
|
||||
public static int adjustPageSizeForBytes(int pageSize, int estimatedBytes, int maxBytes) {
|
||||
if (estimatedBytes <= maxBytes || pageSize <= 1) {
|
||||
return pageSize;
|
||||
}
|
||||
int adjusted = Math.max(1, pageSize / 2);
|
||||
return adjusted;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 入参解析与校验工具:类型规范化、枚举白名单、编码列表、时间区间、参数冲突。
|
||||
*/
|
||||
public final class McpParams {
|
||||
|
||||
private McpParams() {
|
||||
}
|
||||
|
||||
public static boolean has(Map<String, Object> params, String key) {
|
||||
if (params == null || key == null) {
|
||||
return false;
|
||||
}
|
||||
Object value = params.get(key);
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
if (value instanceof String) {
|
||||
return !((String) value).trim().isEmpty();
|
||||
}
|
||||
if (value instanceof java.util.Collection) {
|
||||
return !((java.util.Collection<?>) value).isEmpty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String str(Map<String, Object> params, String key) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = params.get(key);
|
||||
return value == null ? null : McpFormat.trimToNull(String.valueOf(value));
|
||||
}
|
||||
|
||||
public static String requireStr(Map<String, Object> params, String key) {
|
||||
String value = str(params, key);
|
||||
if (value == null) {
|
||||
throw McpToolException.invalidParams("缺少必填参数:" + key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码列表:支持数组,也支持逗号/中文逗号分隔字符串;统一 trim、去重、校验上限。
|
||||
*/
|
||||
public static List<String> codeList(Map<String, Object> params, String key, int maxCount) {
|
||||
if (params == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Object value = params.get(key);
|
||||
if (value == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> raw = new ArrayList<>();
|
||||
if (value instanceof java.util.Collection) {
|
||||
for (Object item : (java.util.Collection<?>) value) {
|
||||
if (item != null) {
|
||||
raw.add(String.valueOf(item));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String text = String.valueOf(value);
|
||||
for (String part : text.split("[,,\\s]+")) {
|
||||
if (!part.trim().isEmpty()) {
|
||||
raw.add(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<String> dedup = new LinkedHashSet<>();
|
||||
for (String item : raw) {
|
||||
String trimmed = McpFormat.trimToNull(item);
|
||||
if (trimmed != null) {
|
||||
dedup.add(trimmed);
|
||||
}
|
||||
}
|
||||
if (dedup.size() > maxCount) {
|
||||
throw McpToolException.invalidParams("参数 " + key + " 数量过多:" + dedup.size()
|
||||
+ ",单次上限 " + maxCount + ",请分批查询");
|
||||
}
|
||||
return new ArrayList<>(dedup);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多个候选键中取第一个非空的编码列表。
|
||||
*/
|
||||
public static List<String> codeListAny(Map<String, Object> params, int maxCount, String... keys) {
|
||||
if (keys != null) {
|
||||
for (String key : keys) {
|
||||
List<String> list = codeList(params, key, maxCount);
|
||||
if (!list.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static Integer intVal(Map<String, Object> params, String key) {
|
||||
String value = str(params, key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(value);
|
||||
} catch (NumberFormatException ex) {
|
||||
throw McpToolException.invalidParams("参数 " + key + " 必须为整数,当前值:" + value);
|
||||
}
|
||||
}
|
||||
|
||||
public static Integer intVal(Map<String, Object> params, String key, int defaultValue) {
|
||||
Integer value = intVal(params, key);
|
||||
return value == null ? defaultValue : value;
|
||||
}
|
||||
|
||||
public static boolean boolVal(Map<String, Object> params, String key, boolean defaultValue) {
|
||||
if (params == null || params.get(key) == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
Object value = params.get(key);
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
String text = String.valueOf(value).trim().toLowerCase();
|
||||
if ("true".equals(text) || "1".equals(text) || "yes".equals(text) || "是".equals(text)) {
|
||||
return true;
|
||||
}
|
||||
if ("false".equals(text) || "0".equals(text) || "no".equals(text) || "否".equals(text)) {
|
||||
return false;
|
||||
}
|
||||
throw McpToolException.invalidParams("参数 " + key + " 必须为布尔值,当前值:" + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举取值:不在白名单内直接报错并回显允许值(避免静默降级)。
|
||||
*/
|
||||
public static String requireEnum(Map<String, Object> params, String key, Set<String> allowed) {
|
||||
String value = requireStr(params, key);
|
||||
String upper = value.toUpperCase();
|
||||
if (!allowed.contains(upper)) {
|
||||
throw McpToolException.invalidParams("参数 " + key + " 取值非法:" + value + ",允许值:" + allowed);
|
||||
}
|
||||
return upper;
|
||||
}
|
||||
|
||||
public static String enumVal(Map<String, Object> params, String key, Set<String> allowed, String defaultValue) {
|
||||
String value = str(params, key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return requireEnum(params, key, allowed);
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举取值(区分大小写):用于 camelCase 类枚举值(如子列表名 snDetails),不做大写归一。
|
||||
*/
|
||||
public static String requireValue(Map<String, Object> params, String key, Set<String> allowed) {
|
||||
String value = requireStr(params, key);
|
||||
if (!allowed.contains(value)) {
|
||||
throw McpToolException.invalidParams("参数 " + key + " 取值非法:" + value + ",允许值:" + allowed);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static Date date(Map<String, Object> params, String key) {
|
||||
return parseDate(str(params, key), key);
|
||||
}
|
||||
|
||||
public static Date parseDate(String value, String key) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
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 McpToolException.invalidParams("参数 " + key + " 时间格式非法:" + value
|
||||
+ ",支持 yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
private static Date tryParse(String value, String pattern) {
|
||||
try {
|
||||
return DateUtils.dateTime(pattern, value);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间区间,语义 [start, end):
|
||||
* 支持 "start~end" 或 "<key>_start" / "<key>_end";日期仅到天时按当日 23:59:59 之后(即次日零点)为右开边界。
|
||||
*
|
||||
* @param defaultMonths 未传时默认回看月数(<=0 表示不限制/不默认)
|
||||
* @param maxMonths 最大允许跨度月数(<=0 表示不限制)
|
||||
*/
|
||||
public static McpTimeRange timeRange(Map<String, Object> params, String key, int defaultMonths, int maxMonths) {
|
||||
String startText = str(params, key + "_start");
|
||||
String endText = str(params, key + "_end");
|
||||
String combined = str(params, key);
|
||||
if (startText == null && endText == null && combined != null && combined.contains("~")) {
|
||||
String[] parts = combined.split("~", -1);
|
||||
startText = parts.length > 0 ? McpFormat.trimToNull(parts[0]) : null;
|
||||
endText = parts.length > 1 ? McpFormat.trimToNull(parts[1]) : null;
|
||||
}
|
||||
Date start = parseDate(startText, key + "_start");
|
||||
Date end = parseDateInclusiveEnd(endText, key + "_end");
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
if (start == null && end == null) {
|
||||
if (defaultMonths <= 0) {
|
||||
return new McpTimeRange(null, null);
|
||||
}
|
||||
calendar.add(Calendar.MONTH, -defaultMonths);
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||
calendar.set(Calendar.MINUTE, 0);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
start = calendar.getTime();
|
||||
}
|
||||
if (start != null && end != null && !start.before(end)) {
|
||||
throw McpToolException.invalidParams("时间区间非法:起始时间必须早于结束时间");
|
||||
}
|
||||
if (maxMonths > 0 && start != null && end != null) {
|
||||
Calendar limit = Calendar.getInstance();
|
||||
limit.setTime(start);
|
||||
limit.add(Calendar.MONTH, maxMonths);
|
||||
if (end.after(limit.getTime())) {
|
||||
throw McpToolException.invalidParams("时间区间跨度过大(超过 " + maxMonths
|
||||
+ " 个月),请缩小范围或按需分批查询");
|
||||
}
|
||||
}
|
||||
return new McpTimeRange(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期字符串若只到"天",右开边界取次日零点(使 yyyy-MM-dd 的区间对使用者表现为"含当天")。
|
||||
*/
|
||||
private static Date parseDateInclusiveEnd(String value, String key) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
Date parsed = parseDate(value, key);
|
||||
if (value.trim().length() <= 10 && parsed != null) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(parsed);
|
||||
calendar.add(Calendar.DAY_OF_MONTH, 1);
|
||||
return calendar.getTime();
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public static void requireAny(Map<String, Object> params, String... keys) {
|
||||
if (keys != null) {
|
||||
for (String key : keys) {
|
||||
if (has(params, key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw McpToolException.invalidParams("至少需要提供以下参数之一:" + String.join(" / ", keys));
|
||||
}
|
||||
|
||||
public static void reject(boolean condition, String message) {
|
||||
if (condition) {
|
||||
throw McpToolException.invalidParams(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void rejectConflict(Map<String, Object> params, String keyA, String keyB, String message) {
|
||||
if (has(params, keyA) && has(params, keyB)) {
|
||||
throw McpToolException.invalidParams(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤条件指纹:用于校验 cursor 与当前过滤条件一致,防止跨条件串用游标。
|
||||
*/
|
||||
public static String filterHash(String... parts) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (parts != null) {
|
||||
for (String part : parts) {
|
||||
builder.append(part == null ? "" : part.trim()).append('|');
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1");
|
||||
byte[] bytes = digest.digest(builder.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
StringBuilder hex = new StringBuilder();
|
||||
for (byte item : bytes) {
|
||||
hex.append(String.format("%02x", item));
|
||||
}
|
||||
return hex.substring(0, 12);
|
||||
} catch (Exception ex) {
|
||||
return String.valueOf(builder.toString().hashCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅用于日志/元数据的时间描述。
|
||||
*/
|
||||
public static String describe(McpTimeRange range) {
|
||||
if (range == null || range.isEmpty()) {
|
||||
return "不限";
|
||||
}
|
||||
return DateUtil.format(range.getStart(), "yyyy-MM-dd HH:mm:ss") + " ~ "
|
||||
+ DateUtil.format(range.getEnd(), "yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 局部游标(sub_cursor)协议:供"不分页但有界"的点查工具对**子列表**单独翻页,避免超限后直接截断造成数据缺失。
|
||||
*
|
||||
* <p>与主游标({@link McpPages#cursor})的关系:</p>
|
||||
* <ul>
|
||||
* <li>调用方传 {@code sub_list}(子列表名)+ {@code sub_cursor}(游标串),两者与主 {@code cursor} <b>互斥</b>;</li>
|
||||
* <li>首次翻页没有 {@code sub_cursor},需传 {@code sub_parent}(父实体标识,如出库单号 / 应收单号)定位子列表;</li>
|
||||
* <li>返回 {@code data.sub_page_info},结构与 {@code page_info} 完全一致,{@code sort_by} 为该子表的排序键。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>游标编码:沿用 {@link McpCursor} 格式,但 {@code t = "<tool>:<sub_list>"}、{@code f = 父实体标识}、
|
||||
* {@code k[0] = 下一条记录的偏移量}(子列表按主键升序稳定排序,偏移量即幂等续页位置)。</p>
|
||||
*/
|
||||
public final class McpSubPage {
|
||||
|
||||
/** 子列表默认页大小 */
|
||||
public static final int DEFAULT_PAGE_SIZE = 100;
|
||||
/** 子列表单页上限 */
|
||||
public static final int MAX_PAGE_SIZE = 100;
|
||||
/**
|
||||
* 子列表最多翻页数(硬上界,防滥用)。
|
||||
* <p>必须保证"单个父实体的子列表可被完整取完":{@code MAX_PAGES × MAX_PAGE_SIZE = 20000} 行,
|
||||
* 否则会出现"截断 + 游标也取不完"的数据缺失(实测 C-20260715001 有 2682 条 SN 明细)。</p>
|
||||
*/
|
||||
public static final int MAX_PAGES = 200;
|
||||
/** 子列表在主响应中的截断阈值(截断时给出的 next_cursor 从该偏移继续) */
|
||||
public static final int TRUNCATE_LIMIT = 500;
|
||||
|
||||
private McpSubPage() {
|
||||
}
|
||||
|
||||
public static boolean requested(Map<String, Object> params) {
|
||||
return McpParams.has(params, "sub_list");
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加 sub_list / sub_parent / sub_cursor / page_size 四个入参说明。
|
||||
*
|
||||
* @param subListNames 允许的子列表名(英文逗号分隔,同时作为枚举回显)
|
||||
*/
|
||||
public static Map<String, Object> withProperties(Map<String, Object> properties, String subListNames) {
|
||||
List<String> names = Arrays.asList(subListNames.split(","));
|
||||
|
||||
Map<String, Object> subListProperty = new LinkedHashMap<>();
|
||||
subListProperty.put("type", "string");
|
||||
subListProperty.put("description", "局部游标翻页:要翻页的子列表名,取值 " + String.join(" / ", names)
|
||||
+ "。传该参数表示只返回该子列表的一页(而不是整条链路),需同时给出 sub_parent 或 sub_cursor");
|
||||
subListProperty.put("enum", names);
|
||||
|
||||
properties.put("sub_list", subListProperty);
|
||||
|
||||
Map<String, Object> subParentProperty = new LinkedHashMap<>();
|
||||
subParentProperty.put("type", "string");
|
||||
subParentProperty.put("description", "父实体标识,首次翻转子列表时必填(取值见 data.truncated_sub_lists[].parent):"
|
||||
+ "inventory_flow 为出库单号 outer_code,finance_order_position 为应收单号 / 应付单号");
|
||||
properties.put("sub_parent", subParentProperty);
|
||||
|
||||
Map<String, Object> subCursorProperty = new LinkedHashMap<>();
|
||||
subCursorProperty.put("type", "string");
|
||||
subCursorProperty.put("description", "局部游标:原样回传上次返回的 data.sub_page_info.next_cursor 继续取下一页;"
|
||||
+ "游标内已携带父实体标识,无需再传 sub_parent;与 cursor 互斥");
|
||||
properties.put("sub_cursor", subCursorProperty);
|
||||
|
||||
Map<String, Object> pageSizeProperty = new LinkedHashMap<>();
|
||||
pageSizeProperty.put("type", "string");
|
||||
pageSizeProperty.put("description", "子列表每页条数,默认 " + DEFAULT_PAGE_SIZE + ",上限 " + MAX_PAGE_SIZE);
|
||||
properties.put("page_size", pageSizeProperty);
|
||||
return properties;
|
||||
}
|
||||
|
||||
public static int pageSize(Map<String, Object> params) {
|
||||
Integer value = McpParams.intVal(params, "page_size");
|
||||
if (value == null) {
|
||||
return DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
if (value <= 0) {
|
||||
throw McpToolException.invalidParams("page_size 必须为正整数");
|
||||
}
|
||||
if (value > MAX_PAGE_SIZE) {
|
||||
throw McpToolException.invalidParams("子列表 page_size 超过上限 " + MAX_PAGE_SIZE + ",当前值:" + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析局部游标定位信息。
|
||||
*
|
||||
* @param params 入参(含 sub_cursor / sub_parent)
|
||||
* @param toolName 工具名(校验游标归属,并参与生成续页游标)
|
||||
* @param subList 子列表名
|
||||
* @param subListNames 允许的子列表名(用于错误提示)
|
||||
* @param sortKey 该子列表的排序键(写入 sub_page_info.sort_by)
|
||||
*/
|
||||
public static SubCursor resolve(Map<String, Object> params, String toolName, String subList,
|
||||
String subListNames, String sortKey) {
|
||||
McpParams.rejectConflict(params, "sub_cursor", "cursor",
|
||||
"sub_cursor 与 cursor 互斥:子列表翻页请只传 sub_list + sub_cursor");
|
||||
|
||||
String encoded = McpParams.str(params, "sub_cursor");
|
||||
if (encoded == null) {
|
||||
return new SubCursor(toolName, subList, sortKey, null, 0, 1);
|
||||
}
|
||||
McpCursor cursor = McpCursor.decode(encoded);
|
||||
if (!(toolName + ":" + subList).equals(cursor == null ? null : cursor.getT())) {
|
||||
throw McpToolException.invalidParams("sub_cursor 与本工具/子列表不匹配(允许的子列表:" + subListNames
|
||||
+ "),请从该子列表第一页重新开始,不要手工拼接 sub_cursor");
|
||||
}
|
||||
if (cursor.getP() > MAX_PAGES) {
|
||||
throw McpToolException.invalidParams("子列表翻页次数超过上限 " + MAX_PAGES + " 页(每页最多 "
|
||||
+ MAX_PAGE_SIZE + " 条),请改小过滤范围,或将 page_size 提到 " + MAX_PAGE_SIZE + " 后再翻");
|
||||
}
|
||||
Long offset = cursor.longKey(0);
|
||||
if (offset == null || offset < 0) {
|
||||
throw McpToolException.invalidParams("sub_cursor 内容非法,请从该子列表第一页重新开始");
|
||||
}
|
||||
return new SubCursor(toolName, subList, sortKey, cursor.getF(), offset.intValue(), cursor.getP());
|
||||
}
|
||||
|
||||
/**
|
||||
* 取本页数据并生成 sub_page_info。
|
||||
*
|
||||
* @param parent 已确定的父实体标识(首次翻页由 {@code sub_parent} 解析得到,续页时与游标内一致),
|
||||
* 用于把标识写入下一页游标,避免"从 sub_parent 起翻时游标丢父实体"
|
||||
*/
|
||||
public static <T> SubPageResult<T> page(SubCursor state, String parent, List<T> all, int pageSize) {
|
||||
int total = all == null ? 0 : all.size();
|
||||
List<T> slice = slice(all, state.getOffset(), pageSize);
|
||||
boolean hasMore = state.getOffset() + slice.size() < total;
|
||||
String resolvedParent = parent != null ? parent : state.getParent();
|
||||
return new SubPageResult<>(slice, pageInfo(state, resolvedParent, slice.size(), pageSize, hasMore, total));
|
||||
}
|
||||
|
||||
public static Map<String, Object> pageInfo(SubCursor state, String parent, int returned, int pageSize,
|
||||
boolean hasMore, int total) {
|
||||
String nextCursor = hasMore
|
||||
? nextCursor(state.getToolName(), state.getSubList(), parent,
|
||||
state.getOffset() + returned, state.getPageNo() + 1)
|
||||
: null;
|
||||
return McpPages.pageInfo(returned, pageSize, hasMore, nextCursor,
|
||||
state.getSortKey(), state.getPageNo(), (long) total, false, false);
|
||||
}
|
||||
|
||||
public static String nextCursor(String toolName, String subList, String parent, int nextOffset, int nextPageNo) {
|
||||
return McpCursor.encode(toolName + ":" + subList, parent, nextPageNo,
|
||||
Collections.singletonList(String.valueOf(nextOffset)));
|
||||
}
|
||||
|
||||
public static <T> List<T> slice(List<T> all, int offset, int pageSize) {
|
||||
if (all == null || all.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (offset <= 0 && all.size() <= pageSize) {
|
||||
return all;
|
||||
}
|
||||
if (offset >= all.size()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
int end = Math.min(all.size(), offset + pageSize);
|
||||
return new ArrayList<>(all.subList(offset, end));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键 id 升序稳定排序(用于保证"截断点 == 续页起点"的一致性)。
|
||||
* <p>部分子表查询 SQL 没有 order by,直接按返回顺序切片会导致跨调用重复/漏数据,故统一在内存中按 id 定序。</p>
|
||||
*/
|
||||
public static List<Map<String, Object>> sortedMapsById(List<Map<String, Object>> rows) {
|
||||
if (rows == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (rows.size() <= 1) {
|
||||
return rows;
|
||||
}
|
||||
List<Map<String, Object>> copy = new ArrayList<>(rows);
|
||||
copy.sort((left, right) -> compareNullable(
|
||||
left == null ? null : left.get("id"),
|
||||
right == null ? null : right.get("id")));
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static int compareNullable(Object left, Object right) {
|
||||
if (left == null && right == null) {
|
||||
return 0;
|
||||
}
|
||||
if (left == null) {
|
||||
return 1;
|
||||
}
|
||||
if (right == null) {
|
||||
return -1;
|
||||
}
|
||||
if (left instanceof Number && right instanceof Number) {
|
||||
return Long.compare(((Number) left).longValue(), ((Number) right).longValue());
|
||||
}
|
||||
return String.valueOf(left).compareTo(String.valueOf(right));
|
||||
}
|
||||
|
||||
/**
|
||||
* 主响应中被子列表上限截断时的标注项:带父实体标识与可直接使用的续页游标。
|
||||
*/
|
||||
public static Map<String, Object> truncatedItem(String toolName, String subList, String parent,
|
||||
int returned, int total) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("list", subList);
|
||||
item.put("parent", parent);
|
||||
item.put("returned", returned);
|
||||
item.put("total", total);
|
||||
item.put("next_cursor", nextCursor(toolName, subList, parent, returned, 2));
|
||||
item.put("hint", "用 sub_list=" + subList + " 与上述 next_cursor 作为 sub_cursor 继续取下一页");
|
||||
return item;
|
||||
}
|
||||
|
||||
/** 局部游标的定位状态。 */
|
||||
public static final class SubCursor {
|
||||
|
||||
private final String toolName;
|
||||
private final String subList;
|
||||
private final String sortKey;
|
||||
private final String parent;
|
||||
private final int offset;
|
||||
private final int pageNo;
|
||||
|
||||
private SubCursor(String toolName, String subList, String sortKey, String parent, int offset, int pageNo) {
|
||||
this.toolName = toolName;
|
||||
this.subList = subList;
|
||||
this.sortKey = sortKey;
|
||||
this.parent = parent;
|
||||
this.offset = offset;
|
||||
this.pageNo = pageNo;
|
||||
}
|
||||
|
||||
/** 游标中携带的父实体标识(首次翻页为 null,此时需用入参 sub_parent)。 */
|
||||
public String getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public int getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public int getPageNo() {
|
||||
return pageNo;
|
||||
}
|
||||
|
||||
public String getToolName() {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
public String getSubList() {
|
||||
return subList;
|
||||
}
|
||||
|
||||
public String getSortKey() {
|
||||
return sortKey;
|
||||
}
|
||||
}
|
||||
|
||||
/** 子列表的一页数据与分页信封。 */
|
||||
public static final class SubPageResult<T> {
|
||||
|
||||
private final List<T> items;
|
||||
private final Map<String, Object> pageInfo;
|
||||
|
||||
private SubPageResult(List<T> items, Map<String, Object> pageInfo) {
|
||||
this.items = items;
|
||||
this.pageInfo = pageInfo;
|
||||
}
|
||||
|
||||
public List<T> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public Map<String, Object> getPageInfo() {
|
||||
return pageInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 表白名单:MCP 工具只允许访问下列业务表(其余表、尤其是备份表一律禁止)。
|
||||
* <p>备份表白名单外的示例(严禁访问):oms_inventory_info_copy1、delivery_list_0618、
|
||||
* product_info_260916、project_info_20260904bak、oms_purchase_order_1211、oms_payable_bill_copy1 等。</p>
|
||||
*/
|
||||
public final class McpTables {
|
||||
|
||||
public static final Set<String> ALLOWED = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
|
||||
// 仓储
|
||||
"oms_inventory_info", "oms_inventory_inner", "oms_inventory_outer", "oms_inventory_outer_detail",
|
||||
"oms_inventory_delivery", "oms_inventory_delivery_detail", "oms_stock_info", "oms_warehouse_info",
|
||||
"order_delivery", "delivery_list", "project_order_info_recall",
|
||||
// 采购
|
||||
"oms_purchase_order", "oms_purchase_order_item", "oms_purchase_order_map", "oms_purchase_order_history",
|
||||
"oms_purchase_order_item_history", "oms_vendor_info", "project_order_info",
|
||||
// 财务
|
||||
"oms_receivable_bill", "oms_receivable_receipt_plan", "oms_receivable_receipt_detail",
|
||||
"oms_receivable_invoice_plan", "oms_receivable_invoice_detail",
|
||||
"oms_payable_bill", "oms_payable_payment_plan", "oms_payable_payment_detail",
|
||||
"oms_payable_ticket_plan", "oms_payable_ticket_detail",
|
||||
"oms_receipt_bill", "oms_payment_bill", "oms_invoice_bill", "oms_ticket_bill",
|
||||
"oms_finance_charge", "oms_fin_attachment", "oms_company_info",
|
||||
// 项目 / 合同
|
||||
"project_info", "project_product_info", "project_work_progress", "project_poc_info",
|
||||
"oms_quotation", "oms_quotation_product_info", "order_info", "order_list",
|
||||
// 主数据
|
||||
"partner_info", "customer_info", "agent_info", "product_info", "sys_user",
|
||||
// 审批
|
||||
"bu_todo", "bu_todo_completed"
|
||||
)));
|
||||
|
||||
/**
|
||||
* 明确"看起来可用但不采用"的表(方案已确认,禁止回退查询)。
|
||||
*/
|
||||
public static final Set<String> EXCLUDED = Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
|
||||
"vendor_info", "oms_inventory_inner_detail", "oms_inventory_inner_maintenance",
|
||||
"oms_inventory_info_copy1", "delivery_list_0618", "product_info_260916",
|
||||
"project_info_20260904bak", "oms_purchase_order_1211", "oms_payable_bill_copy1",
|
||||
"oms_finance_operate_report"
|
||||
)));
|
||||
|
||||
private McpTables() {
|
||||
}
|
||||
|
||||
public static boolean isAllowed(String table) {
|
||||
return table != null && ALLOWED.contains(table.toLowerCase());
|
||||
}
|
||||
|
||||
public static void assertAllowed(String table) {
|
||||
if (!isAllowed(table)) {
|
||||
throw McpToolException.serverError("禁止访问表:" + table);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 时间区间,语义为 [start, end)(左闭右开),用于时间条件下推。
|
||||
*/
|
||||
public class McpTimeRange {
|
||||
|
||||
private final Date start;
|
||||
private final Date end;
|
||||
|
||||
public McpTimeRange(Date start, Date end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public static McpTimeRange of(Date start, Date end) {
|
||||
return new McpTimeRange(start, end);
|
||||
}
|
||||
|
||||
public Date getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public Date getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return start == null && end == null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.ruoyi.sip.llm.tools.support;
|
||||
|
||||
import com.ruoyi.common.utils.McpErrorUtils;
|
||||
|
||||
/**
|
||||
* MCP 工具异常:携带 JSON-RPC 错误码与错误类型,供 McpController 统一转换为 error 响应。
|
||||
*/
|
||||
public class McpToolException extends RuntimeException {
|
||||
|
||||
private final int code;
|
||||
private final String type;
|
||||
|
||||
public McpToolException(int code, String type, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public static McpToolException invalidParams(String message) {
|
||||
return new McpToolException(McpErrorUtils.INVALID_PARAMS, "invalid_params", message);
|
||||
}
|
||||
|
||||
public static McpToolException authError(String message) {
|
||||
return new McpToolException(McpErrorUtils.AUTH_ERROR, "authorization_error", message);
|
||||
}
|
||||
|
||||
public static McpToolException serverError(String message) {
|
||||
return new McpToolException(McpErrorUtils.SERVER_ERROR, "server_error", message);
|
||||
}
|
||||
|
||||
public static McpToolException rateLimited(String message) {
|
||||
return new McpToolException(McpErrorUtils.RATE_LIMITED, "rate_limit_error", message);
|
||||
}
|
||||
|
||||
public static McpToolException timeout(String message) {
|
||||
return new McpToolException(McpErrorUtils.QUERY_TIMEOUT, "query_timeout", message);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.AgentInfo;
|
||||
|
||||
/**
|
||||
|
|
@ -61,4 +62,11 @@ public interface AgentInfoMapper
|
|||
public int selectCountByCode(AgentInfo agentInfo);
|
||||
|
||||
AgentInfo selectAgentInfoByCode(String agentCode);
|
||||
|
||||
/**
|
||||
* MCP 只读:主数据列表(批量编码 + 名称模糊 + 游标分页,排序键 agent_code,id)。
|
||||
*
|
||||
* @param query codeList / nameKeyword / status / lastCode / lastId / limit
|
||||
*/
|
||||
List<AgentInfo> listAgentMasterPage(Map<String, Object> query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 受限跨域透视(MCP 工具 {@code cross_domain_aggregate})专属只读聚合 Mapper。
|
||||
*
|
||||
* <p>方案依据:{@code docx/mcp-data-tools-plan.md} 第 7 章第 18 项(新增 CrossDomainAggregateMapper.java/.xml)、
|
||||
* 第 9 章(聚合口径)、第 13 章(一次算完,禁止 Agent 翻页累加)、第 15 章(时间区间下推、类型规范化)、16.2(白名单与护栏)。</p>
|
||||
*
|
||||
* <p>★本接口只包含 5 条按链路划分的分组聚合 {@code select},无任何写操作;
|
||||
* 且全部 SQL 均落在本文件对应的 XML 内(不改动任何既有 Mapper 接口/XML),避免与他人改动冲突。</p>
|
||||
*
|
||||
* <p>5 条链路与本文件方法的对应关系:</p>
|
||||
* <ul>
|
||||
* <li>{@link #selectSalesAggregate} :SALES,{@code project_product_info} ⋈ {@code project_order_info} ⋈ {@code project_info}</li>
|
||||
* <li>{@link #selectPurchaseAggregate}:PURCHASE,{@code oms_purchase_order_item} ⋈ {@code oms_purchase_order}</li>
|
||||
* <li>{@link #selectStockAggregate}:STOCK,{@code oms_inventory_info}</li>
|
||||
* <li>{@link #selectReceivableAggregate}:FINANCE_AR,{@code oms_receivable_bill}(金额直接取冗余列,不重算)</li>
|
||||
* <li>{@link #selectPayableAggregate}:FINANCE_AP,{@code oms_payable_bill}(同上)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>通用入参约定(所有方法一致):</p>
|
||||
* <ul>
|
||||
* <li>{@code d0}/{@code d1}/{@code d2}:维度键(链路口径的白名单枚举,如 {@code CUSTOMER}/{@code PRODUCT}/{@code MONTH});
|
||||
* {@code d1}/{@code d2} 为 {@code null} 表示该位置不分组。维度键在 Java 侧已按白名单校验,XML 内用 {@code <choose>} 映射为固定表达式,
|
||||
* <b>不接受任何来自模型的原始 SQL 片段</b>。</li>
|
||||
* <li>{@code orderMetric}:排序度量枚举(按“第一个 metric 降序”),XML 内同样用 {@code <choose>} 映射为固定别名,避免字符串拼接注入。</li>
|
||||
* <li>{@code startTime}/{@code endTime}:时间区间 <b>[start, end)</b>,一律做区间下推({@code >= start and < end}),
|
||||
* 禁止 {@code date_format(...)} 作为过滤条件(见方案 15.1)。</li>
|
||||
* <li>{@code limit}:分组数上限(调用方传 {@code group_limit + 1} 以探测截断)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>返回:{@code List<Map<String,Object>>},每行含 {@code d0/d0Name[、d1/d1Name、d2/d2Name]} + 该链路全部度量列 + {@code rowQty}(参与行数)。</p>
|
||||
*/
|
||||
public interface CrossDomainAggregateMapper {
|
||||
|
||||
/**
|
||||
* SALES 链路聚合:{@code project_product_info} ⋈ {@code project_order_info}(project_id)⋈ {@code project_info}(project_id)。
|
||||
* <p>维度:{@code CUSTOMER}/{@code AGENT}/{@code PRODUCT}/{@code PROJECT}/{@code MONTH}/{@code QUARTER};
|
||||
* 度量:{@code salesAmountWithTax}、{@code salesAmountWithoutTax}、{@code productQty};时间字段 {@code project_order_info.create_time}。</p>
|
||||
*/
|
||||
List<Map<String, Object>> selectSalesAggregate(@Param("d0") String d0,
|
||||
@Param("d1") String d1,
|
||||
@Param("d2") String d2,
|
||||
@Param("orderMetric") String orderMetric,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* PURCHASE 链路聚合:{@code oms_purchase_order_item} ⋈ {@code oms_purchase_order}(purchase_id)。
|
||||
* <p>维度:{@code PRODUCT}/{@code PARTNER}(=制造商,经 {@code oms_purchase_order.vendor_id} → {@code oms_vendor_info})/
|
||||
* {@code MONTH}/{@code QUARTER};度量:{@code purchaseQty}、{@code purchaseAmountWithTax}、{@code purchaseTax};
|
||||
* 时间字段 {@code oms_purchase_order.purchase_date}。</p>
|
||||
*/
|
||||
List<Map<String, Object>> selectPurchaseAggregate(@Param("d0") String d0,
|
||||
@Param("d1") String d1,
|
||||
@Param("d2") String d2,
|
||||
@Param("orderMetric") String orderMetric,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* STOCK 链路聚合:{@code oms_inventory_info} 单表(按需 left join 字典表取名称)。
|
||||
* <p>维度:{@code PRODUCT}/{@code WAREHOUSE}/{@code MONTH}/{@code QUARTER};
|
||||
* 度量:{@code inStockQty}、{@code outStockQty};时间字段 {@code oms_inventory_info.create_time}。</p>
|
||||
*/
|
||||
List<Map<String, Object>> selectStockAggregate(@Param("d0") String d0,
|
||||
@Param("d1") String d1,
|
||||
@Param("d2") String d2,
|
||||
@Param("orderMetric") String orderMetric,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* FINANCE_AR 链路聚合:{@code oms_receivable_bill}(金额列直接 sum 冗余列,不重算)。
|
||||
* <p>维度:{@code CUSTOMER}({@code partner_code},客户)/{@code MONTH}/{@code QUARTER};
|
||||
* 度量:{@code receivableWithTax}、{@code receivedWithTax}、{@code unreceivedWithTax}、{@code uninvoicedWithTax};
|
||||
* 时间字段 {@code oms_receivable_bill.create_time}。</p>
|
||||
*/
|
||||
List<Map<String, Object>> selectReceivableAggregate(@Param("d0") String d0,
|
||||
@Param("d1") String d1,
|
||||
@Param("d2") String d2,
|
||||
@Param("orderMetric") String orderMetric,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* FINANCE_AP 链路聚合:{@code oms_payable_bill}(金额列直接 sum 冗余列,不重算)。
|
||||
* <p>维度:{@code PARTNER}({@code vendor_code},此处 partner 语义为<b>制造商</b>)/{@code MONTH}/{@code QUARTER};
|
||||
* 度量:{@code payableWithTax}、{@code paidWithTax}、{@code unpaidWithTax}、{@code unticketedWithTax};
|
||||
* 时间字段 {@code oms_payable_bill.create_time}。</p>
|
||||
*/
|
||||
List<Map<String, Object>> selectPayableAggregate(@Param("d0") String d0,
|
||||
@Param("d1") String d1,
|
||||
@Param("d2") String d2,
|
||||
@Param("orderMetric") String orderMetric,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.ruoyi.sip.domain.CustomerInfo;
|
|||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -68,4 +69,11 @@ public interface CustomerInfoMapper
|
|||
int selectCountByName(@Param("name") String name,
|
||||
@Param("excludeId") Long excludeId);
|
||||
|
||||
/**
|
||||
* MCP 只读:主数据列表(批量编码 + 名称模糊 + 游标分页,排序键 customer_code,id)。
|
||||
*
|
||||
* @param query codeList / nameKeyword / status / lastCode / lastId / limit
|
||||
*/
|
||||
List<CustomerInfo> listCustomerMasterPage(Map<String, Object> query);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,4 +72,9 @@ public interface DeliveryListMapper
|
|||
void deleteDeliveryListByDeliveryIds(String[] strArray);
|
||||
|
||||
void updateTimeByOrderId(List<Long> longs);
|
||||
|
||||
/**
|
||||
* MCP 只读:按发货单 id 批量查 SN 明细(必须过滤 deleted_at is null)。
|
||||
*/
|
||||
List<DeliveryList> listByDeliveryIdList(@Param("deliveryIdList") List<Long> deliveryIdList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.InventoryDelivery;
|
||||
import com.ruoyi.sip.dto.ApiDataQueryDto;
|
||||
import com.ruoyi.sip.vo.DeliveryApproveVo;
|
||||
|
|
@ -110,4 +111,20 @@ public interface InventoryDeliveryMapper
|
|||
*/
|
||||
Date selectLastRecallTimeByOuterCode(@Param("outerCode") String outerCode);
|
||||
|
||||
// ======================= MCP 只读查询(新增,仅 SELECT) =======================
|
||||
|
||||
/**
|
||||
* MCP 只读:按出库单号列表批量查发货单(现有 selectInventoryDeliveryList 仅支持单个 outerCode)。
|
||||
*/
|
||||
List<InventoryDelivery> listByOuterCodeList(@Param("outerCodeList") List<String> outerCodeList);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=DELIVERY):发货单游标分页(排序键 outer_code,id)。
|
||||
*/
|
||||
List<InventoryDelivery> listDeliveryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=DELIVERY):count。
|
||||
*/
|
||||
long countDeliveryPage(Map<String, Object> params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.InventoryInfo;
|
||||
import com.ruoyi.sip.dto.inventory.ProductWarehouseInfo;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
|
@ -102,4 +103,71 @@ public interface InventoryInfoMapper
|
|||
void clearOrderCodeByIds(@Param("idList") List<Long> idList);
|
||||
|
||||
void clearPayableBillCodeByIds(@Param("idList") List<Long> idList);
|
||||
|
||||
// ======================= MCP 只读查询(新增,仅 SELECT) =======================
|
||||
|
||||
/**
|
||||
* MCP inventory_sn_trace:按产品 SN 列表查库存明细(带 limit 上界,字段含 model/描述/仓库)。
|
||||
*/
|
||||
List<InventoryInfo> listSnTraceByProductSnList(@Param("productSnList") List<String> productSnList,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* MCP inventory_sn_trace:按入库单号列表查库存明细(带 limit 上界)。
|
||||
*/
|
||||
List<InventoryInfo> listSnTraceByInnerCodeList(@Param("innerCodeList") List<String> innerCodeList,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* MCP inventory_sn_trace:按出库单号列表查库存明细(带 limit 上界)。
|
||||
*/
|
||||
List<InventoryInfo> listSnTraceByOuterCodeList(@Param("outerCodeList") List<String> outerCodeList,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* MCP inventory_sn_trace:按合同编号列表反查已出库 SN 明细(带 limit 上界)。
|
||||
*/
|
||||
List<InventoryInfo> listSnTraceByOrderCode(@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* MCP inventory_stock_aggregate:一次全量聚合(SUMMARY)。
|
||||
* groupBy ∈ NONE/PRODUCT/WAREHOUSE/STATUS,按数量倒序取 topN。
|
||||
*/
|
||||
List<Map<String, Object>> selectInventoryStockSummary(@Param("groupBy") String groupBy,
|
||||
@Param("productCodeList") List<String> productCodeList,
|
||||
@Param("warehouseId") Long warehouseId,
|
||||
@Param("includeZero") boolean includeZero,
|
||||
@Param("topN") Integer topN);
|
||||
|
||||
/**
|
||||
* MCP inventory_stock_aggregate(LIST):按产品分页聚合(游标键 product_code,升序)。
|
||||
*/
|
||||
List<Map<String, Object>> selectStockGroupPage(@Param("productCodeList") List<String> productCodeList,
|
||||
@Param("warehouseId") Long warehouseId,
|
||||
@Param("cursor") String cursor,
|
||||
@Param("includeZero") boolean includeZero,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* MCP inventory_stock_aggregate(LIST):按产品码批量取仓库拆分。
|
||||
*/
|
||||
List<Map<String, Object>> selectStockByProductWarehouse(@Param("productCodeList") List<String> productCodeList,
|
||||
@Param("warehouseId") Long warehouseId);
|
||||
|
||||
/**
|
||||
* MCP inventory_stock_aggregate(LIST):count(产品分组数,受 count_cap 保护由调用方处理)。
|
||||
*/
|
||||
long countStockGroups(@Param("productCodeList") List<String> productCodeList,
|
||||
@Param("warehouseId") Long warehouseId);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=SN):SN 明细游标分页(排序键 product_sn,id)。
|
||||
*/
|
||||
List<InventoryInfo> listSnPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=SN):count(受 count_cap 保护由调用方处理)。
|
||||
*/
|
||||
long countSnPage(Map<String, Object> params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.InventoryOuterDetail;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 出库单详情Mapper接口
|
||||
|
|
@ -65,4 +66,9 @@ public interface InventoryOuterDetailMapper
|
|||
|
||||
|
||||
List<InventoryOuterDetail> listMailDataByOuterId(Long id);
|
||||
|
||||
/**
|
||||
* MCP 只读:按出库单号批量查出库明细(消除 N+1,依赖 P0-2 索引 idx_outer_code)。
|
||||
*/
|
||||
List<InventoryOuterDetail> listByOuterCodeList(@Param("outerCodeList") List<String> outerCodeList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.InventoryOuter;
|
||||
import com.ruoyi.sip.domain.VendorInfo;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
|
@ -81,4 +82,16 @@ public interface InventoryOuterMapper
|
|||
* @return 结果
|
||||
*/
|
||||
int updateDeliveryStatusByOuterCode(@Param("outerCode") String outerCode, @Param("deliveryStatus") String deliveryStatus);
|
||||
|
||||
// ======================= MCP 只读查询(新增,仅 SELECT) =======================
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=OUTER):出库单游标分页(排序键 outer_code,id)。
|
||||
*/
|
||||
List<InventoryOuter> listOuterPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=OUTER):count。
|
||||
*/
|
||||
long countOuterPage(Map<String, Object> params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.OmsCompanyInfo;
|
||||
|
||||
/**
|
||||
|
|
@ -58,4 +59,11 @@ public interface OmsCompanyInfoMapper
|
|||
* @return 结果
|
||||
*/
|
||||
public int deleteOmsCompanyInfoByIds(Long[] ids);
|
||||
|
||||
/**
|
||||
* MCP 只读:主数据列表(批量编码 + 名称模糊 + 游标分页,排序键 company_code,id)。
|
||||
*
|
||||
* @param query codeList / nameKeyword / lastCode / lastId / limit
|
||||
*/
|
||||
List<OmsCompanyInfo> listCompanyMasterPage(Map<String, Object> query);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.OmsFinAttachment;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
|
@ -63,4 +64,34 @@ public interface OmsFinAttachmentMapper
|
|||
void deleteOmsFinAttachmentByPayment(@Param("list") List<Long> paymentBillCodeList,@Param("type") String type);
|
||||
|
||||
void deleteOmsFinAttachmentByRelationId(Long id);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】财务附件游标分页查询(仅元数据,不返回 file_path;强制 del_flag='0';排序键 related_bill_id,id)。
|
||||
*
|
||||
* @param relatedBillIdList 关联单据ID列表(bill_code_list 对 ATTACHMENT 即关联单据ID)
|
||||
* @param relatedBillType 关联单据类型(payment/ticket/receipt/invoice)
|
||||
* @param startTime create_time 下界(含,仅在显式传入时间区间时下发)
|
||||
* @param endTime create_time 上界(不含)
|
||||
* @param lastRelatedBillId 游标:上一页最后一行的 related_bill_id
|
||||
* @param lastId 游标:上一页最后一行的 id
|
||||
* @param limit 取数条数(应为 pageSize + 1)
|
||||
*/
|
||||
List<OmsFinAttachment> listAttachmentPage(@Param("relatedBillIdList") List<Long> relatedBillIdList,
|
||||
@Param("relatedBillType") String relatedBillType,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastRelatedBillId") Long lastRelatedBillId,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】财务附件分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countAttachmentPage(@Param("relatedBillIdList") List<Long> relatedBillIdList,
|
||||
@Param("relatedBillType") String relatedBillType,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.OmsFinanceCharge;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 财务计收Mapper接口
|
||||
|
|
@ -65,4 +67,33 @@ public interface OmsFinanceChargeMapper
|
|||
|
||||
int revoke(OmsFinanceCharge omsFinanceCharge);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】财务计收游标分页查询(排序键 order_code,id;时间字段 biz_charge_date 区间下推,禁止 date_format)。
|
||||
*
|
||||
* @param orderCodeList 合同编号列表(bill_code_list 对 CHARGE 即订单号;order_code 亦并入此处)
|
||||
* @param chargeStatus 计收状态(0等待收款/1商务可申请计收/2财务已申请计收/3已完成计收)
|
||||
* @param startTime biz_charge_date 下界(含)
|
||||
* @param endTime biz_charge_date 上界(不含)
|
||||
* @param lastCode 游标:上一页最后一行的 order_code
|
||||
* @param lastId 游标:上一页最后一行的 id
|
||||
* @param limit 取数条数(应为 pageSize + 1)
|
||||
*/
|
||||
List<OmsFinanceCharge> listFinanceChargePage(@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("chargeStatus") String chargeStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastCode") String lastCode,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】财务计收分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countFinanceChargePage(@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("chargeStatus") String chargeStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.OmsInventoryDeliveryDetail;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 产品库存Mapper接口
|
||||
|
|
@ -64,4 +65,9 @@ public interface OmsInventoryDeliveryDetailMapper
|
|||
void deleteOmsInventoryDeliveryDetailByDeliveryId(Long id);
|
||||
|
||||
List<OmsInventoryDeliveryDetail> selectOmsInventoryDeliveryDetailByDeliveryId(Long id);
|
||||
|
||||
/**
|
||||
* MCP 只读:按发货单 id 批量查 SN 明细(消除 N+1,idx_delivery_id 已具备)。
|
||||
*/
|
||||
List<OmsInventoryDeliveryDetail> listByDeliveryIdList(@Param("deliveryIdList") List<Long> deliveryIdList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.OmsInventoryInner;
|
||||
import com.ruoyi.sip.dto.inventory.ProductInventoryInnerDTO;
|
||||
import com.ruoyi.sip.dto.inventory.ProductInventoryQueryDTO;
|
||||
|
|
@ -74,4 +75,16 @@ public interface OmsInventoryInnerMapper
|
|||
List<OmsInventoryInner> selectOmsInventoryInnerByInnerCodeList(List<String> innerCodeList);
|
||||
|
||||
List<ProductInventoryInnerDTO> listInventoryInner(ProductInventoryQueryDTO query);
|
||||
|
||||
// ======================= MCP 只读查询(新增,仅 SELECT) =======================
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=INNER):入库单游标分页(排序键 inner_code,id,时间过滤走 create_time 区间下推)。
|
||||
*/
|
||||
List<OmsInventoryInner> listInnerPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=INNER):count。
|
||||
*/
|
||||
long countInnerPage(Map<String, Object> params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.ruoyi.sip.domain.OmsInvoiceBill;
|
|||
import com.ruoyi.sip.flowable.domain.FlowRelationDto;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -96,4 +97,29 @@ public interface OmsInvoiceBillMapper
|
|||
|
||||
void updateReturnWriteOffBatch(List<OmsInvoiceBill> updateBills);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】开票单游标分页查询(排序键 invoice_bill_code,id;时间字段 invoice_time 区间下推,禁止 date_format)。
|
||||
*/
|
||||
List<OmsInvoiceBill> listInvoiceBillPage(@Param("invoiceBillCodeList") List<String> invoiceBillCodeList,
|
||||
@Param("partnerCodeList") List<String> partnerCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastCode") String lastCode,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】开票单分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countInvoiceBillPage(@Param("invoiceBillCodeList") List<String> invoiceBillCodeList,
|
||||
@Param("partnerCodeList") List<String> partnerCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.OmsPayableBill;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
|
|
@ -81,4 +82,58 @@ public interface OmsPayableBillMapper
|
|||
|
||||
String selectPurcahseNoByPayableBillCode(@Param("payableBillCode") String payableBillCode);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应付单游标分页查询(排序键 payable_bill_code,id;时间字段 create_time 区间下推,禁止 date_format)。
|
||||
*
|
||||
* @param payableBillCodeList 应付单号列表(bill_code_list)
|
||||
* @param orderCodeList 合同编号列表
|
||||
* @param vendorCodeList 制造商编码列表(vendor_code_list)
|
||||
* @param startTime create_time 下界(含)
|
||||
* @param endTime create_time 上界(不含)
|
||||
* @param lastCode 游标:上一页最后一行的 payable_bill_code
|
||||
* @param lastId 游标:上一页最后一行的 id
|
||||
* @param limit 取数条数(应为 pageSize + 1)
|
||||
*/
|
||||
List<OmsPayableBill> listPayableBillPage(@Param("payableBillCodeList") List<String> payableBillCodeList,
|
||||
@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastCode") String lastCode,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应付单分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countPayableBillPage(@Param("payableBillCodeList") List<String> payableBillCodeList,
|
||||
@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应付余额聚合:动态 group_by(NONE / ORDER / PARTNER / TIME_MONTH)。
|
||||
* <p>度量直接取冗余列(sum total_price_with_tax / paid_payment_amount / unpaid_payment_amount / unreceived_ticket_amount),不重算。</p>
|
||||
*
|
||||
* @return 分组行:[groupKey, groupName, payableWithTax, paidWithTax, unpaidWithTax, unticketedWithTax, itemQty]
|
||||
*/
|
||||
List<Map<String, Object>> selectPayableBalanceSummary(@Param("groupBy") String groupBy,
|
||||
@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("keyset") Integer keyset,
|
||||
@Param("lastGroupKey") String lastGroupKey,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应付历史时点重算:Σ应付(create_time ≤ T) 与 Σ已付(payment_time ≤ T)。
|
||||
*
|
||||
* @return totalWithTax=Σ oms_payable_bill.total_price_with_tax,settledAmount=Σ oms_payable_payment_detail.payment_amount
|
||||
*/
|
||||
Map<String, Object> selectPayableAsOf(@Param("asOfDate") Date asOfDate);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.OmsPayableTicketPlan;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 应付单收票计划Mapper接口
|
||||
|
|
@ -68,4 +69,9 @@ public interface OmsPayableTicketPlanMapper
|
|||
* @return 结果
|
||||
*/
|
||||
public int deleteByPayableBillId(Long payableBillId);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】按应付单主键列表批量查询收票计划(消除 N+1)。
|
||||
*/
|
||||
List<OmsPayableTicketPlan> listByPayableBillIdList(@Param("payableBillIdList") List<Long> payableBillIdList);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -104,4 +105,31 @@ public interface OmsPaymentBillMapper
|
|||
List<Map<String, Object>> listRefundSummaryByOriginalBillIds(@Param("originalBillIds") List<Long> originalBillIds,
|
||||
@Param("excludeApproveStatus") String excludeApproveStatus);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】付款单游标分页查询(排序键 payment_bill_code,id;时间字段 payment_time 区间下推,禁止 date_format)。
|
||||
*/
|
||||
List<OmsPaymentBill> listPaymentBillPage(@Param("paymentBillCodeList") List<String> paymentBillCodeList,
|
||||
@Param("orderCode") String orderCode,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastCode") String lastCode,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】付款单分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countPaymentBillPage(@Param("paymentBillCodeList") List<String> paymentBillCodeList,
|
||||
@Param("orderCode") String orderCode,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrderHistory;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -42,4 +45,36 @@ public interface OmsPurchaseOrderHistoryMapper
|
|||
* @return 采购单明细历史记录集合
|
||||
*/
|
||||
public List<com.ruoyi.sip.domain.OmsPurchaseOrderItemHistory> selectOmsPurchaseOrderItemHistoryListByPurchaseHistoryId(Long purchaseHistoryId);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】采购单历史按游标分页查询(排序键 purchase_no,id;limit+1 探测 has_more)。
|
||||
* <p>历史表无 purchase_date 列,时间条件基于 create_time 区间下推(>= startTime and < endTime),不使用 date_format。</p>
|
||||
*
|
||||
* @param purchaseNoList 采购单号列表(code_list)
|
||||
* @param status 采购状态(0待入库 1部分入库 2已完成)
|
||||
* @param approveStatus 审批状态(0草稿 1审批中 2已通过 3驳回)
|
||||
* @param confirmStatus 确认状态(0待确认 1已确认 2已驳回)
|
||||
* @param flowType 流程类型(online/offline)
|
||||
* @param startTime 创建时间下界(含)
|
||||
* @param endTime 创建时间上界(不含)
|
||||
* @param lastPurchaseNo 游标:上一页最后一行的 purchase_no
|
||||
* @param lastId 游标:上一页最后一行的 id
|
||||
* @param limit 取数条数(应为 pageSize + 1)
|
||||
*/
|
||||
public List<OmsPurchaseOrderHistory> listByPurchaseNoList(@Param("purchaseNoList") List<String> purchaseNoList,
|
||||
@Param("status") Integer status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("confirmStatus") String confirmStatus,
|
||||
@Param("flowType") String flowType,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastPurchaseNo") String lastPurchaseNo,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】按采购单历史ID批量查询历史明细(消除 N+1)。
|
||||
*/
|
||||
public List<com.ruoyi.sip.domain.OmsPurchaseOrderItemHistory> listItemByPurchaseHistoryIdList(
|
||||
@Param("purchaseHistoryIdList") List<Long> purchaseHistoryIdList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.ruoyi.sip.mapper;
|
|||
import com.ruoyi.sip.domain.OmsPurchaseOrderMap;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface OmsPurchaseOrderMapMapper {
|
||||
|
||||
OmsPurchaseOrderMap selectByOrderIdAndPurchaseId(@Param("orderId") Long orderId,
|
||||
|
|
@ -14,4 +17,29 @@ public interface OmsPurchaseOrderMapMapper {
|
|||
int updateOmsPurchaseOrderMap(OmsPurchaseOrderMap omsPurchaseOrderMap);
|
||||
|
||||
int initOmsPurchaseOrderMapBindNum(@Param("orderId") Long orderId);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】按订单号(合同编号)批量查询采购-订单绑定行,并补齐采购单号与该表的主键排序键。
|
||||
* <p>注意:{@code oms_purchase_order_map.order_id} 实测关联 {@code project_order_info.id}
|
||||
* (见 {@code OmsPurchaseOrderMapper.listBindOrder} / {@code ProjectOrderInfoMapper}),
|
||||
* 订单号取自 {@code project_order_info.order_code},并按方案 15.14 对编码 join 做 {@code trim()} 对齐。</p>
|
||||
* <p>该表 order_id 无索引,仅适用于小数据量(实测 0/少量行)。排序键 order_id,purchase_id,id(id 为唯一兜底,避免同单同采购多产品行在游标处漏行)。</p>
|
||||
*
|
||||
* @param orderCodeList 订单号(合同编号)列表
|
||||
* @param lastOrderId 游标:上一页最后一行的 order_id
|
||||
* @param lastPurchaseId 游标:上一页最后一行的 purchase_id
|
||||
* @param lastId 游标:上一页最后一行的 id
|
||||
* @param limit 取数条数(应为 pageSize + 1)
|
||||
* @return 行:[orderId, orderCode, purchaseId, purchaseNo, productCode, bindNum]
|
||||
*/
|
||||
List<Map<String, Object>> listByOrderCodeList(@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("lastOrderId") Long lastOrderId,
|
||||
@Param("lastPurchaseId") Long lastPurchaseId,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】按 order_id 批量查询绑定行(辅助方法,无游标)。
|
||||
*/
|
||||
List<OmsPurchaseOrderMap> listByOrderIdList(@Param("orderIdList") List<Long> orderIdList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrder;
|
||||
import com.ruoyi.sip.domain.OmsPurchaseOrderItem;
|
||||
import com.ruoyi.sip.dto.inventory.OmsPurchaseOrderItemDto;
|
||||
|
|
@ -119,4 +121,75 @@ public interface OmsPurchaseOrderMapper
|
|||
|
||||
int recallPurchaseOrder(OmsPurchaseOrder omsPurchaseOrder);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】采购单列表按游标分页查询(排序键 purchase_no,id;limit+1 探测 has_more)。
|
||||
* <p>时间条件基于 purchase_date 区间下推(>= startTime and < endTime),不使用 date_format;逻辑删除过滤 del_flag。</p>
|
||||
*
|
||||
* @param purchaseNoList 采购单号列表(code_list)
|
||||
* @param vendorCodeList 用户指定的制造商编码列表(vendor_code_list)
|
||||
* @param authVendorCodeList 当前用户可访问的制造商编码(行级数据范围)
|
||||
* @param status 采购状态(0待入库 1部分入库 2已完成)
|
||||
* @param approveStatus 审批状态(0草稿 1审批中 2已通过 3驳回)
|
||||
* @param confirmStatus 供应商确认状态(0待确认 1已确认 2已驳回)
|
||||
* @param flowType 流程类型(online/offline)
|
||||
* @param productCodeList 产品编码列表(存在性过滤)
|
||||
* @param startTime 采购日期下界(含)
|
||||
* @param endTime 采购日期上界(不含)
|
||||
* @param lastPurchaseNo 游标:上一页最后一行的 purchase_no
|
||||
* @param lastId 游标:上一页最后一行的 id
|
||||
* @param limit 取数条数(应为 pageSize + 1)
|
||||
*/
|
||||
List<OmsPurchaseOrder> selectPurchaseOrderPage(@Param("purchaseNoList") List<String> purchaseNoList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("authVendorCodeList") List<String> authVendorCodeList,
|
||||
@Param("status") Integer status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("confirmStatus") String confirmStatus,
|
||||
@Param("flowType") String flowType,
|
||||
@Param("productCodeList") List<String> productCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastPurchaseNo") String lastPurchaseNo,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】采购明细列表按游标分页查询(排序键 purchase_no,id,purchase_no 来自 oms_purchase_order 关联)。
|
||||
*/
|
||||
List<OmsPurchaseOrderItem> selectPurchaseOrderItemPage(@Param("purchaseNoList") List<String> purchaseNoList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("authVendorCodeList") List<String> authVendorCodeList,
|
||||
@Param("status") Integer status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("confirmStatus") String confirmStatus,
|
||||
@Param("flowType") String flowType,
|
||||
@Param("productCodeList") List<String> productCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastPurchaseNo") String lastPurchaseNo,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】采购到货汇总:{@code oms_purchase_order} ⋈ {@code oms_purchase_order_item} 动态分组聚合。
|
||||
* <p>group_by 取值为 NONE / ORDER / VENDOR / PRODUCT / STATUS / TIME_MONTH;
|
||||
* 时间条件基于 purchase_date 区间下推(>= startTime and < endTime)。</p>
|
||||
*
|
||||
* @param groupBy 分组维度
|
||||
* @param purchaseNoList 采购单号收窄(可空)
|
||||
* @param vendorCodeList 制造商编码收窄(可空)
|
||||
* @param authVendorCodeList 行级数据范围(可空)
|
||||
* @param startTime 采购日期下界(含)
|
||||
* @param endTime 采购日期上界(不含)
|
||||
* @param limit top_n(NONE 时取 1)
|
||||
* @return 分组行:[groupKey, vendorCode, vendorName, purchaseQty, innerQty, pendingQty, amountWithTax, taxTotal]
|
||||
*/
|
||||
List<Map<String, Object>> selectPurchaseArrivalSummary(@Param("groupBy") String groupBy,
|
||||
@Param("purchaseNoList") List<String> purchaseNoList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("authVendorCodeList") List<String> authVendorCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("limit") int limit);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.ruoyi.sip.domain.OmsReceiptBill;
|
|||
import com.ruoyi.sip.flowable.domain.FlowRelationDto;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
|
@ -62,4 +63,31 @@ public interface OmsReceiptBillMapper {
|
|||
|
||||
void revoke(OmsReceiptBill omsReceiptBill);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】收款单游标分页查询(排序键 receipt_bill_code,id;时间字段 receipt_time 区间下推,禁止 date_format)。
|
||||
*/
|
||||
List<OmsReceiptBill> listReceiptBillPage(@Param("receiptBillCodeList") List<String> receiptBillCodeList,
|
||||
@Param("orderCode") String orderCode,
|
||||
@Param("partnerCodeList") List<String> partnerCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastCode") String lastCode,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】收款单分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countReceiptBillPage(@Param("receiptBillCodeList") List<String> receiptBillCodeList,
|
||||
@Param("orderCode") String orderCode,
|
||||
@Param("partnerCodeList") List<String> partnerCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.OmsReceivableBill;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 销售应收单Mapper接口
|
||||
|
|
@ -79,4 +82,67 @@ public interface OmsReceivableBillMapper
|
|||
|
||||
List<OmsReceivableBill> listReceivableBill(OmsReceivableBill queryUpdateAllReceivableBill);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应收单游标分页查询(排序键 receivable_bill_code,id;时间字段 create_time 区间下推,禁止 date_format)。
|
||||
*
|
||||
* @param receivableBillCodeList 应收单号列表(bill_code_list)
|
||||
* @param orderCodeList 合同编号列表(order_code / 由单号反查得到)
|
||||
* @param partnerCodeList 客户编码列表(partner_code_list)
|
||||
* @param startTime 创建时间下界(含)
|
||||
* @param endTime 创建时间上界(不含)
|
||||
* @param lastCode 游标:上一页最后一行的 receivable_bill_code
|
||||
* @param lastId 游标:上一页最后一行的 id
|
||||
* @param limit 取数条数(应为 pageSize + 1)
|
||||
*/
|
||||
List<OmsReceivableBill> listReceivableBillPage(@Param("receivableBillCodeList") List<String> receivableBillCodeList,
|
||||
@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("partnerCodeList") List<String> partnerCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastCode") String lastCode,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应收单分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countReceivableBillPage(@Param("receivableBillCodeList") List<String> receivableBillCodeList,
|
||||
@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("partnerCodeList") List<String> partnerCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应收余额聚合:动态 group_by(NONE / ORDER / PARTNER / TIME_MONTH / OVERDUE_BUCKET)。
|
||||
* <p>度量直接取冗余列(sum total_price_with_tax / received_amount / unreceived_amount / uninvoiced_amount),不重算。</p>
|
||||
*
|
||||
* @param groupBy 分组维度
|
||||
* @param orderCodeList 合同编号收窄(可空)
|
||||
* @param partnerCodeList 客户编码收窄(可空)
|
||||
* @param startTime create_time 下界(含)
|
||||
* @param endTime create_time 上界(不含)
|
||||
* @param keyset 1=按 order_code 升序的 keyset 分页(LIST);其余=按度量降序(SUMMARY / top_n)
|
||||
* @param lastGroupKey keyset 游标:上一页最后一行的 order_code(仅 keyset=1 生效)
|
||||
* @param limit top_n(NONE 取 1)
|
||||
* @return 分组行:[groupKey, groupName, receivableWithTax, receivedWithTax, unreceivedWithTax, uninvoicedWithTax, itemQty]
|
||||
*/
|
||||
List<Map<String, Object>> selectReceivableBalanceSummary(@Param("groupBy") String groupBy,
|
||||
@Param("orderCodeList") List<String> orderCodeList,
|
||||
@Param("partnerCodeList") List<String> partnerCodeList,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("keyset") Integer keyset,
|
||||
@Param("lastGroupKey") String lastGroupKey,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】应收历史时点重算:Σ应收(create_time ≤ T) 与 Σ已收(receipt_time ≤ T)。
|
||||
*
|
||||
* @param asOfDate 时点日期 T
|
||||
* @return totalWithTax=Σ oms_receivable_bill.total_price_with_tax,settledAmount=Σ oms_receivable_receipt_detail.receipt_amount
|
||||
*/
|
||||
Map<String, Object> selectReceivableAsOf(@Param("asOfDate") Date asOfDate);
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.OmsReceivableInvoicePlan;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 应收单开票计划Mapper接口
|
||||
|
|
@ -69,4 +70,9 @@ public interface OmsReceivableInvoicePlanMapper
|
|||
*/
|
||||
public int deleteByReceivableBillId(Long receivableBillId);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】按应收单主键列表批量查询开票计划(消除 N+1)。
|
||||
*/
|
||||
List<OmsReceivableInvoicePlan> listByReceivableBillIdList(@Param("receivableBillIdList") List<Long> receivableBillIdList);
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.OmsReceivableReceiptPlan;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 应收单收款计划Mapper接口
|
||||
|
|
@ -76,4 +77,9 @@ public interface OmsReceivableReceiptPlanMapper
|
|||
public int deleteByReceivableBillId(Long receivableBillId);
|
||||
|
||||
OmsReceivableReceiptPlan firstUnPayPlan(Long id);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】按应收单主键列表批量查询收款计划(消除 N+1)。
|
||||
*/
|
||||
List<OmsReceivableReceiptPlan> listByReceivableBillIdList(@Param("receivableBillIdList") List<Long> receivableBillIdList);
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.ruoyi.sip.mapper;
|
|||
import com.ruoyi.sip.domain.OmsStockInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author ch
|
||||
|
|
@ -46,4 +47,16 @@ public interface OmsStockInfoMapper {
|
|||
*/
|
||||
int batchRemove(String[] ids);
|
||||
|
||||
// ======================= MCP 只读查询(新增,仅 SELECT) =======================
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=STOCK):备货信息游标分页(排序键 order_code,id)。
|
||||
*/
|
||||
List<OmsStockInfo> listStockPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=STOCK):count。
|
||||
*/
|
||||
long countStockPage(Map<String, Object> params);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.ruoyi.sip.domain.OmsTicketBill;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
|
@ -72,4 +73,29 @@ public interface OmsTicketBillMapper
|
|||
|
||||
void revokeTicket(OmsTicketBill updateBill);
|
||||
|
||||
/* ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== */
|
||||
|
||||
/**
|
||||
* 【MCP 只读】收票单游标分页查询(排序键 ticket_bill_code,id;时间字段 ticket_time 区间下推,禁止 date_format)。
|
||||
*/
|
||||
List<OmsTicketBill> listTicketBillPage(@Param("ticketBillCodeList") List<String> ticketBillCodeList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime,
|
||||
@Param("lastCode") String lastCode,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 【MCP 只读】收票单分页过滤条件下的总条数(不翻页)。
|
||||
*/
|
||||
long countTicketBillPage(@Param("ticketBillCodeList") List<String> ticketBillCodeList,
|
||||
@Param("vendorCodeList") List<String> vendorCodeList,
|
||||
@Param("status") String status,
|
||||
@Param("approveStatus") String approveStatus,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.ruoyi.sip.mapper;
|
|||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.OrderDelivery;
|
||||
|
||||
/**
|
||||
|
|
@ -61,4 +62,17 @@ public interface OrderDeliveryMapper
|
|||
public int deleteOrderDeliveryByIds(String[] ids);
|
||||
|
||||
int count(OrderDelivery orderDelivery);
|
||||
|
||||
// ======================= MCP 只读查询(新增,仅 SELECT) =======================
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=ORDER_DELIVERY):manage 域发货单游标分页(排序键 delivery_code,id)。
|
||||
* 强制 deleted_at is null,left join order_info(trim(order_code) 对齐)补合同/客户信息。
|
||||
*/
|
||||
List<Map<String, Object>> listOrderDeliveryPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=ORDER_DELIVERY):count。
|
||||
*/
|
||||
long countOrderDeliveryPage(Map<String, Object> params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.sip.domain.MaintenanceRecordsDto;
|
||||
|
|
@ -8,6 +9,7 @@ import com.ruoyi.sip.domain.OrderInfo;
|
|||
import com.ruoyi.sip.domain.OrderList;
|
||||
import com.ruoyi.sip.dto.HomepageQueryDto;
|
||||
import com.ruoyi.sip.dto.StatisticsDetailDto;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 合同档案Mapper接口
|
||||
|
|
@ -117,4 +119,25 @@ public interface OrderInfoMapper
|
|||
List<StatisticsDetailDto> homePageData(HomepageQueryDto dto);
|
||||
|
||||
void updateTimeByDeliveryId(List<Long> deliveryId);
|
||||
|
||||
/**
|
||||
* MCP 只读(manage 域合同列表):order_code 批量(trim 对齐)/ 名称模糊 / 客户 / 代理商 / 合同类型 /
|
||||
* 签订日期区间 + 游标分页,排序键 order_code,version_code,id;默认过滤 deleted_at is null。
|
||||
*
|
||||
* @param query codeList / nameKeyword / customerCodeList / agentCodeList / orderType / status /
|
||||
* startTime / endTime / lastCode / lastVersion / lastId / limit
|
||||
*/
|
||||
List<OrderInfo> listContractPage(Map<String, Object> query);
|
||||
|
||||
/**
|
||||
* MCP 只读(合同明细):order_id 批量 + 产品编码批量 + 游标分页,排序键 order_id,id;默认过滤 deleted_at is null。
|
||||
*
|
||||
* @param query orderIdList / productCodeList / lastOrderId / lastId / limit
|
||||
*/
|
||||
List<OrderList> listContractProductPage(Map<String, Object> query);
|
||||
|
||||
/**
|
||||
* MCP 只读:按合同ID批量取合同清单明细(include_detail=true)。
|
||||
*/
|
||||
List<OrderList> listOrderListByOrderIdList(@Param("list") List<Long> orderIdList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.PartnerInfo;
|
||||
|
||||
/**
|
||||
|
|
@ -64,4 +65,11 @@ public interface PartnerInfoMapper
|
|||
List<PartnerInfo> listByOrderCode(List<String> orderCodeList);
|
||||
|
||||
int selectMaxByPrefix(String string);
|
||||
|
||||
/**
|
||||
* MCP 只读:主数据列表(批量编码 + 名称模糊 + 游标分页,排序键 partner_code,id)。
|
||||
*
|
||||
* @param query codeList / nameKeyword / status / lastCode / lastId / limit
|
||||
*/
|
||||
List<PartnerInfo> listPartnerMasterPage(Map<String, Object> query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.ruoyi.sip.domain.MaintenanceRecordsDto;
|
||||
import com.ruoyi.sip.domain.ProductInfo;
|
||||
|
|
@ -81,4 +82,11 @@ public interface ProductInfoMapper
|
|||
List<ProductInfo> listInventory(ProductInfo info);
|
||||
|
||||
ProductInfo selectProductInfoByBomCode(String productCode);
|
||||
|
||||
/**
|
||||
* MCP 只读:主数据列表(批量编码 + 名称模糊 + 产品类型 + 游标分页,排序键 product_code,id)。
|
||||
*
|
||||
* @param query codeList / nameKeyword / type / status / lastCode / lastId / limit
|
||||
*/
|
||||
List<ProductInfo> listProductMasterPage(Map<String, Object> query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.sip.domain.ProjectInfo;
|
||||
|
|
@ -77,4 +78,11 @@ public interface ProjectInfoMapper
|
|||
ProjectInfo selectProjectInfoByOrderCode(String orderCode);
|
||||
|
||||
List<ProjectInfo> selectProjectInfoByOrderCodeList(List<String> orderCodeList);
|
||||
|
||||
/**
|
||||
* MCP 只读:项目列表(编码批量 + 名称模糊 + 客户/代表处 + 时间区间 + 游标分页,排序键 project_code,id)。
|
||||
*
|
||||
* @param query codeList / nameKeyword / customerCodeList / agentCodeList / startTime / endTime / lastCode / lastId / limit
|
||||
*/
|
||||
List<ProjectInfo> listProjectPage(Map<String, Object> query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.ProjectOrderInfoRecall;
|
||||
import com.ruoyi.sip.dto.ApiDataQueryDto;
|
||||
import com.ruoyi.sip.vo.ProjectOrderInfoRecallVO;
|
||||
|
|
@ -23,4 +24,15 @@ public interface ProjectOrderInfoRecallMapper
|
|||
|
||||
List<ProjectOrderInfoRecallVO> getOrderRecallInfo(ApiDataQueryDto dto);
|
||||
|
||||
// ======================= MCP 只读查询(新增,仅 SELECT) =======================
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=RECALL):撤回历史游标分页(排序键 order_code,create_time desc,id)。
|
||||
*/
|
||||
List<ProjectOrderInfoRecall> listRecallPage(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* MCP warehouse_list(entity=RECALL):count。
|
||||
*/
|
||||
long countRecallPage(Map<String, Object> params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.ProjectPocInfo;
|
||||
import com.ruoyi.sip.domain.ProjectPocInfoDetail;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* poc测试信息Mapper接口
|
||||
|
|
@ -88,4 +90,16 @@ public interface ProjectPocInfoMapper
|
|||
List<ProjectPocInfo> listByProjectId(List<Long> longs);
|
||||
|
||||
void updateBatchProjectPocInfoDetail(List<ProjectPocInfoDetail> updateList);
|
||||
|
||||
/**
|
||||
* MCP 只读:POC 列表(项目ID批量 + 游标分页,排序键 project_id,id)。
|
||||
*
|
||||
* @param query projectIdList / lastProjectId / lastId / limit
|
||||
*/
|
||||
List<ProjectPocInfo> listProjectPocPage(Map<String, Object> query);
|
||||
|
||||
/**
|
||||
* MCP 只读:按 POC 主键批量取测试进展明细(include_detail=true)。
|
||||
*/
|
||||
List<ProjectPocInfoDetail> listPocDetailByPocIdList(@Param("list") List<Long> pocIdList);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.ruoyi.sip.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.sip.domain.ProjectProductInfo;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
|
|
@ -72,4 +73,11 @@ public interface ProjectProductInfoMapper
|
|||
void restoreData(@Param("orderCode")String orderCode, @Param("versionCode")String versionCode,@Param("projectId") Long projectId);
|
||||
|
||||
int countBakData(@Param("orderCode")String orderCode,@Param("versionCode") String versionCode);
|
||||
|
||||
/**
|
||||
* MCP 只读分页(新增,仅 SELECT):项目产品列表(项目ID批量 + 产品编码批量 + 游标分页,排序键 project_id,id)。
|
||||
*
|
||||
* @param query projectIdList / productCodeList / lastProjectId / lastId / limit
|
||||
*/
|
||||
List<ProjectProductInfo> listProjectProductPage(Map<String, Object> query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.ruoyi.sip.mapper;
|
|||
import com.ruoyi.sip.domain.Quotation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Author makejava
|
||||
|
|
@ -47,4 +48,11 @@ public interface QuotationMapper {
|
|||
*/
|
||||
int batchRemove(Integer[] ids);
|
||||
|
||||
/**
|
||||
* MCP 只读:报价单列表(单号批量 + 名称模糊 + 代表处 + 时间区间 + 游标分页,排序键 quotation_code,id)。
|
||||
*
|
||||
* @param query codeList / nameKeyword / agentCodeList / startTime / endTime / lastCode / lastId / limit
|
||||
*/
|
||||
List<Quotation> listQuotationPage(Map<String, Object> query);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.ruoyi.sip.mapper;
|
|||
import com.ruoyi.sip.domain.VendorInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -62,4 +63,12 @@ public interface VendorInfoMapper
|
|||
public int deleteVendorInfoByVendorIds(String[] vendorIds);
|
||||
|
||||
VendorInfo selectVendorInfoByVendorCode(String vendorCode);
|
||||
|
||||
/**
|
||||
* MCP 只读:主数据列表(批量编码 + 名称模糊 + 合作状态 + 游标分页,排序键 vendor_code,vendor_id)。
|
||||
* 只查 oms_vendor_info,不回退 vendor_info 冗余表。
|
||||
*
|
||||
* @param query codeList / nameKeyword / status / lastCode / lastId / limit
|
||||
*/
|
||||
List<VendorInfo> listVendorMasterPage(Map<String, Object> query);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -657,6 +657,66 @@
|
|||
delete from oms_receivable_receipt_detail where receipt_bill_code=#{code}
|
||||
</delete>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=RECEIPT):收款单游标分页,排序键 receipt_bill_code,id,时间字段 receipt_time -->
|
||||
<select id="listReceiptBillPage" resultType="com.ruoyi.sip.domain.OmsReceiptBill">
|
||||
select
|
||||
t1.id, t1.receipt_bill_code, t1.receipt_bill_type, t1.receipt_time, t1.actual_receipt_time,
|
||||
t1.partner_code, t2.partner_name, t1.order_code,
|
||||
t1.total_price_with_tax, t1.total_price_without_tax, t1.tax_amount, t1.remaining_amount,
|
||||
t1.receipt_status, t1.approve_status, t1.approve_time,
|
||||
t1.receipt_method, t1.receipt_account_name, t1.receipt_bank_number, t1.receipt_bank_open_address,
|
||||
t1.bank_number, t1.refund_status, t1.project_code, t1.project_name,
|
||||
t1.create_by, t1.create_time, t1.update_by, t1.update_time, t1.remark
|
||||
from oms_receipt_bill t1
|
||||
left join partner_info t2 on t1.partner_code = t2.partner_code
|
||||
<where>
|
||||
<if test="receiptBillCodeList != null and receiptBillCodeList.size > 0">
|
||||
and t1.receipt_bill_code in
|
||||
<foreach item="item" collection="receiptBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCode != null and orderCode != ''"> and t1.order_code = #{orderCode}</if>
|
||||
<if test="partnerCodeList != null and partnerCodeList.size > 0">
|
||||
and t1.partner_code in
|
||||
<foreach item="item" collection="partnerCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and t1.receipt_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t1.approve_status = #{approveStatus}</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.receipt_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.receipt_time < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 receipt_bill_code,id) -->
|
||||
<if test="lastCode != null and lastCode != ''">
|
||||
and (t1.receipt_bill_code > #{lastCode}
|
||||
or (t1.receipt_bill_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.receipt_bill_code asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=RECEIPT):include_total 计数 -->
|
||||
<select id="countReceiptBillPage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_receipt_bill t1
|
||||
<where>
|
||||
<if test="receiptBillCodeList != null and receiptBillCodeList.size > 0">
|
||||
and t1.receipt_bill_code in
|
||||
<foreach item="item" collection="receiptBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCode != null and orderCode != ''"> and t1.order_code = #{orderCode}</if>
|
||||
<if test="partnerCodeList != null and partnerCodeList.size > 0">
|
||||
and t1.partner_code in
|
||||
<foreach item="item" collection="partnerCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and t1.receipt_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t1.approve_status = #{approveStatus}</if>
|
||||
<if test="startTime != null"> and t1.receipt_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.receipt_time < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,35 @@
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ============ MCP 只读查询(新增,仅 SELECT;不影响既有语句) ============ -->
|
||||
<select id="listQuotationPage" resultType="com.ruoyi.sip.domain.Quotation">
|
||||
select t1.id, t1.quotation_code, t1.quotation_name, t1.quotation_amount, t1.discount_amount,
|
||||
t1.quotation_status, t1.agent_code, t1.amount_type, t1.customer_name,
|
||||
t1.create_by, t1.create_time, t1.update_by, t1.update_time, t1.remark
|
||||
from oms_quotation t1
|
||||
<where>
|
||||
<if test="codeList != null and codeList.size() > 0">
|
||||
and t1.quotation_code in
|
||||
<foreach item="item" collection="codeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="nameKeyword != null and nameKeyword != ''">
|
||||
and t1.quotation_name like concat('%', #{nameKeyword}, '%')
|
||||
</if>
|
||||
<if test="agentCodeList != null and agentCodeList.size() > 0">
|
||||
and t1.agent_code in
|
||||
<foreach item="item" collection="agentCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null">and t1.create_time <![CDATA[ >= ]]> #{startTime}</if>
|
||||
<if test="endTime != null">and t1.create_time <![CDATA[ < ]]> #{endTime}</if>
|
||||
<if test="lastCode != null and lastId != null">
|
||||
and (t1.quotation_code > #{lastCode}
|
||||
or (t1.quotation_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.quotation_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -137,4 +137,50 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</update>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=ATTACHMENT):附件元数据游标分页(不返回 file_path;强制 del_flag='0';排序键 related_bill_id,id) -->
|
||||
<select id="listAttachmentPage" resultType="com.ruoyi.sip.domain.OmsFinAttachment">
|
||||
select
|
||||
t1.id, t1.related_bill_id, t1.related_bill_type, t1.file_name, t1.file_size, t1.file_type,
|
||||
t1.price_with_tax, t1.price_without_tax, t1.del_flag, t1.create_time, t1.update_time, t1.remark,
|
||||
t2.user_name as create_by_name
|
||||
from oms_fin_attachment t1
|
||||
left join sys_user t2 on t1.create_by = t2.user_id
|
||||
<where>
|
||||
t1.del_flag = '0'
|
||||
<if test="relatedBillIdList != null and relatedBillIdList.size > 0">
|
||||
and t1.related_bill_id in
|
||||
<foreach item="item" collection="relatedBillIdList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="relatedBillType != null and relatedBillType != ''"> and t1.related_bill_type = #{relatedBillType}</if>
|
||||
<!-- 时间区间下推(可选),禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 related_bill_id,id) -->
|
||||
<if test="lastRelatedBillId != null">
|
||||
and (t1.related_bill_id > #{lastRelatedBillId}
|
||||
or (t1.related_bill_id = #{lastRelatedBillId} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.related_bill_id asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=ATTACHMENT):include_total 计数 -->
|
||||
<select id="countAttachmentPage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_fin_attachment t1
|
||||
<where>
|
||||
t1.del_flag = '0'
|
||||
<if test="relatedBillIdList != null and relatedBillIdList.size > 0">
|
||||
and t1.related_bill_id in
|
||||
<foreach item="item" collection="relatedBillIdList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="relatedBillType != null and relatedBillType != ''"> and t1.related_bill_type = #{relatedBillType}</if>
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -443,4 +443,58 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=CHARGE):财务计收游标分页,排序键 order_code,id,时间字段 biz_charge_date -->
|
||||
<select id="listFinanceChargePage" resultType="com.ruoyi.sip.domain.OmsFinanceCharge">
|
||||
select
|
||||
t1.id, t1.order_code, t1.biz_charge_date, t1.finance_charge_date,
|
||||
t1.income_with_tax_total, t1.income_without_tax_total,
|
||||
t1.cost_software_without_tax, t1.cost_software_with_tax,
|
||||
t1.cost_hardware_without_tax, t1.cost_hardware_with_tax,
|
||||
t1.cost_software_maint_without_tax, t1.cost_software_maint_with_tax,
|
||||
t1.cost_hardware_maint_without_tax, t1.cost_hardware_maint_with_tax,
|
||||
t1.cost_province_service_with_tax, t1.cost_province_service_without_tax,
|
||||
t1.cost_other_with_tax, t1.cost_other_without_tax,
|
||||
t1.charge_status, t1.remark, t1.create_time, t1.update_time,
|
||||
t3.project_code, t3.project_name, t2.order_channel, t2.supplier,
|
||||
t2.partner_code, t4.partner_name
|
||||
from oms_finance_charge t1
|
||||
left join project_order_info t2 on t1.order_code = t2.order_code
|
||||
left join project_info t3 on t2.project_id = t3.id
|
||||
left join partner_info t4 on t2.partner_code = t4.partner_code
|
||||
<where>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="chargeStatus != null and chargeStatus != ''"> and t1.charge_status = #{chargeStatus}</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.biz_charge_date >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.biz_charge_date < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 order_code,id) -->
|
||||
<if test="lastCode != null and lastCode != ''">
|
||||
and (t1.order_code > #{lastCode}
|
||||
or (t1.order_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.order_code asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=CHARGE):include_total 计数 -->
|
||||
<select id="countFinanceChargePage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_finance_charge t1
|
||||
<where>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="chargeStatus != null and chargeStatus != ''"> and t1.charge_status = #{chargeStatus}</if>
|
||||
<if test="startTime != null"> and t1.biz_charge_date >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.biz_charge_date < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
@ -362,5 +362,140 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
inner join oms_inventory_inner as t2 on t1.inner_code = t2.inner_code
|
||||
inner join oms_purchase_order as t3 on t2.purchase_no = t3.purchase_no
|
||||
</select>
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=PAYABLE) / finance_order_position:应付单游标分页,排序键 payable_bill_code,id -->
|
||||
<select id="listPayableBillPage" resultType="com.ruoyi.sip.domain.OmsPayableBill">
|
||||
select
|
||||
t1.id, t1.payable_bill_code, t1.vendor_code, v.vendor_name, t1.order_code, t1.inventory_code,
|
||||
t1.product_type, t1.product_code, pro.level2_type as product_level2_type,
|
||||
t1.total_price_with_tax, t1.total_price_without_tax, t1.tax_amount, t1.tax_rate,
|
||||
t1.paid_payment_amount, t1.unpaid_payment_amount,
|
||||
t1.received_ticket_amount, t1.unreceived_ticket_amount,
|
||||
t1.last_payment_plan_id, t1.last_ticket_plan_id,
|
||||
p1.project_code, p1.project_name,
|
||||
ppp.plan_payment_date, ppp.plan_amount,
|
||||
tp.plan_ticket_date, tp.plan_amount as plan_ticket_amount,
|
||||
t1.create_by, t1.create_time, t1.update_by, t1.update_time, t1.remark, t1.del_flag
|
||||
from oms_payable_bill t1
|
||||
left join project_order_info p0 on t1.order_code = p0.order_code
|
||||
left join project_info p1 on p0.project_id = p1.id
|
||||
left join oms_vendor_info v on t1.vendor_code = v.vendor_code
|
||||
left join oms_payable_payment_plan ppp on t1.last_payment_plan_id = ppp.id
|
||||
left join oms_payable_ticket_plan tp on t1.last_ticket_plan_id = tp.id
|
||||
left join product_info pro on t1.product_code = pro.product_code
|
||||
<where>
|
||||
<if test="payableBillCodeList != null and payableBillCodeList.size > 0">
|
||||
and t1.payable_bill_code in
|
||||
<foreach item="item" collection="payableBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and t1.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 payable_bill_code,id) -->
|
||||
<if test="lastCode != null and lastCode != ''">
|
||||
and (t1.payable_bill_code > #{lastCode}
|
||||
or (t1.payable_bill_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.payable_bill_code asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=PAYABLE):include_total 计数 -->
|
||||
<select id="countPayableBillPage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_payable_bill t1
|
||||
<where>
|
||||
<if test="payableBillCodeList != null and payableBillCodeList.size > 0">
|
||||
and t1.payable_bill_code in
|
||||
<foreach item="item" collection="payableBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and t1.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- finance_balance_aggregate(应付侧):动态分组聚合(SUMMARY 全量 / LIST 按 order_code keyset) -->
|
||||
<select id="selectPayableBalanceSummary" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'"> t1.order_code as groupKey </when>
|
||||
<when test="groupBy == 'PARTNER'"> t1.vendor_code as groupKey, max(v.vendor_name) as groupName </when>
|
||||
<!-- 分组桶用 date_format(仅分组表达式,不参与索引过滤);时间过滤一律区间下推 -->
|
||||
<when test="groupBy == 'TIME_MONTH'"> date_format(t1.create_time, '%Y-%m') as groupKey </when>
|
||||
<otherwise> 'TOTAL' as groupKey </otherwise>
|
||||
</choose>
|
||||
, sum(t1.total_price_with_tax) as payableWithTax
|
||||
, sum(t1.paid_payment_amount) as paidWithTax
|
||||
, sum(t1.unpaid_payment_amount) as unpaidWithTax
|
||||
, sum(t1.unreceived_ticket_amount) as unticketedWithTax
|
||||
, count(*) as itemQty
|
||||
from oms_payable_bill t1
|
||||
left join oms_vendor_info v on t1.vendor_code = v.vendor_code
|
||||
<where>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and t1.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
<if test="keyset != null and keyset == 1 and lastGroupKey != null and lastGroupKey != ''">
|
||||
and t1.order_code > #{lastGroupKey}
|
||||
</if>
|
||||
</where>
|
||||
<if test="groupBy != 'NONE'">
|
||||
group by
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'"> t1.order_code </when>
|
||||
<when test="groupBy == 'PARTNER'"> t1.vendor_code </when>
|
||||
<otherwise> date_format(t1.create_time, '%Y-%m') </otherwise>
|
||||
</choose>
|
||||
order by
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'">
|
||||
<choose>
|
||||
<when test="keyset != null and keyset == 1"> t1.order_code asc </when>
|
||||
<otherwise> sum(t1.total_price_with_tax) desc </otherwise>
|
||||
</choose>
|
||||
</when>
|
||||
<when test="groupBy == 'TIME_MONTH'"> date_format(t1.create_time, '%Y-%m') asc </when>
|
||||
<otherwise> sum(t1.total_price_with_tax) desc </otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_balance_aggregate(as_of_date):应付历史时点重算 Σ应付(create_time ≤ T) / Σ已付(payment_time ≤ T) -->
|
||||
<select id="selectPayableAsOf" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
(select ifnull(sum(b.total_price_with_tax), 0)
|
||||
from oms_payable_bill b
|
||||
where b.create_time <= #{asOfDate}) as totalWithTax,
|
||||
(select ifnull(sum(d.payment_amount), 0)
|
||||
from oms_payable_payment_detail d
|
||||
where d.payment_time is not null
|
||||
and d.payment_time <= #{asOfDate}) as settledAmount
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -535,5 +535,67 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
group by original_bill_id
|
||||
</select>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=PAYMENT):付款单游标分页,排序键 payment_bill_code,id,时间字段 payment_time -->
|
||||
<select id="listPaymentBillPage" resultType="com.ruoyi.sip.domain.OmsPaymentBill">
|
||||
select
|
||||
pb.id, pb.payment_bill_code, pb.payment_bill_type, pb.payment_time, pb.actual_payment_time,
|
||||
pb.vendor_code, v.vendor_name, pb.order_code, pb.project_code, pb.project_name,
|
||||
pb.total_price_with_tax, pb.total_price_without_tax, pb.tax_amount,
|
||||
pb.payment_status, pb.approve_status, pb.approve_time, pb.payment_method,
|
||||
pb.pay_name, pb.pay_bank_number, pb.pay_bank_open_address, pb.bank_number,
|
||||
pb.pre_residue_amount, pb.refund_status, v.pay_type,
|
||||
pb.create_by, pb.create_time, pb.update_by, pb.update_time, pb.remark, pb.del_flag
|
||||
from oms_payment_bill pb
|
||||
left join oms_vendor_info v on pb.vendor_code = v.vendor_code
|
||||
<where>
|
||||
(pb.del_flag = '0' or pb.del_flag is null or pb.del_flag = '')
|
||||
<if test="paymentBillCodeList != null and paymentBillCodeList.size > 0">
|
||||
and pb.payment_bill_code in
|
||||
<foreach item="item" collection="paymentBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCode != null and orderCode != ''"> and pb.order_code = #{orderCode}</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and pb.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and pb.payment_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and pb.approve_status = #{approveStatus}</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and pb.payment_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and pb.payment_time < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 payment_bill_code,id) -->
|
||||
<if test="lastCode != null and lastCode != ''">
|
||||
and (pb.payment_bill_code > #{lastCode}
|
||||
or (pb.payment_bill_code = #{lastCode} and pb.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by pb.payment_bill_code asc, pb.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=PAYMENT):include_total 计数 -->
|
||||
<select id="countPaymentBillPage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_payment_bill pb
|
||||
<where>
|
||||
(pb.del_flag = '0' or pb.del_flag is null or pb.del_flag = '')
|
||||
<if test="paymentBillCodeList != null and paymentBillCodeList.size > 0">
|
||||
and pb.payment_bill_code in
|
||||
<foreach item="item" collection="paymentBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCode != null and orderCode != ''"> and pb.order_code = #{orderCode}</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and pb.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and pb.payment_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and pb.approve_status = #{approveStatus}</if>
|
||||
<if test="startTime != null"> and pb.payment_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and pb.payment_time < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -335,4 +335,52 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
#{item}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT;不影响既有语句) ================= -->
|
||||
<sql id="todoPageFilter">
|
||||
<if test="approveUser != null and approveUser != ''">and approve_user = #{approveUser}</if>
|
||||
<if test="processKeyList != null and processKeyList.size() > 0">
|
||||
and process_key in
|
||||
<foreach item="item" collection="processKeyList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="businessKeyList != null and businessKeyList.size() > 0">
|
||||
and business_key in
|
||||
<foreach item="item" collection="businessKeyList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="listTodoPage" resultType="com.ruoyi.sip.flowable.domain.Todo">
|
||||
select id, todo_id, process_instance_id, task_id, business_key, process_key, process_name, task_name,
|
||||
approve_user, approve_user_name, apply_user_name, apply_time, form_key,
|
||||
extend_field1, extend_field2, extend_field3
|
||||
from bu_todo
|
||||
<where>
|
||||
<include refid="todoPageFilter"/>
|
||||
<if test="startTime != null">and apply_time <![CDATA[ >= ]]> #{startTime}</if>
|
||||
<if test="endTime != null">and apply_time <![CDATA[ < ]]> #{endTime}</if>
|
||||
<if test="lastTime != null and lastId != null">
|
||||
and (apply_time < #{lastTime} or (apply_time = #{lastTime} and id < #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by apply_time desc, id desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="listTodoCompletedPage" resultType="com.ruoyi.sip.flowable.domain.Todo">
|
||||
select id, todo_id, process_instance_id, task_id, business_key, process_key, process_name, task_name,
|
||||
approve_user, approve_user_name, all_approve_user_name, apply_user_name, apply_time, approve_time,
|
||||
approve_opinion, approve_status, form_key,
|
||||
extend_field1, extend_field2, extend_field3
|
||||
from bu_todo_completed
|
||||
<where>
|
||||
<include refid="todoPageFilter"/>
|
||||
<if test="startTime != null">and approve_time <![CDATA[ >= ]]> #{startTime}</if>
|
||||
<if test="endTime != null">and approve_time <![CDATA[ < ]]> #{endTime}</if>
|
||||
<if test="lastTime != null and lastId != null">
|
||||
and (approve_time < #{lastTime} or (approve_time = #{lastTime} and id < #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by approve_time desc, id desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
@ -356,4 +356,77 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
and t1.delivery_status = '2'
|
||||
</select>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT) ================= -->
|
||||
|
||||
<!-- 按出库单号列表批量查发货单(现有 selectInventoryDeliveryList 仅支持单个 outerCode) -->
|
||||
<select id="listByOuterCodeList" resultType="com.ruoyi.sip.domain.InventoryDelivery">
|
||||
select t1.id, t1.outer_code, t1.warehouse_id, t1.quantity, t1.logistics_company, t1.logistics_code,
|
||||
t1.delivery_status, t1.approve_status, t1.delivery_time, t1.delivery_type,
|
||||
t1.create_by, t1.create_time, t1.version_code,
|
||||
t2.warehouse_name,
|
||||
t3.order_code, t3.product_code,
|
||||
t5.project_code, t5.project_name, t4.notifier_address,
|
||||
t6.user_name as createByName,
|
||||
t7.model
|
||||
from oms_inventory_delivery t1
|
||||
left join oms_warehouse_info t2 on t1.warehouse_id = t2.id
|
||||
left join oms_inventory_outer t3 on t1.outer_code = t3.outer_code
|
||||
left join project_order_info t4 on t3.order_code = t4.order_code
|
||||
left join project_info t5 on t4.project_id = t5.id
|
||||
left join sys_user t6 on t1.create_by = t6.user_id
|
||||
left join product_info t7 on t3.product_code = t7.product_code
|
||||
where t1.outer_code in
|
||||
<foreach item="item" collection="outerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
order by t1.outer_code, t1.id
|
||||
</select>
|
||||
|
||||
<sql id="deliveryPageFilter">
|
||||
<if test="outerCode != null and outerCode != ''">and t1.outer_code = #{outerCode}</if>
|
||||
<if test="orderCode != null and orderCode != ''">and t3.order_code = #{orderCode}</if>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t3.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="deliveryStatus != null and deliveryStatus != ''">and t1.delivery_status = #{deliveryStatus}</if>
|
||||
<if test="startTime != null">and t1.delivery_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and t1.delivery_time < #{endTime}</if>
|
||||
</sql>
|
||||
|
||||
<!-- warehouse_list(entity=DELIVERY):发货单游标分页(排序键 outer_code,id) -->
|
||||
<select id="listDeliveryPage" resultType="com.ruoyi.sip.domain.InventoryDelivery">
|
||||
select t1.id, t1.outer_code, t1.warehouse_id, t1.quantity, t1.logistics_company, t1.logistics_code,
|
||||
t1.delivery_status, t1.approve_status, t1.delivery_time, t1.delivery_type,
|
||||
t1.create_by, t1.create_time, t1.version_code,
|
||||
t2.warehouse_name,
|
||||
t3.order_code, t3.product_code,
|
||||
t5.project_code, t5.project_name, t4.notifier_address,
|
||||
t6.user_name as createByName,
|
||||
t7.model
|
||||
from oms_inventory_delivery t1
|
||||
left join oms_warehouse_info t2 on t1.warehouse_id = t2.id
|
||||
left join oms_inventory_outer t3 on t1.outer_code = t3.outer_code
|
||||
left join project_order_info t4 on t3.order_code = t4.order_code
|
||||
left join project_info t5 on t4.project_id = t5.id
|
||||
left join sys_user t6 on t1.create_by = t6.user_id
|
||||
left join product_info t7 on t3.product_code = t7.product_code
|
||||
<where>
|
||||
<include refid="deliveryPageFilter"/>
|
||||
<if test="cursorKey != null">
|
||||
and (t1.outer_code > #{cursorKey}
|
||||
or (t1.outer_code = #{cursorKey} and t1.id > #{cursorId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.outer_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countDeliveryPage" resultType="long">
|
||||
select count(1)
|
||||
from oms_inventory_delivery t1
|
||||
left join oms_inventory_outer t3 on t1.outer_code = t3.outer_code
|
||||
<where>
|
||||
<include refid="deliveryPageFilter"/>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -310,4 +310,183 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
)
|
||||
</delete>
|
||||
|
||||
<!-- ====================================================================== -->
|
||||
<!-- MCP 只读查询(新增,仅 SELECT,不修改既有语句) -->
|
||||
<!-- 注意:purchase_no 非 oms_inventory_info 列(见方案 2.3/2.4),采购单号由 -->
|
||||
<!-- oms_inventory_inner.purchase_no 按 inner_code 关联得到,故此处不查询该列。 -->
|
||||
<!-- ====================================================================== -->
|
||||
|
||||
<!-- inventory_sn_trace:按产品 SN 列表(≤50,唯一索引) -->
|
||||
<select id="listSnTraceByProductSnList" resultType="com.ruoyi.sip.domain.InventoryInfo">
|
||||
<include refid="selectInventoryInfoVo"/>
|
||||
where t1.product_sn in
|
||||
<foreach item="item" collection="productSnList" open="(" separator="," close=")">#{item}</foreach>
|
||||
order by t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- inventory_sn_trace:按入库单号列表(≤20) -->
|
||||
<select id="listSnTraceByInnerCodeList" resultType="com.ruoyi.sip.domain.InventoryInfo">
|
||||
<include refid="selectInventoryInfoVo"/>
|
||||
where t1.inner_code in
|
||||
<foreach item="item" collection="innerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
order by t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- inventory_sn_trace:按出库单号列表(≤20) -->
|
||||
<select id="listSnTraceByOuterCodeList" resultType="com.ruoyi.sip.domain.InventoryInfo">
|
||||
<include refid="selectInventoryInfoVo"/>
|
||||
where t1.outer_code in
|
||||
<foreach item="item" collection="outerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
order by t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- inventory_sn_trace:按合同编号反查已出库 SN(与既有 selectInventoryInfoByOrderCode 同口径,加 limit 上界) -->
|
||||
<select id="listSnTraceByOrderCode" resultType="com.ruoyi.sip.domain.InventoryInfo">
|
||||
<include refid="selectInventoryInfoVo"/>
|
||||
where t1.outer_code in (
|
||||
select outer_code from oms_inventory_outer where order_code in
|
||||
<foreach item="item" collection="orderCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
)
|
||||
order by t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- inventory_stock_aggregate(SUMMARY):一次全量聚合,按 group_by 动态分组 + 数量倒序 top_n -->
|
||||
<select id="selectInventoryStockSummary" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="groupBy == 'PRODUCT'">t1.product_code as groupKey, null as groupName,</when>
|
||||
<when test="groupBy == 'WAREHOUSE'">t1.warehouse_id as groupKey, t2.warehouse_name as groupName,</when>
|
||||
<when test="groupBy == 'STATUS'">t1.inventory_status as groupKey, null as groupName,</when>
|
||||
<otherwise>null as groupKey, null as groupName,</otherwise>
|
||||
</choose>
|
||||
sum(case when t1.inventory_status = '0' then 1 else 0 end) as inStockQty,
|
||||
sum(case when t1.inventory_status = '1' then 1 else 0 end) as outStockQty,
|
||||
count(1) as totalQty,
|
||||
sum(ifnull(t1.inner_price, 0)) as innerAmount,
|
||||
sum(ifnull(t1.outer_price, 0)) as outerAmount
|
||||
from oms_inventory_info t1
|
||||
left join oms_warehouse_info t2 on t1.warehouse_id = t2.id
|
||||
<where>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="warehouseId != null">and t1.warehouse_id = #{warehouseId}</if>
|
||||
</where>
|
||||
<choose>
|
||||
<when test="groupBy == 'PRODUCT'">group by t1.product_code</when>
|
||||
<when test="groupBy == 'WAREHOUSE'">group by t1.warehouse_id, t2.warehouse_name</when>
|
||||
<when test="groupBy == 'STATUS'">group by t1.inventory_status</when>
|
||||
</choose>
|
||||
<if test="!includeZero">
|
||||
having (inStockQty <> 0 or outStockQty <> 0 or totalQty <> 0 or innerAmount <> 0 or outerAmount <> 0)
|
||||
</if>
|
||||
order by totalQty desc
|
||||
<if test="topN != null">limit #{topN}</if>
|
||||
</select>
|
||||
|
||||
<!-- inventory_stock_aggregate(LIST):按产品分页聚合(游标键 product_code 升序,取 limit 条) -->
|
||||
<select id="selectStockGroupPage" resultType="java.util.LinkedHashMap">
|
||||
select t1.product_code as productCode,
|
||||
sum(case when t1.inventory_status = '0' then 1 else 0 end) as inStockQty,
|
||||
sum(case when t1.inventory_status = '1' then 1 else 0 end) as outStockQty,
|
||||
count(1) as totalQty,
|
||||
sum(ifnull(t1.inner_price, 0)) as innerAmount,
|
||||
sum(ifnull(t1.outer_price, 0)) as outerAmount
|
||||
from oms_inventory_info t1
|
||||
<where>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="warehouseId != null">and t1.warehouse_id = #{warehouseId}</if>
|
||||
<if test="cursor != null">and t1.product_code > #{cursor}</if>
|
||||
</where>
|
||||
group by t1.product_code
|
||||
<if test="!includeZero">
|
||||
having (inStockQty <> 0 or outStockQty <> 0 or totalQty <> 0 or innerAmount <> 0 or outerAmount <> 0)
|
||||
</if>
|
||||
order by t1.product_code
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- inventory_stock_aggregate(LIST):按产品码批量取仓库拆分(禁止逐行查询) -->
|
||||
<select id="selectStockByProductWarehouse" resultType="java.util.LinkedHashMap">
|
||||
select t1.product_code as productCode,
|
||||
t1.warehouse_id as warehouseId,
|
||||
t2.warehouse_name as warehouseName,
|
||||
sum(case when t1.inventory_status = '0' then 1 else 0 end) as inStockQty,
|
||||
sum(case when t1.inventory_status = '1' then 1 else 0 end) as outStockQty
|
||||
from oms_inventory_info t1
|
||||
left join oms_warehouse_info t2 on t1.warehouse_id = t2.id
|
||||
<where>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="warehouseId != null">and t1.warehouse_id = #{warehouseId}</if>
|
||||
</where>
|
||||
group by t1.product_code, t1.warehouse_id, t2.warehouse_name
|
||||
order by t1.product_code, t1.warehouse_id
|
||||
</select>
|
||||
|
||||
<!-- inventory_stock_aggregate(LIST):count 产品分组数(受 count_cap 由调用方处理) -->
|
||||
<select id="countStockGroups" resultType="long">
|
||||
select count(distinct t1.product_code)
|
||||
from oms_inventory_info t1
|
||||
<where>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="warehouseId != null">and t1.warehouse_id = #{warehouseId}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- warehouse_list(entity=SN):SN 明细游标分页(排序键 product_sn,id) -->
|
||||
<select id="listSnPage" resultType="com.ruoyi.sip.domain.InventoryInfo">
|
||||
<include refid="selectInventoryInfoVo"/>
|
||||
<where>
|
||||
<if test="productSnList != null and productSnList.size() > 0">
|
||||
and t1.product_sn in
|
||||
<foreach item="item" collection="productSnList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="inventoryStatus != null and inventoryStatus != ''">and t1.inventory_status = #{inventoryStatus}</if>
|
||||
<if test="innerCode != null and innerCode != ''">and t1.inner_code = #{innerCode}</if>
|
||||
<if test="outerCode != null and outerCode != ''">and t1.outer_code = #{outerCode}</if>
|
||||
<if test="cursorKey != null">
|
||||
and (t1.product_sn > #{cursorKey}
|
||||
or (t1.product_sn = #{cursorKey} and t1.id > #{cursorId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.product_sn, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countSnPage" resultType="long">
|
||||
select count(1)
|
||||
from oms_inventory_info t1
|
||||
<where>
|
||||
<if test="productSnList != null and productSnList.size() > 0">
|
||||
and t1.product_sn in
|
||||
<foreach item="item" collection="productSnList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="inventoryStatus != null and inventoryStatus != ''">and t1.inventory_status = #{inventoryStatus}</if>
|
||||
<if test="innerCode != null and innerCode != ''">and t1.inner_code = #{innerCode}</if>
|
||||
<if test="outerCode != null and outerCode != ''">and t1.outer_code = #{outerCode}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -121,4 +121,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- MCP 只读:按出库单号批量查出库明细(依赖 P0-2 索引 idx_outer_code,消除 N+1) -->
|
||||
<select id="listByOuterCodeList" resultType="com.ruoyi.sip.domain.InventoryOuterDetail">
|
||||
select t1.id, t1.outer_code, t1.warehouse_id, t1.quantity, t1.outer_status,
|
||||
t2.warehouse_name,
|
||||
t3.product_code,
|
||||
t4.model, t4.product_name, t4.available_count, t4.type as product_type
|
||||
from oms_inventory_outer_detail t1
|
||||
left join oms_warehouse_info t2 on t1.warehouse_id = t2.id
|
||||
left join oms_inventory_outer t3 on t1.outer_code = t3.outer_code
|
||||
left join product_info t4 on t4.product_code = t3.product_code
|
||||
where t1.outer_code in
|
||||
<foreach item="item" collection="outerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
order by t1.id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -296,4 +296,60 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT) ================= -->
|
||||
|
||||
<sql id="outerPageFilter">
|
||||
<if test="outerCodeList != null and outerCodeList.size() > 0">
|
||||
and t1.outer_code in
|
||||
<foreach item="item" collection="outerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCodeList != null and orderCodeList.size() > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="outerStatusList != null and outerStatusList.size() > 0">
|
||||
and t1.outer_status in
|
||||
<foreach item="item" collection="outerStatusList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null">and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and t1.create_time < #{endTime}</if>
|
||||
</sql>
|
||||
|
||||
<!-- warehouse_list(entity=OUTER):出库单游标分页(排序键 outer_code,id) -->
|
||||
<select id="listOuterPage" resultType="com.ruoyi.sip.domain.InventoryOuter">
|
||||
select t1.id, t1.outer_code, t1.product_code, t1.quantity, t1.outer_status, t1.delivery_status,
|
||||
t1.order_code, t1.contact_person, t1.contact_phone, t1.contact_address, t1.delivery_time,
|
||||
t1.delivery_time_type, t1.version_code, t1.create_time,
|
||||
t2.model,
|
||||
t4.project_code, t4.project_name,
|
||||
t4.id as project_id,
|
||||
t5.user_name as createByName
|
||||
from oms_inventory_outer t1
|
||||
left join product_info t2 on t1.product_code = t2.product_code
|
||||
left join project_order_info t3 on t1.order_code = t3.order_code
|
||||
left join project_info t4 on t4.id = t3.project_id
|
||||
left join sys_user t5 on t1.create_by = t5.user_id
|
||||
<where>
|
||||
<include refid="outerPageFilter"/>
|
||||
<if test="cursorKey != null">
|
||||
and (t1.outer_code > #{cursorKey}
|
||||
or (t1.outer_code = #{cursorKey} and t1.id > #{cursorId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.outer_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countOuterPage" resultType="long">
|
||||
select count(1)
|
||||
from oms_inventory_outer t1
|
||||
<where>
|
||||
<include refid="outerPageFilter"/>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -77,4 +77,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
delete from oms_inventory_delivery_detail where delivery_id =#{id}
|
||||
</delete>
|
||||
|
||||
<!-- MCP 只读:按发货单 id 批量查 SN 明细(idx_delivery_id 已具备,消除 N+1) -->
|
||||
<select id="listByDeliveryIdList" resultType="com.ruoyi.sip.domain.OmsInventoryDeliveryDetail">
|
||||
select id, delivery_id, product_sn
|
||||
from oms_inventory_delivery_detail
|
||||
where delivery_id in
|
||||
<foreach item="item" collection="deliveryIdList" open="(" separator="," close=")">#{item}</foreach>
|
||||
order by id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -226,4 +226,50 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT) ================= -->
|
||||
|
||||
<sql id="innerPageFilter">
|
||||
<if test="innerCodeList != null and innerCodeList.size() > 0">
|
||||
and t1.inner_code in
|
||||
<foreach item="item" collection="innerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="purchaseNo != null and purchaseNo != ''">and t1.purchase_no = #{purchaseNo}</if>
|
||||
<if test="orderCode != null and orderCode != ''">and t1.order_code = #{orderCode}</if>
|
||||
<if test="startTime != null">and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and t1.create_time < #{endTime}</if>
|
||||
</sql>
|
||||
|
||||
<!-- warehouse_list(entity=INNER):入库单游标分页(排序键 inner_code,id) -->
|
||||
<select id="listInnerPage" resultType="com.ruoyi.sip.domain.OmsInventoryInner">
|
||||
select t1.id, t1.inner_code, t1.purchase_no, t1.product_code, t1.product_type, t1.quantity,
|
||||
t1.vendor_code, t1.warehouse_id, t1.order_code, t1.total_amount, t1.tax_rate, t1.tax_total,
|
||||
t1.remark, t1.create_time,
|
||||
t2.warehouse_name, t2.warehouse_type,
|
||||
t3.user_name as createByName,
|
||||
t4.vendor_name,
|
||||
t5.model
|
||||
from oms_inventory_inner t1
|
||||
left join oms_warehouse_info t2 on t1.warehouse_id = t2.id
|
||||
left join sys_user t3 on t1.create_by = t3.user_id
|
||||
left join oms_vendor_info t4 on t1.vendor_code = t4.vendor_code
|
||||
left join product_info t5 on t1.product_code = t5.product_code
|
||||
<where>
|
||||
<include refid="innerPageFilter"/>
|
||||
<if test="cursorKey != null">
|
||||
and (t1.inner_code > #{cursorKey}
|
||||
or (t1.inner_code = #{cursorKey} and t1.id > #{cursorId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.inner_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countInnerPage" resultType="long">
|
||||
select count(1)
|
||||
from oms_inventory_inner t1
|
||||
<where>
|
||||
<include refid="innerPageFilter"/>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -163,4 +163,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</if>
|
||||
</select>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT;不影响既有语句) ================= -->
|
||||
<select id="listCustomerMasterPage" resultType="com.ruoyi.sip.domain.CustomerInfo">
|
||||
select id, customer_code, customer_name, customer_postcode, province, city, address, contact_person,
|
||||
contact_phone, contact_email, bg_property, industry_type, status, create_at, update_at
|
||||
from customer_info
|
||||
<where>
|
||||
<if test="codeList != null and codeList.size() > 0">
|
||||
and customer_code in
|
||||
<foreach item="item" collection="codeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="nameKeyword != null and nameKeyword != ''">
|
||||
and customer_name like concat('%', #{nameKeyword}, '%')
|
||||
</if>
|
||||
<if test="status != null">and status = #{status}</if>
|
||||
<if test="lastCode != null and lastId != null">
|
||||
and (customer_code > #{lastCode} or (customer_code = #{lastCode} and id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by customer_code, id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -163,4 +163,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- MCP 只读:按发货单 id 批量查 SN 明细(必须过滤 deleted_at is null) -->
|
||||
<select id="listByDeliveryIdList" resultType="com.ruoyi.sip.domain.DeliveryList">
|
||||
select id, delivery_id, product_code, serial_number, remark, created_at, updated_at
|
||||
from delivery_list
|
||||
where deleted_at is null
|
||||
and delivery_id in
|
||||
<foreach item="item" collection="deliveryIdList" open="(" separator="," close=")">#{item}</foreach>
|
||||
order by delivery_id, id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -152,4 +152,70 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT) ================= -->
|
||||
|
||||
<sql id="orderDeliveryPageFilter">
|
||||
<if test="deliveryCodeList != null and deliveryCodeList.size() > 0">
|
||||
and t1.delivery_code in
|
||||
<foreach item="item" collection="deliveryCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCodeList != null and orderCodeList.size() > 0">
|
||||
and trim(t2.order_code) in
|
||||
<foreach item="item" collection="orderCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="deliveryStatus != null and deliveryStatus != ''">and t1.delivery_status = #{deliveryStatus}</if>
|
||||
<if test="startTime != null">and t1.delivery_date >= #{startTime}</if>
|
||||
<if test="endTime != null">and t1.delivery_date < #{endTime}</if>
|
||||
</sql>
|
||||
|
||||
<!-- warehouse_list(entity=ORDER_DELIVERY):manage 域发货单游标分页(排序键 delivery_code,id)
|
||||
强制 deleted_at is null;left join order_info(trim(order_code) 对齐)补合同/客户信息 -->
|
||||
<select id="listOrderDeliveryPage" resultType="java.util.LinkedHashMap">
|
||||
select t1.id as id,
|
||||
t1.delivery_code as deliveryCode,
|
||||
t1.order_id as orderId,
|
||||
t1.delivery_date as deliveryDate,
|
||||
t1.delivery_type as deliveryType,
|
||||
t1.logistics_company as logisticsCompany,
|
||||
t1.logistics_code as logisticsCode,
|
||||
t1.receiver_name as receiverName,
|
||||
t1.receiver_phone as receiverPhone,
|
||||
t1.receiver_address as receiverAddress,
|
||||
t1.delivery_status as deliveryStatus,
|
||||
t1.sign_time as signTime,
|
||||
t1.remark as remark,
|
||||
t1.created_at as createdAt,
|
||||
t1.updated_at as updatedAt,
|
||||
t4.user_name as createByName,
|
||||
trim(t2.order_code) as orderCode,
|
||||
t2.order_name as orderName,
|
||||
t2.customer_code as customerCode,
|
||||
t2.customer_name as customerName,
|
||||
t2.order_agent_code as orderAgentCode,
|
||||
t2.order_partner_code as orderPartnerCode
|
||||
from order_delivery t1
|
||||
left join order_info t2 on t1.order_id = t2.id
|
||||
left join sys_user t4 on t1.create_by = t4.user_id
|
||||
<where>
|
||||
t1.deleted_at is null
|
||||
<include refid="orderDeliveryPageFilter"/>
|
||||
<if test="cursorKey != null">
|
||||
and (t1.delivery_code > #{cursorKey}
|
||||
or (t1.delivery_code = #{cursorKey} and t1.id > #{cursorId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.delivery_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countOrderDeliveryPage" resultType="long">
|
||||
select count(1)
|
||||
from order_delivery t1
|
||||
left join order_info t2 on t1.order_id = t2.id
|
||||
<where>
|
||||
t1.deleted_at is null
|
||||
<include refid="orderDeliveryPageFilter"/>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -436,4 +436,85 @@
|
|||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT;不影响既有语句) ================= -->
|
||||
<select id="listContractPage" resultType="com.ruoyi.sip.domain.OrderInfo">
|
||||
select t1.id, t1.project_code, t1.order_code, t1.version_code, t1.order_name, t1.customer_code,
|
||||
t1.customer_name, t1.customer_address, t1.customer_contact, t1.customer_phone, t1.customer_email,
|
||||
t1.customer_postcode, t1.industry_type, t1.bg_type, t1.order_type, t1.order_agent_code,
|
||||
t1.order_partner_code, t1.order_date, t1.status, t1.remark,
|
||||
t1.created_at, t1.updated_at, t1.deleted_at, t1.create_by, t1.update_by,
|
||||
t2.agent_name as order_agent_name,
|
||||
t3.partner_name as order_partner_name
|
||||
from order_info t1
|
||||
left join agent_info t2 on t1.order_agent_code = t2.agent_code
|
||||
left join partner_info t3 on t1.order_partner_code = t3.partner_code
|
||||
<where>
|
||||
t1.deleted_at is null
|
||||
<if test="codeList != null and codeList.size() > 0">
|
||||
and trim(replace(t1.order_code, '\t', '')) in
|
||||
<foreach item="item" collection="codeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="nameKeyword != null and nameKeyword != ''">
|
||||
and t1.order_name like concat('%', #{nameKeyword}, '%')
|
||||
</if>
|
||||
<if test="customerCodeList != null and customerCodeList.size() > 0">
|
||||
and t1.customer_code in
|
||||
<foreach item="item" collection="customerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="agentCodeList != null and agentCodeList.size() > 0">
|
||||
and t1.order_agent_code in
|
||||
<foreach item="item" collection="agentCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderType != null and orderType != ''">and t1.order_type = #{orderType}</if>
|
||||
<if test="status != null">and t1.status = #{status}</if>
|
||||
<if test="startTime != null">and t1.order_date <![CDATA[ >= ]]> #{startTime}</if>
|
||||
<if test="endTime != null">and t1.order_date <![CDATA[ < ]]> #{endTime}</if>
|
||||
<!-- order_info.order_code 实测有 14 行前导制表符 + 1 行尾随制表符脏数据:
|
||||
MySQL 的 trim() 只去空格不去制表符,故统一用 trim(replace(..., '\t', '')) 对齐(与 Java 侧 trim 等价) -->
|
||||
<if test="lastCode != null and lastVersion != null and lastId != null">
|
||||
and (trim(replace(t1.order_code, '\t', '')) > #{lastCode}
|
||||
or (trim(replace(t1.order_code, '\t', '')) = #{lastCode}
|
||||
and (t1.version_code > #{lastVersion}
|
||||
or (t1.version_code = #{lastVersion} and t1.id > #{lastId}))))
|
||||
</if>
|
||||
</where>
|
||||
order by trim(replace(t1.order_code, '\t', '')), t1.version_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="listContractProductPage" resultType="com.ruoyi.sip.domain.OrderList">
|
||||
select t1.id, t1.order_id, t1.product_code, t1.quantity, t1.price, t1.discount, t1.amount, t1.remark,
|
||||
t1.created_at, t1.updated_at, t1.deleted_at, t1.status,
|
||||
t2.product_name
|
||||
from order_list t1
|
||||
left join product_info t2 on t1.product_code = t2.product_code
|
||||
<where>
|
||||
t1.deleted_at is null
|
||||
<if test="orderIdList != null and orderIdList.size() > 0">
|
||||
and t1.order_id in
|
||||
<foreach item="item" collection="orderIdList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="productCodeList != null and productCodeList.size() > 0">
|
||||
and trim(t1.product_code) in
|
||||
<foreach item="item" collection="productCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="lastOrderId != null and lastId != null">
|
||||
and (t1.order_id > #{lastOrderId} or (t1.order_id = #{lastOrderId} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.order_id, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="listOrderListByOrderIdList" resultType="com.ruoyi.sip.domain.OrderList">
|
||||
select t1.id, t1.order_id, t1.product_code, t1.quantity, t1.price, t1.discount, t1.amount, t1.remark,
|
||||
t1.created_at, t1.updated_at,
|
||||
t2.product_name
|
||||
from order_list t1
|
||||
left join product_info t2 on t1.product_code = t2.product_code
|
||||
where t1.deleted_at is null
|
||||
and t1.order_id in
|
||||
<foreach item="item" collection="list" open="(" separator="," close=")">#{item}</foreach>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.sip.mapper.CrossDomainAggregateMapper">
|
||||
|
||||
<!--
|
||||
cross_domain_aggregate(受限跨域透视)专属只读聚合 SQL(共 5 条,每链路一条)。
|
||||
方案依据:docx/mcp-data-tools-plan.md 第 7 章第 18 项 / 第 9 章 / 第 13 章 / 第 15 章 / 16.2。
|
||||
|
||||
统一约定:
|
||||
1) d0/d1/d2 为链路白名单维度键(Java 侧已校验),这里用 <choose> 映射为固定表达式,不拼接任何模型传入片段;
|
||||
d1/d2 为 null 表示该位置不分组。每个维度同时输出编码(dN)与名称(dNName);时间维度无独立名称列,
|
||||
名称位重复输出桶值(Java 侧对 MONTH/QUARTER 只取 dN,不输出名称字段)。
|
||||
2) 时间过滤一律区间下推(>= start and < end),禁止 date_format(...) 作为过滤条件(方案 15.1);
|
||||
date_format 只用于分组/输出表达式(方案 15.2 维度矩阵允许)。
|
||||
3) 名称补齐全部在本 SQL 内 left join 字典表完成(无二次查询、无 N+1),且仅在该维度被请求时才 join,
|
||||
避免不必要的回表与潜在的行放大(product_info / customer_info 等均为编码唯一的主数据)。
|
||||
4) order by 只允许白名单度量别名(<choose> 映射),并以 d0 升序作为次序稳定键;limit 传 group_limit+1 用于探测截断。
|
||||
5) 表清单严格落在 McpTables.ALLOWED 内:project_product_info、project_order_info、project_info、product_info、
|
||||
customer_info、agent_info、oms_purchase_order、oms_purchase_order_item、oms_vendor_info、oms_inventory_info、
|
||||
oms_warehouse_info、oms_receivable_bill、oms_payable_bill;
|
||||
严禁任何备份/历史表(如 project_product_info_bak、oms_purchase_order_1211、oms_payable_bill_copy1 等,见方案 15.12)。
|
||||
-->
|
||||
|
||||
<!-- ================================================================== SALES ================================================================== -->
|
||||
<!-- 链路:project_product_info ⋈ project_order_info(project_id)⋈ project_info(project_id,取客户/代表处/项目)
|
||||
时间字段:project_order_info.create_time(下单时间)
|
||||
度量口径:salesAmountWithTax = Σ ppi.all_price(与既有 ProjectOrderInfoToolProvider 一致:all_price 视为含税总价);
|
||||
salesAmountWithoutTax = Σ all_price/(1+税率),税率 >1 视为百分数,与既有 normalizePercentTaxRate 口径一致;
|
||||
productQty = Σ ppi.quantity。 -->
|
||||
<select id="selectSalesAggregate" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="d0 == 'CUSTOMER'">pi.customer_code as d0, ci.customer_name as d0Name</when>
|
||||
<when test="d0 == 'AGENT'">pi.agent_code as d0, ag.agent_name as d0Name</when>
|
||||
<when test="d0 == 'PRODUCT'">ppi.product_bom_code as d0, pd.product_name as d0Name</when>
|
||||
<when test="d0 == 'PROJECT'">pi.project_code as d0, pi.project_name as d0Name</when>
|
||||
<when test="d0 == 'MONTH'">date_format(poi.create_time, '%Y-%m') as d0, date_format(poi.create_time, '%Y-%m') as d0Name</when>
|
||||
<otherwise>concat(year(poi.create_time), 'Q', quarter(poi.create_time)) as d0, concat(year(poi.create_time), 'Q', quarter(poi.create_time)) as d0Name</otherwise>
|
||||
</choose>
|
||||
<if test="d1 != null">
|
||||
, <choose>
|
||||
<when test="d1 == 'CUSTOMER'">pi.customer_code as d1, ci.customer_name as d1Name</when>
|
||||
<when test="d1 == 'AGENT'">pi.agent_code as d1, ag.agent_name as d1Name</when>
|
||||
<when test="d1 == 'PRODUCT'">ppi.product_bom_code as d1, pd.product_name as d1Name</when>
|
||||
<when test="d1 == 'PROJECT'">pi.project_code as d1, pi.project_name as d1Name</when>
|
||||
<when test="d1 == 'MONTH'">date_format(poi.create_time, '%Y-%m') as d1, date_format(poi.create_time, '%Y-%m') as d1Name</when>
|
||||
<otherwise>concat(year(poi.create_time), 'Q', quarter(poi.create_time)) as d1, concat(year(poi.create_time), 'Q', quarter(poi.create_time)) as d1Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="d2 != null">
|
||||
, <choose>
|
||||
<when test="d2 == 'CUSTOMER'">pi.customer_code as d2, ci.customer_name as d2Name</when>
|
||||
<when test="d2 == 'AGENT'">pi.agent_code as d2, ag.agent_name as d2Name</when>
|
||||
<when test="d2 == 'PRODUCT'">ppi.product_bom_code as d2, pd.product_name as d2Name</when>
|
||||
<when test="d2 == 'PROJECT'">pi.project_code as d2, pi.project_name as d2Name</when>
|
||||
<when test="d2 == 'MONTH'">date_format(poi.create_time, '%Y-%m') as d2, date_format(poi.create_time, '%Y-%m') as d2Name</when>
|
||||
<otherwise>concat(year(poi.create_time), 'Q', quarter(poi.create_time)) as d2, concat(year(poi.create_time), 'Q', quarter(poi.create_time)) as d2Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
, sum(ifnull(ppi.all_price, 0)) as salesAmountWithTax
|
||||
, sum(ifnull(ppi.all_price, 0) / (1 + (case when ifnull(ppi.tax_rate, 0) > 1 then ifnull(ppi.tax_rate, 0) / 100 else ifnull(ppi.tax_rate, 0) end))) as salesAmountWithoutTax
|
||||
, sum(ifnull(ppi.quantity, 0)) as productQty
|
||||
, count(*) as rowQty
|
||||
from project_product_info ppi
|
||||
inner join project_order_info poi on ppi.project_id = poi.id
|
||||
<if test="d0 == 'CUSTOMER' or d1 == 'CUSTOMER' or d2 == 'CUSTOMER' or d0 == 'AGENT' or d1 == 'AGENT' or d2 == 'AGENT' or d0 == 'PROJECT' or d1 == 'PROJECT' or d2 == 'PROJECT'">
|
||||
left join project_info pi on poi.project_id = pi.id
|
||||
</if>
|
||||
<if test="d0 == 'CUSTOMER' or d1 == 'CUSTOMER' or d2 == 'CUSTOMER'">
|
||||
left join customer_info ci on pi.customer_code = ci.customer_code
|
||||
</if>
|
||||
<if test="d0 == 'AGENT' or d1 == 'AGENT' or d2 == 'AGENT'">
|
||||
left join agent_info ag on pi.agent_code = ag.agent_code
|
||||
</if>
|
||||
<if test="d0 == 'PRODUCT' or d1 == 'PRODUCT' or d2 == 'PRODUCT'">
|
||||
left join product_info pd on ppi.product_bom_code = pd.product_code
|
||||
</if>
|
||||
where poi.create_time >= #{startTime} and poi.create_time < #{endTime}
|
||||
group by d0, d0Name
|
||||
<if test="d1 != null">, d1, d1Name</if>
|
||||
<if test="d2 != null">, d2, d2Name</if>
|
||||
order by
|
||||
<choose>
|
||||
<when test="orderMetric == 'SALES_AMOUNT_WITHOUT_TAX'">salesAmountWithoutTax</when>
|
||||
<when test="orderMetric == 'PRODUCT_QTY'">productQty</when>
|
||||
<otherwise>salesAmountWithTax</otherwise>
|
||||
</choose>
|
||||
desc, d0 asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- ================================================================== PURCHASE ================================================================== -->
|
||||
<!-- 链路:oms_purchase_order_item ⋈ oms_purchase_order(i.purchase_id = o.id,与既有 selectPurchaseArrivalSummary 一致)
|
||||
时间字段:oms_purchase_order.purchase_date(方案 15.1 的采购业务口径)
|
||||
度量口径:purchaseQty = Σ item.quantity;purchaseAmountWithTax = Σ item.amount_total(列注释“含税金额”);
|
||||
purchaseTax = Σ item.tax_total(列注释“税额”)。 -->
|
||||
<select id="selectPurchaseAggregate" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="d0 == 'PRODUCT'">i.product_code as d0, pd.product_name as d0Name</when>
|
||||
<when test="d0 == 'PARTNER'">v.vendor_code as d0, v.vendor_name as d0Name</when>
|
||||
<when test="d0 == 'MONTH'">date_format(o.purchase_date, '%Y-%m') as d0, date_format(o.purchase_date, '%Y-%m') as d0Name</when>
|
||||
<otherwise>concat(year(o.purchase_date), 'Q', quarter(o.purchase_date)) as d0, concat(year(o.purchase_date), 'Q', quarter(o.purchase_date)) as d0Name</otherwise>
|
||||
</choose>
|
||||
<if test="d1 != null">
|
||||
, <choose>
|
||||
<when test="d1 == 'PRODUCT'">i.product_code as d1, pd.product_name as d1Name</when>
|
||||
<when test="d1 == 'PARTNER'">v.vendor_code as d1, v.vendor_name as d1Name</when>
|
||||
<when test="d1 == 'MONTH'">date_format(o.purchase_date, '%Y-%m') as d1, date_format(o.purchase_date, '%Y-%m') as d1Name</when>
|
||||
<otherwise>concat(year(o.purchase_date), 'Q', quarter(o.purchase_date)) as d1, concat(year(o.purchase_date), 'Q', quarter(o.purchase_date)) as d1Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="d2 != null">
|
||||
, <choose>
|
||||
<when test="d2 == 'PRODUCT'">i.product_code as d2, pd.product_name as d2Name</when>
|
||||
<when test="d2 == 'PARTNER'">v.vendor_code as d2, v.vendor_name as d2Name</when>
|
||||
<when test="d2 == 'MONTH'">date_format(o.purchase_date, '%Y-%m') as d2, date_format(o.purchase_date, '%Y-%m') as d2Name</when>
|
||||
<otherwise>concat(year(o.purchase_date), 'Q', quarter(o.purchase_date)) as d2, concat(year(o.purchase_date), 'Q', quarter(o.purchase_date)) as d2Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
, sum(ifnull(i.quantity, 0)) as purchaseQty
|
||||
, sum(ifnull(i.amount_total, 0)) as purchaseAmountWithTax
|
||||
, sum(ifnull(i.tax_total, 0)) as purchaseTax
|
||||
, count(*) as rowQty
|
||||
from oms_purchase_order_item i
|
||||
inner join oms_purchase_order o on i.purchase_id = o.id
|
||||
<if test="d0 == 'PARTNER' or d1 == 'PARTNER' or d2 == 'PARTNER'">
|
||||
left join oms_vendor_info v on o.vendor_id = v.vendor_id
|
||||
</if>
|
||||
<if test="d0 == 'PRODUCT' or d1 == 'PRODUCT' or d2 == 'PRODUCT'">
|
||||
left join product_info pd on i.product_code = pd.product_code
|
||||
</if>
|
||||
where (o.del_flag = 0 or o.del_flag is null)
|
||||
and o.purchase_date >= #{startTime} and o.purchase_date < #{endTime}
|
||||
group by d0, d0Name
|
||||
<if test="d1 != null">, d1, d1Name</if>
|
||||
<if test="d2 != null">, d2, d2Name</if>
|
||||
order by
|
||||
<choose>
|
||||
<when test="orderMetric == 'PURCHASE_AMOUNT_WITH_TAX'">purchaseAmountWithTax</when>
|
||||
<when test="orderMetric == 'PURCHASE_TAX'">purchaseTax</when>
|
||||
<otherwise>purchaseQty</otherwise>
|
||||
</choose>
|
||||
desc, d0 asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- ================================================================== STOCK ================================================================== -->
|
||||
<!-- 链路:oms_inventory_info 单表(方案 4.4 链路表)
|
||||
时间字段:oms_inventory_info.create_time(方案 15.1 默认时间字段)
|
||||
度量口径:inStockQty = count(inventory_status='0')(在库);outStockQty = count(inventory_status='1')(已出库);
|
||||
在库口径按方案 15.7 默认 A:未扣除“已发货未出库”的占用。 -->
|
||||
<select id="selectStockAggregate" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="d0 == 'PRODUCT'">t1.product_code as d0, pd.product_name as d0Name</when>
|
||||
<when test="d0 == 'WAREHOUSE'">t1.warehouse_id as d0, w.warehouse_name as d0Name</when>
|
||||
<when test="d0 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d0, date_format(t1.create_time, '%Y-%m') as d0Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d0, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d0Name</otherwise>
|
||||
</choose>
|
||||
<if test="d1 != null">
|
||||
, <choose>
|
||||
<when test="d1 == 'PRODUCT'">t1.product_code as d1, pd.product_name as d1Name</when>
|
||||
<when test="d1 == 'WAREHOUSE'">t1.warehouse_id as d1, w.warehouse_name as d1Name</when>
|
||||
<when test="d1 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d1, date_format(t1.create_time, '%Y-%m') as d1Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d1, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d1Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="d2 != null">
|
||||
, <choose>
|
||||
<when test="d2 == 'PRODUCT'">t1.product_code as d2, pd.product_name as d2Name</when>
|
||||
<when test="d2 == 'WAREHOUSE'">t1.warehouse_id as d2, w.warehouse_name as d2Name</when>
|
||||
<when test="d2 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d2, date_format(t1.create_time, '%Y-%m') as d2Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d2, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d2Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
, sum(case when t1.inventory_status = '0' then 1 else 0 end) as inStockQty
|
||||
, sum(case when t1.inventory_status = '1' then 1 else 0 end) as outStockQty
|
||||
, count(*) as rowQty
|
||||
from oms_inventory_info t1
|
||||
<if test="d0 == 'PRODUCT' or d1 == 'PRODUCT' or d2 == 'PRODUCT'">
|
||||
left join product_info pd on t1.product_code = pd.product_code
|
||||
</if>
|
||||
<if test="d0 == 'WAREHOUSE' or d1 == 'WAREHOUSE' or d2 == 'WAREHOUSE'">
|
||||
left join oms_warehouse_info w on t1.warehouse_id = w.id
|
||||
</if>
|
||||
where t1.create_time >= #{startTime} and t1.create_time < #{endTime}
|
||||
group by d0, d0Name
|
||||
<if test="d1 != null">, d1, d1Name</if>
|
||||
<if test="d2 != null">, d2, d2Name</if>
|
||||
order by
|
||||
<choose>
|
||||
<when test="orderMetric == 'OUT_STOCK_QTY'">outStockQty</when>
|
||||
<otherwise>inStockQty</otherwise>
|
||||
</choose>
|
||||
desc, d0 asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- ================================================================== FINANCE_AR ================================================================== -->
|
||||
<!-- 链路:oms_receivable_bill(金额直接 sum 冗余列,★不重算,与页面一致,见方案第九章)
|
||||
时间字段:oms_receivable_bill.create_time(记账口径)
|
||||
维度:CUSTOMER = partner_code(客户,名称取冗余列 partner_name)。
|
||||
说明:本表无 project_id/project_code 列;经 order_code 关联 project_order_info 存在一对多(多版本)导致金额放大的风险,
|
||||
故不提供 PROJECT 维度(方案未要求,且放大口径错误比缺失维度更危险)。 -->
|
||||
<select id="selectReceivableAggregate" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="d0 == 'CUSTOMER'">t1.partner_code as d0, t1.partner_name as d0Name</when>
|
||||
<when test="d0 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d0, date_format(t1.create_time, '%Y-%m') as d0Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d0, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d0Name</otherwise>
|
||||
</choose>
|
||||
<if test="d1 != null">
|
||||
, <choose>
|
||||
<when test="d1 == 'CUSTOMER'">t1.partner_code as d1, t1.partner_name as d1Name</when>
|
||||
<when test="d1 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d1, date_format(t1.create_time, '%Y-%m') as d1Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d1, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d1Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="d2 != null">
|
||||
, <choose>
|
||||
<when test="d2 == 'CUSTOMER'">t1.partner_code as d2, t1.partner_name as d2Name</when>
|
||||
<when test="d2 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d2, date_format(t1.create_time, '%Y-%m') as d2Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d2, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d2Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
, sum(ifnull(t1.total_price_with_tax, 0)) as receivableWithTax
|
||||
, sum(ifnull(t1.received_amount, 0)) as receivedWithTax
|
||||
, sum(ifnull(t1.unreceived_amount, 0)) as unreceivedWithTax
|
||||
, sum(ifnull(t1.uninvoiced_amount, 0)) as uninvoicedWithTax
|
||||
, count(*) as rowQty
|
||||
from oms_receivable_bill t1
|
||||
where (t1.del_flag = '0' or t1.del_flag is null or t1.del_flag = '')
|
||||
and t1.create_time >= #{startTime} and t1.create_time < #{endTime}
|
||||
group by d0, d0Name
|
||||
<if test="d1 != null">, d1, d1Name</if>
|
||||
<if test="d2 != null">, d2, d2Name</if>
|
||||
order by
|
||||
<choose>
|
||||
<when test="orderMetric == 'RECEIVED_WITH_TAX'">receivedWithTax</when>
|
||||
<when test="orderMetric == 'UNRECEIVED_WITH_TAX'">unreceivedWithTax</when>
|
||||
<when test="orderMetric == 'UNINVOICED_WITH_TAX'">uninvoicedWithTax</when>
|
||||
<otherwise>receivableWithTax</otherwise>
|
||||
</choose>
|
||||
desc, d0 asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- ================================================================== FINANCE_AP ================================================================== -->
|
||||
<!-- 链路:oms_payable_bill(金额直接 sum 冗余列,★不重算,与页面一致,见方案第九章)
|
||||
时间字段:oms_payable_bill.create_time(记账口径)
|
||||
维度:PARTNER = vendor_code(★此处 partner 语义为“制造商/供应商”,名称取冗余列 vendor_name)。
|
||||
说明:同 FINANCE_AR,不提供 PROJECT 维度(本表无 project 列,经 order_code 关联会放大金额)。 -->
|
||||
<select id="selectPayableAggregate" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="d0 == 'PARTNER'">t1.vendor_code as d0, t2.vendor_name as d0Name</when>
|
||||
<when test="d0 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d0, date_format(t1.create_time, '%Y-%m') as d0Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d0, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d0Name</otherwise>
|
||||
</choose>
|
||||
<if test="d1 != null">
|
||||
, <choose>
|
||||
<when test="d1 == 'PARTNER'">t1.vendor_code as d1, t2.vendor_name as d1Name</when>
|
||||
<when test="d1 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d1, date_format(t1.create_time, '%Y-%m') as d1Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d1, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d1Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="d2 != null">
|
||||
, <choose>
|
||||
<when test="d2 == 'PARTNER'">t1.vendor_code as d2, t2.vendor_name as d2Name</when>
|
||||
<when test="d2 == 'MONTH'">date_format(t1.create_time, '%Y-%m') as d2, date_format(t1.create_time, '%Y-%m') as d2Name</when>
|
||||
<otherwise>concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d2, concat(year(t1.create_time), 'Q', quarter(t1.create_time)) as d2Name</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
, sum(ifnull(t1.total_price_with_tax, 0)) as payableWithTax
|
||||
, sum(ifnull(t1.paid_payment_amount, 0)) as paidWithTax
|
||||
, sum(ifnull(t1.unpaid_payment_amount, 0)) as unpaidWithTax
|
||||
, sum(ifnull(t1.unreceived_ticket_amount, 0)) as unticketedWithTax
|
||||
, count(*) as rowQty
|
||||
from oms_payable_bill t1
|
||||
<!-- oms_payable_bill 无 vendor_name 列,制造商名称经 vendor_code 关联 oms_vendor_info 获取 -->
|
||||
left join oms_vendor_info t2 on t2.vendor_code = t1.vendor_code
|
||||
where (t1.del_flag = '0' or t1.del_flag is null or t1.del_flag = '')
|
||||
and t1.create_time >= #{startTime} and t1.create_time < #{endTime}
|
||||
group by d0, d0Name
|
||||
<if test="d1 != null">, d1, d1Name</if>
|
||||
<if test="d2 != null">, d2, d2Name</if>
|
||||
order by
|
||||
<choose>
|
||||
<when test="orderMetric == 'PAID_WITH_TAX'">paidWithTax</when>
|
||||
<when test="orderMetric == 'UNPAID_WITH_TAX'">unpaidWithTax</when>
|
||||
<when test="orderMetric == 'UNTICKETED_WITH_TAX'">unticketedWithTax</when>
|
||||
<otherwise>payableWithTax</otherwise>
|
||||
</choose>
|
||||
desc, d0 asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -112,4 +112,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT;不影响既有语句) ================= -->
|
||||
<select id="listCompanyMasterPage" resultType="com.ruoyi.sip.domain.OmsCompanyInfo">
|
||||
select id, company_name, company_code, company_user, company_email, company_phone, company_address,
|
||||
pay_bank_number, pay_bank_open_address, bank_number, social_credit, pay_name
|
||||
from oms_company_info
|
||||
<where>
|
||||
<if test="codeList != null and codeList.size() > 0">
|
||||
and company_code in
|
||||
<foreach item="item" collection="codeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="nameKeyword != null and nameKeyword != ''">
|
||||
and company_name like concat('%', #{nameKeyword}, '%')
|
||||
</if>
|
||||
<if test="lastCode != null and lastId != null">
|
||||
and (company_code > #{lastCode} or (company_code = #{lastCode} and id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by company_code, id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -663,4 +663,59 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
from oms_invoice_bill
|
||||
where invoice_bill_code LIKE CONCAT(#{codePrefix}, '%')
|
||||
</select>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=INVOICE):开票单游标分页,排序键 invoice_bill_code,id,时间字段 invoice_time -->
|
||||
<select id="listInvoiceBillPage" resultType="com.ruoyi.sip.domain.OmsInvoiceBill">
|
||||
select
|
||||
t1.id, t1.invoice_bill_code, t1.invoice_type, t1.invoice_bill_type,
|
||||
t1.invoice_time, t1.actual_invoice_time, t1.partner_code, t1.partner_name,
|
||||
t1.total_price_with_tax, t1.total_price_without_tax, t1.tax_rate,
|
||||
t1.invoice_status, t1.approve_status, t1.approve_time, t1.refund_status,
|
||||
t1.create_by, t1.create_time, t1.update_by, t1.update_time, t1.remark
|
||||
from oms_invoice_bill t1
|
||||
<where>
|
||||
<if test="invoiceBillCodeList != null and invoiceBillCodeList.size > 0">
|
||||
and t1.invoice_bill_code in
|
||||
<foreach item="item" collection="invoiceBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="partnerCodeList != null and partnerCodeList.size > 0">
|
||||
and t1.partner_code in
|
||||
<foreach item="item" collection="partnerCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and t1.invoice_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t1.approve_status = #{approveStatus}</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.invoice_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.invoice_time < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 invoice_bill_code,id) -->
|
||||
<if test="lastCode != null and lastCode != ''">
|
||||
and (t1.invoice_bill_code > #{lastCode}
|
||||
or (t1.invoice_bill_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.invoice_bill_code asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=INVOICE):include_total 计数 -->
|
||||
<select id="countInvoiceBillPage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_invoice_bill t1
|
||||
<where>
|
||||
<if test="invoiceBillCodeList != null and invoiceBillCodeList.size > 0">
|
||||
and t1.invoice_bill_code in
|
||||
<foreach item="item" collection="invoiceBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="partnerCodeList != null and partnerCodeList.size > 0">
|
||||
and t1.partner_code in
|
||||
<foreach item="item" collection="partnerCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and t1.invoice_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t1.approve_status = #{approveStatus}</if>
|
||||
<if test="startTime != null"> and t1.invoice_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.invoice_time < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -169,4 +169,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
delete from oms_payable_ticket_plan where payable_bill_id = #{payableBillId}
|
||||
</delete>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=PAYABLE) / finance_order_position:按应付单ID列表批量查询收票计划 -->
|
||||
<select id="listByPayableBillIdList" resultType="com.ruoyi.sip.domain.OmsPayableTicketPlan">
|
||||
select id, payable_bill_id, plan_ticket_date, plan_amount, plan_rate, create_time, update_time
|
||||
from oms_payable_ticket_plan
|
||||
where payable_bill_id in
|
||||
<foreach item="item" collection="payableBillIdList" separator="," open="(" close=")">#{item}</foreach>
|
||||
order by payable_bill_id asc, plan_ticket_date asc, id asc
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -159,4 +159,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<include refid="selectOmsPurchaseOrderItemHistoryVo"/>
|
||||
where purchase_history_id = #{purchaseHistoryId}
|
||||
</select>
|
||||
|
||||
<!-- ==================== MCP 采购只读查询(新增) ==================== -->
|
||||
|
||||
<!-- purchase_list(entity=HISTORY):排序键 purchase_no,id;历史表无 purchase_date,时间条件基于 create_time 区间下推 -->
|
||||
<select id="listByPurchaseNoList" resultType="com.ruoyi.sip.domain.OmsPurchaseOrderHistory">
|
||||
select
|
||||
id, purchase_id, purchase_no, buyer_name, buyer_address, vendor_id, vendor_name, vendor_user, vendor_phone,
|
||||
currency, purchaser_id, purchaser_name, purchaser_mobile, purchaser_email, warehouse_id, pay_method,
|
||||
owner_id, owner_name, total_amount, status, approve_status, approve_time, approve_node, confirm_status,
|
||||
is_virtual, del_flag, flow_type, version, create_by, create_time, update_by, update_time, remark, file_id
|
||||
from oms_purchase_order_history
|
||||
<where>
|
||||
(del_flag = 0 or del_flag is null)
|
||||
<if test="purchaseNoList != null and purchaseNoList.size > 0">
|
||||
and purchase_no in
|
||||
<foreach item="item" collection="purchaseNoList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null"> and status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and approve_status = #{approveStatus}</if>
|
||||
<if test="confirmStatus != null and confirmStatus != ''"> and confirm_status = #{confirmStatus}</if>
|
||||
<if test="flowType != null and flowType != ''"> and flow_type = #{flowType}</if>
|
||||
<if test="startTime != null"> and create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and create_time < #{endTime}</if>
|
||||
<if test="lastPurchaseNo != null and lastPurchaseNo != ''">
|
||||
and (purchase_no > #{lastPurchaseNo}
|
||||
or (purchase_no = #{lastPurchaseNo} and id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by purchase_no asc, id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 批量查历史明细(消除 N+1) -->
|
||||
<select id="listItemByPurchaseHistoryIdList" resultMap="OmsPurchaseOrderItemHistoryResult">
|
||||
<include refid="selectOmsPurchaseOrderItemHistoryVo"/>
|
||||
where purchase_history_id in
|
||||
<foreach item="item" collection="purchaseHistoryIdList" separator="," open="(" close=")">#{item}</foreach>
|
||||
order by purchase_history_id asc, id asc
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -38,4 +38,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
where order_id = #{orderId}
|
||||
</update>
|
||||
|
||||
<!-- ==================== MCP 采购只读查询(新增) ==================== -->
|
||||
|
||||
<!-- purchase_list(entity=ORDER_BIND):订单号经 project_order_info.order_code 对齐(trim 处理脏数据,方案 15.14) -->
|
||||
<select id="listByOrderCodeList" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
m.id as id,
|
||||
m.order_id as orderId,
|
||||
trim(p.order_code) as orderCode,
|
||||
m.purchase_id as purchaseId,
|
||||
o.purchase_no as purchaseNo,
|
||||
m.product_code as productCode,
|
||||
m.bind_num as bindNum
|
||||
from oms_purchase_order_map m
|
||||
inner join project_order_info p on m.order_id = p.id
|
||||
left join oms_purchase_order o on m.purchase_id = o.id
|
||||
<where>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and trim(p.order_code) in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="lastOrderId != null">
|
||||
and (m.order_id > #{lastOrderId}
|
||||
or (m.order_id = #{lastOrderId} and m.purchase_id > #{lastPurchaseId})
|
||||
or (m.order_id = #{lastOrderId} and m.purchase_id = #{lastPurchaseId} and m.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by m.order_id asc, m.purchase_id asc, m.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- 按 order_id 批量查询绑定行(辅助) -->
|
||||
<select id="listByOrderIdList" resultMap="OmsPurchaseOrderMapResult">
|
||||
select id, order_id, purchase_id, product_code, bind_num
|
||||
from oms_purchase_order_map
|
||||
where order_id in
|
||||
<foreach item="item" collection="orderIdList" separator="," open="(" close=")">#{item}</foreach>
|
||||
order by order_id asc, purchase_id asc, id asc
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -542,4 +542,156 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
#{item.price}, #{item.taxRate}, #{item.taxTotal}, #{item.amountTotal}, #{item.deliveryDate})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
<!-- ==================== MCP 采购只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- purchase_list(entity=ORDER):主表 oms_purchase_order,排序键 purchase_no,id -->
|
||||
<select id="selectPurchaseOrderPage" resultType="com.ruoyi.sip.domain.OmsPurchaseOrder">
|
||||
select
|
||||
t1.id, t1.purchase_no, t1.buyer_name, t1.buyer_address, t1.vendor_id, t1.currency,
|
||||
t1.purchaser_id, t1.purchaser_name, t1.purchaser_mobile, t1.purchaser_email,
|
||||
t1.warehouse_id, t1.pay_method, t1.owner_id, t1.owner_name, t1.remark, t1.total_amount,
|
||||
t1.status, t1.approve_status, t1.approve_time, t1.approve_node, t1.confirm_status,
|
||||
t1.is_virtual, t1.purchase_date, t1.create_time, t1.update_time, t1.del_flag, t1.version, t1.flow_type,
|
||||
t2.vendor_code, t2.vendor_name, t2.vendor_address, t2.vendor_user, t2.vendor_phone,
|
||||
t3.warehouse_name,
|
||||
(select max(it.tax_rate) from oms_purchase_order_item it where it.purchase_id = t1.id) as tax_rate
|
||||
from oms_purchase_order t1
|
||||
left join oms_vendor_info t2 on t1.vendor_id = t2.vendor_id
|
||||
left join oms_warehouse_info t3 on t1.warehouse_id = t3.id
|
||||
<where>
|
||||
(t1.del_flag = 0 or t1.del_flag is null)
|
||||
<if test="purchaseNoList != null and purchaseNoList.size > 0">
|
||||
and t1.purchase_no in
|
||||
<foreach item="item" collection="purchaseNoList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and t2.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="authVendorCodeList != null and authVendorCodeList.size > 0">
|
||||
and t2.vendor_code in
|
||||
<foreach item="item" collection="authVendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null"> and t1.status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t1.approve_status = #{approveStatus}</if>
|
||||
<if test="confirmStatus != null and confirmStatus != ''"> and t1.confirm_status = #{confirmStatus}</if>
|
||||
<if test="flowType != null and flowType != ''"> and t1.flow_type = #{flowType}</if>
|
||||
<if test="productCodeList != null and productCodeList.size > 0">
|
||||
and exists (select 1 from oms_purchase_order_item it where it.purchase_id = t1.id and it.product_code in
|
||||
<foreach item="item" collection="productCodeList" separator="," open="(" close=")">#{item}</foreach>)
|
||||
</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.purchase_date >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.purchase_date < #{endTime}</if>
|
||||
<!-- 游标 keyset -->
|
||||
<if test="lastPurchaseNo != null and lastPurchaseNo != ''">
|
||||
and (t1.purchase_no > #{lastPurchaseNo}
|
||||
or (t1.purchase_no = #{lastPurchaseNo} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.purchase_no asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- purchase_list(entity=ITEM):明细表 oms_purchase_order_item,排序键 purchase_no,id -->
|
||||
<select id="selectPurchaseOrderItemPage" resultType="com.ruoyi.sip.domain.OmsPurchaseOrderItem">
|
||||
select
|
||||
t1.id, t1.purchase_id, t1.product_code, t1.quantity, t1.inner_quantity, t1.price,
|
||||
t1.tax_rate, t1.tax_total, t1.amount_total, t1.delivery_date,
|
||||
t2.purchase_no,
|
||||
t3.model as product_model, t3.type as product_type, t3.description as product_description
|
||||
from oms_purchase_order_item t1
|
||||
inner join oms_purchase_order t2 on t1.purchase_id = t2.id
|
||||
left join product_info t3 on t1.product_code = t3.product_code
|
||||
left join oms_vendor_info t4 on t2.vendor_id = t4.vendor_id
|
||||
<where>
|
||||
(t2.del_flag = 0 or t2.del_flag is null)
|
||||
<if test="purchaseNoList != null and purchaseNoList.size > 0">
|
||||
and t2.purchase_no in
|
||||
<foreach item="item" collection="purchaseNoList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and t4.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="authVendorCodeList != null and authVendorCodeList.size > 0">
|
||||
and t4.vendor_code in
|
||||
<foreach item="item" collection="authVendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null"> and t2.status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t2.approve_status = #{approveStatus}</if>
|
||||
<if test="confirmStatus != null and confirmStatus != ''"> and t2.confirm_status = #{confirmStatus}</if>
|
||||
<if test="flowType != null and flowType != ''"> and t2.flow_type = #{flowType}</if>
|
||||
<if test="productCodeList != null and productCodeList.size > 0">
|
||||
and t1.product_code in
|
||||
<foreach item="item" collection="productCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null"> and t2.purchase_date >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t2.purchase_date < #{endTime}</if>
|
||||
<if test="lastPurchaseNo != null and lastPurchaseNo != ''">
|
||||
and (t2.purchase_no > #{lastPurchaseNo}
|
||||
or (t2.purchase_no = #{lastPurchaseNo} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t2.purchase_no asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- purchase_arrival_aggregate:动态分组聚合(SUMMARY) -->
|
||||
<select id="selectPurchaseArrivalSummary" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'"> o.purchase_no as groupKey </when>
|
||||
<when test="groupBy == 'VENDOR'"> o.vendor_id as groupKey, v.vendor_code as vendorCode, v.vendor_name as vendorName </when>
|
||||
<when test="groupBy == 'PRODUCT'"> i.product_code as groupKey </when>
|
||||
<when test="groupBy == 'STATUS'"> o.status as groupKey </when>
|
||||
<!-- 分组桶用 date_format(不参与索引过滤);时间过滤一律区间下推,见 where 中的 startTime/endTime -->
|
||||
<when test="groupBy == 'TIME_MONTH'"> date_format(o.purchase_date, '%Y-%m') as groupKey </when>
|
||||
<otherwise> 'TOTAL' as groupKey </otherwise>
|
||||
</choose>
|
||||
, sum(i.quantity) as purchaseQty
|
||||
, sum(i.inner_quantity) as innerQty
|
||||
, sum(case when i.quantity - i.inner_quantity < 0 then 0 else i.quantity - i.inner_quantity end) as pendingQty
|
||||
, sum(i.amount_total) as amountWithTax
|
||||
, sum(i.tax_total) as taxTotal
|
||||
from oms_purchase_order o
|
||||
inner join oms_purchase_order_item i on o.id = i.purchase_id
|
||||
left join oms_vendor_info v on o.vendor_id = v.vendor_id
|
||||
<where>
|
||||
(o.del_flag = 0 or o.del_flag is null)
|
||||
<if test="purchaseNoList != null and purchaseNoList.size > 0">
|
||||
and o.purchase_no in
|
||||
<foreach item="item" collection="purchaseNoList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and v.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="authVendorCodeList != null and authVendorCodeList.size > 0">
|
||||
and v.vendor_code in
|
||||
<foreach item="item" collection="authVendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and o.purchase_date >= #{startTime}</if>
|
||||
<if test="endTime != null"> and o.purchase_date < #{endTime}</if>
|
||||
</where>
|
||||
<!-- group_by=NONE 时不分组,返回单行总计;其余维度显式列出全部非聚合列,兼容 ONLY_FULL_GROUP_BY -->
|
||||
<if test="groupBy != 'NONE'">
|
||||
group by
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'"> o.purchase_no </when>
|
||||
<when test="groupBy == 'VENDOR'"> o.vendor_id, v.vendor_code, v.vendor_name </when>
|
||||
<when test="groupBy == 'PRODUCT'"> i.product_code </when>
|
||||
<when test="groupBy == 'STATUS'"> o.status </when>
|
||||
<otherwise> date_format(o.purchase_date, '%Y-%m') </otherwise>
|
||||
</choose>
|
||||
order by
|
||||
<choose>
|
||||
<when test="groupBy == 'TIME_MONTH'"> date_format(o.purchase_date, '%Y-%m') asc </when>
|
||||
<otherwise> sum(i.quantity) desc </otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
limit #{limit}
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -242,4 +242,165 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
WHERE id = #{item.id}
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=RECEIVABLE) / finance_order_position:应收单游标分页,排序键 receivable_bill_code,id -->
|
||||
<select id="listReceivableBillPage" resultType="com.ruoyi.sip.domain.OmsReceivableBill">
|
||||
select
|
||||
t1.id, t1.receivable_bill_code, t1.last_receipt_plan_id, t1.last_invoice_plan_id,
|
||||
t1.partner_code, t1.partner_name, t1.order_code, t1.inventory_code,
|
||||
t1.product_type, t1.product_code, t1.total_price_with_tax, t1.total_price_without_tax,
|
||||
t1.tax_rate, t1.tax_amount, t1.received_amount, t1.unreceived_amount,
|
||||
t1.invoiced_amount, t1.uninvoiced_amount,
|
||||
t1.create_by, t1.create_time, t1.update_by, t1.update_time, t1.remark, t1.del_flag,
|
||||
t3.project_code, t3.project_name,
|
||||
t4.plan_amount, t4.plan_receipt_date,
|
||||
t5.plan_amount as plan_invoice_amount, t5.plan_invoice_date
|
||||
from oms_receivable_bill t1
|
||||
left join project_order_info t2 on t1.order_code = t2.order_code
|
||||
left join project_info t3 on t2.project_id = t3.id
|
||||
left join oms_receivable_receipt_plan t4 on t1.last_receipt_plan_id = t4.id
|
||||
left join oms_receivable_invoice_plan t5 on t1.last_invoice_plan_id = t5.id
|
||||
<where>
|
||||
(t1.del_flag = '0' or t1.del_flag is null or t1.del_flag = '')
|
||||
<if test="receivableBillCodeList != null and receivableBillCodeList.size > 0">
|
||||
and t1.receivable_bill_code in
|
||||
<foreach item="item" collection="receivableBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="partnerCodeList != null and partnerCodeList.size > 0">
|
||||
and t1.partner_code in
|
||||
<foreach item="item" collection="partnerCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 receivable_bill_code,id) -->
|
||||
<if test="lastCode != null and lastCode != ''">
|
||||
and (t1.receivable_bill_code > #{lastCode}
|
||||
or (t1.receivable_bill_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.receivable_bill_code asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=RECEIVABLE):include_total 计数 -->
|
||||
<select id="countReceivableBillPage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_receivable_bill t1
|
||||
<where>
|
||||
(t1.del_flag = '0' or t1.del_flag is null or t1.del_flag = '')
|
||||
<if test="receivableBillCodeList != null and receivableBillCodeList.size > 0">
|
||||
and t1.receivable_bill_code in
|
||||
<foreach item="item" collection="receivableBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="partnerCodeList != null and partnerCodeList.size > 0">
|
||||
and t1.partner_code in
|
||||
<foreach item="item" collection="partnerCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!-- finance_balance_aggregate(应收侧):动态分组聚合(SUMMARY 全量 / LIST 按 order_code keyset) -->
|
||||
<select id="selectReceivableBalanceSummary" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'"> t1.order_code as groupKey </when>
|
||||
<when test="groupBy == 'PARTNER'"> t1.partner_code as groupKey, max(t1.partner_name) as groupName </when>
|
||||
<!-- 分组桶用 date_format(仅分组表达式,不参与索引过滤);时间过滤一律区间下推 -->
|
||||
<when test="groupBy == 'TIME_MONTH'"> date_format(t1.create_time, '%Y-%m') as groupKey </when>
|
||||
<when test="groupBy == 'OVERDUE_BUCKET'">
|
||||
case
|
||||
when t4.plan_receipt_date is null then 'NO_PLAN'
|
||||
when datediff(curdate(), t4.plan_receipt_date) <= 30 then '0-30'
|
||||
when datediff(curdate(), t4.plan_receipt_date) <= 60 then '31-60'
|
||||
when datediff(curdate(), t4.plan_receipt_date) <= 90 then '61-90'
|
||||
else '90+'
|
||||
end as groupKey
|
||||
</when>
|
||||
<otherwise> 'TOTAL' as groupKey </otherwise>
|
||||
</choose>
|
||||
, sum(t1.total_price_with_tax) as receivableWithTax
|
||||
, sum(t1.received_amount) as receivedWithTax
|
||||
, sum(t1.unreceived_amount) as unreceivedWithTax
|
||||
, sum(t1.uninvoiced_amount) as uninvoicedWithTax
|
||||
, count(*) as itemQty
|
||||
from oms_receivable_bill t1
|
||||
<if test="groupBy == 'OVERDUE_BUCKET'">
|
||||
left join oms_receivable_receipt_plan t4 on t1.last_receipt_plan_id = t4.id
|
||||
</if>
|
||||
<where>
|
||||
(t1.del_flag = '0' or t1.del_flag is null or t1.del_flag = '')
|
||||
<if test="orderCodeList != null and orderCodeList.size > 0">
|
||||
and t1.order_code in
|
||||
<foreach item="item" collection="orderCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="partnerCodeList != null and partnerCodeList.size > 0">
|
||||
and t1.partner_code in
|
||||
<foreach item="item" collection="partnerCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null"> and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.create_time < #{endTime}</if>
|
||||
<!-- 账龄分桶仅统计未收款行 -->
|
||||
<if test="groupBy == 'OVERDUE_BUCKET'"> and t1.unreceived_amount > 0</if>
|
||||
<!-- LIST(keyset)游标:order_code 严格递增 -->
|
||||
<if test="keyset != null and keyset == 1 and lastGroupKey != null and lastGroupKey != ''">
|
||||
and t1.order_code > #{lastGroupKey}
|
||||
</if>
|
||||
</where>
|
||||
<if test="groupBy != 'NONE'">
|
||||
group by
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'"> t1.order_code </when>
|
||||
<when test="groupBy == 'PARTNER'"> t1.partner_code </when>
|
||||
<when test="groupBy == 'TIME_MONTH'"> date_format(t1.create_time, '%Y-%m') </when>
|
||||
<otherwise>
|
||||
case
|
||||
when t4.plan_receipt_date is null then 'NO_PLAN'
|
||||
when datediff(curdate(), t4.plan_receipt_date) <= 30 then '0-30'
|
||||
when datediff(curdate(), t4.plan_receipt_date) <= 60 then '31-60'
|
||||
when datediff(curdate(), t4.plan_receipt_date) <= 90 then '61-90'
|
||||
else '90+'
|
||||
end
|
||||
</otherwise>
|
||||
</choose>
|
||||
order by
|
||||
<choose>
|
||||
<when test="groupBy == 'ORDER'">
|
||||
<choose>
|
||||
<when test="keyset != null and keyset == 1"> t1.order_code asc </when>
|
||||
<otherwise> sum(t1.total_price_with_tax) desc </otherwise>
|
||||
</choose>
|
||||
</when>
|
||||
<when test="groupBy == 'TIME_MONTH'"> date_format(t1.create_time, '%Y-%m') asc </when>
|
||||
<when test="groupBy == 'OVERDUE_BUCKET'"> field(groupKey, '0-30', '31-60', '61-90', '90+', 'NO_PLAN') </when>
|
||||
<otherwise> sum(t1.total_price_with_tax) desc </otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_balance_aggregate(as_of_date):应收历史时点重算 Σ应收(create_time ≤ T) / Σ已收(receipt_time ≤ T) -->
|
||||
<select id="selectReceivableAsOf" resultType="java.util.LinkedHashMap">
|
||||
select
|
||||
(select ifnull(sum(b.total_price_with_tax), 0)
|
||||
from oms_receivable_bill b
|
||||
where (b.del_flag = '0' or b.del_flag is null or b.del_flag = '')
|
||||
and b.create_time <= #{asOfDate}) as totalWithTax,
|
||||
(select ifnull(sum(d.receipt_amount), 0)
|
||||
from oms_receivable_receipt_detail d
|
||||
where d.receipt_time is not null
|
||||
and d.receipt_time <= #{asOfDate}) as settledAmount
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
|
|||
|
|
@ -103,4 +103,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<delete id="deleteByReceivableBillId" parameterType="Long">
|
||||
delete from oms_receivable_invoice_plan where receivable_bill_id = #{receivableBillId}
|
||||
</delete>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=RECEIVABLE) / finance_order_position:按应收单ID列表批量查询开票计划 -->
|
||||
<select id="listByReceivableBillIdList" resultType="com.ruoyi.sip.domain.OmsReceivableInvoicePlan">
|
||||
select id, receivable_bill_id, plan_invoice_date, plan_amount, plan_rate, remark, create_time, update_time
|
||||
from oms_receivable_invoice_plan
|
||||
where receivable_bill_id in
|
||||
<foreach item="item" collection="receivableBillIdList" separator="," open="(" close=")">#{item}</foreach>
|
||||
order by receivable_bill_id asc, plan_invoice_date asc, id asc
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
@ -115,4 +115,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<delete id="deleteByReceivableBillId" parameterType="Long">
|
||||
delete from oms_receivable_receipt_plan where receivable_bill_id = #{receivableBillId}
|
||||
</delete>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=RECEIVABLE) / finance_order_position:按应收单ID列表批量查询收款计划 -->
|
||||
<select id="listByReceivableBillIdList" resultType="com.ruoyi.sip.domain.OmsReceivableReceiptPlan">
|
||||
select id, receivable_bill_id, plan_receipt_date, plan_amount, plan_rate, remark, create_time, update_time
|
||||
from oms_receivable_receipt_plan
|
||||
where receivable_bill_id in
|
||||
<foreach item="item" collection="receivableBillIdList" separator="," open="(" close=")">#{item}</foreach>
|
||||
order by receivable_bill_id asc, plan_receipt_date asc, id asc
|
||||
</select>
|
||||
</mapper>
|
||||
|
|
@ -153,6 +153,51 @@
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT) ================= -->
|
||||
|
||||
<sql id="stockPageFilter">
|
||||
<if test="orderCode != null and orderCode != ''">and t1.order_code = #{orderCode}</if>
|
||||
<if test="stockStatus != null and stockStatus != ''">and t1.stock_status = #{stockStatus}</if>
|
||||
<if test="startTime != null">and t1.create_time >= #{startTime}</if>
|
||||
<if test="endTime != null">and t1.create_time < #{endTime}</if>
|
||||
</sql>
|
||||
|
||||
<!-- warehouse_list(entity=STOCK):备货信息游标分页(排序键 order_code,id) -->
|
||||
<select id="listStockPage" resultType="com.ruoyi.sip.domain.OmsStockInfo">
|
||||
select t1.id, t1.order_code, t1.stock_status, t1.once_in_stock, t1.create_time,
|
||||
t2.project_id,
|
||||
t2.id as order_id,
|
||||
t2.delivery_time,
|
||||
t2.notifier_address,
|
||||
t2.notifier,
|
||||
t2.notifier_phone,
|
||||
t3.project_name,
|
||||
t3.project_code,
|
||||
t4.all_quantity
|
||||
from oms_stock_info t1
|
||||
left join project_order_info t2 on t1.order_code = t2.order_code
|
||||
left join project_info t3 on t2.project_id = t3.id
|
||||
left join (select sum(quantity) all_quantity, project_id from project_product_info group by project_id) t4
|
||||
on t2.project_id = t4.project_id
|
||||
<where>
|
||||
<include refid="stockPageFilter"/>
|
||||
<if test="cursorKey != null">
|
||||
and (t1.order_code > #{cursorKey}
|
||||
or (t1.order_code = #{cursorKey} and t1.id > #{cursorId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.order_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<select id="countStockPage" resultType="long">
|
||||
select count(1)
|
||||
from oms_stock_info t1
|
||||
<where>
|
||||
<include refid="stockPageFilter"/>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -597,4 +597,61 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
|
||||
where ticket_bill_code = #{ticketBillCode}</select>
|
||||
|
||||
<!-- ==================== MCP 财务只读查询(新增,勿改上面既有语句语义) ==================== -->
|
||||
|
||||
<!-- finance_list(entity=TICKET):收票单游标分页,排序键 ticket_bill_code,id,时间字段 ticket_time -->
|
||||
<select id="listTicketBillPage" resultType="com.ruoyi.sip.domain.OmsTicketBill">
|
||||
select
|
||||
t1.id, t1.ticket_bill_code, t1.ticket_type, t1.ticket_bill_type,
|
||||
t1.ticket_time, t1.vendor_ticket_time, t1.actual_ticket_time,
|
||||
t1.vendor_code, t2.vendor_name,
|
||||
t1.total_price_with_tax, t1.total_price_without_tax, t1.tax_rate,
|
||||
t1.ticket_status, t1.approve_status, t1.approve_time, t1.refund_status,
|
||||
t1.create_by, t1.create_time, t1.update_by, t1.update_time, t1.remark
|
||||
from oms_ticket_bill t1
|
||||
left join oms_vendor_info t2 on t1.vendor_code = t2.vendor_code
|
||||
<where>
|
||||
<if test="ticketBillCodeList != null and ticketBillCodeList.size > 0">
|
||||
and t1.ticket_bill_code in
|
||||
<foreach item="item" collection="ticketBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and t1.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and t1.ticket_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t1.approve_status = #{approveStatus}</if>
|
||||
<!-- 时间区间下推,禁止 date_format -->
|
||||
<if test="startTime != null"> and t1.ticket_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.ticket_time < #{endTime}</if>
|
||||
<!-- 游标 keyset(排序键 ticket_bill_code,id) -->
|
||||
<if test="lastCode != null and lastCode != ''">
|
||||
and (t1.ticket_bill_code > #{lastCode}
|
||||
or (t1.ticket_bill_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.ticket_bill_code asc, t1.id asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<!-- finance_list(entity=TICKET):include_total 计数 -->
|
||||
<select id="countTicketBillPage" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
from oms_ticket_bill t1
|
||||
<where>
|
||||
<if test="ticketBillCodeList != null and ticketBillCodeList.size > 0">
|
||||
and t1.ticket_bill_code in
|
||||
<foreach item="item" collection="ticketBillCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="vendorCodeList != null and vendorCodeList.size > 0">
|
||||
and t1.vendor_code in
|
||||
<foreach item="item" collection="vendorCodeList" separator="," open="(" close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="status != null and status != ''"> and t1.ticket_status = #{status}</if>
|
||||
<if test="approveStatus != null and approveStatus != ''"> and t1.approve_status = #{approveStatus}</if>
|
||||
<if test="startTime != null"> and t1.ticket_time >= #{startTime}</if>
|
||||
<if test="endTime != null"> and t1.ticket_time < #{endTime}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
@ -499,4 +499,44 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
</foreach>
|
||||
</delete>
|
||||
|
||||
<!-- ================= MCP 只读查询(新增,仅 SELECT;不影响既有语句) ================= -->
|
||||
<select id="listProjectPage" resultType="com.ruoyi.sip.domain.ProjectInfo">
|
||||
select t1.id, t1.project_code, t1.project_name, t1.bg_property, t1.customer_code, t1.customer_name,
|
||||
t1.customer_user_name, t1.customer_phone, t1.industry_type, t1.agent_code, t1.project_stage,
|
||||
t1.construction_type, t1.project_grasp_degree, t1.hz_support_user, t1.operate_institution,
|
||||
t1.partner_code, t1.partner_name, t1.partner_user_name, t1.contact_way, t1.estimated_amount,
|
||||
t1.currency_type, t1.estimated_order_time, t1.estimated_deliver_time, t1.competitor,
|
||||
t1.country_product, t1.key_problem, t1.project_desc, t1.poc, t1.joint_trial,
|
||||
t1.create_by, t1.create_time, t1.update_by, t1.update_time,
|
||||
t2.agent_name, t3.user_name as hz_support_user_name
|
||||
from project_info t1
|
||||
left join agent_info t2 on t1.agent_code = t2.agent_code
|
||||
left join sys_user t3 on t1.hz_support_user = t3.user_id
|
||||
<where>
|
||||
<if test="codeList != null and codeList.size() > 0">
|
||||
and t1.project_code in
|
||||
<foreach item="item" collection="codeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="nameKeyword != null and nameKeyword != ''">
|
||||
and t1.project_name like concat('%', #{nameKeyword}, '%')
|
||||
</if>
|
||||
<if test="customerCodeList != null and customerCodeList.size() > 0">
|
||||
and t1.customer_code in
|
||||
<foreach item="item" collection="customerCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="agentCodeList != null and agentCodeList.size() > 0">
|
||||
and t1.agent_code in
|
||||
<foreach item="item" collection="agentCodeList" open="(" separator="," close=")">#{item}</foreach>
|
||||
</if>
|
||||
<if test="startTime != null">and t1.create_time <![CDATA[ >= ]]> #{startTime}</if>
|
||||
<if test="endTime != null">and t1.create_time <![CDATA[ < ]]> #{endTime}</if>
|
||||
<if test="lastCode != null and lastId != null">
|
||||
and (t1.project_code > #{lastCode}
|
||||
or (t1.project_code = #{lastCode} and t1.id > #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by t1.project_code, t1.id
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue