feat(mcp): add warehouse inbound query tools
This commit is contained in:
@@ -82,6 +82,8 @@ xiaoqu-agent-service/
|
||||
| `agent.list.query` | customer | read | 代理商分页查询。入参 id/name/level/channelNo/province/city/serialnumber/page/rows;出参 id/name/level/channelNo/serialnumber/supervisorName/province/city/countyseat/contact/phone/agencyarea/memo/createTime。行权限:isagent=1 只查直属下级,id 精确查可看自身。supervisorName 批量 IN 富化 |
|
||||
| `property.list.query` | customer | read | 物业(客户)分页查询(m0 properyt 表)。入参 name/agentId/serialnumber/province/city/contact/phone/page/rows;出参 id/name/serialnumber/agentName/createName/province/city/countyseat/address/contact/phone/createTime。行权限走 PermissionService 完整版 getByIsAgent 语义(不含 enterprise_type=2 限制,与老 /property/listByPage 一致)。agentName/createName 批量 IN 富化 |
|
||||
| `integration.context.get` | integration | read | 身份与能力自检(REQ-20260911-0012)。无入参;返回 tenant_id(作用域编码映射)/principal_type/subject_id/scopes(可调用工具集合,与强制校验同源实时计算)/data_scope_version(token_scope 绑定最大修改时间 epoch 毫秒)/capability_levels(QUERY/SYNC,按 *.changes.list 数据驱动)/server_time |
|
||||
| `warehouse.in.list.query` | warehouse | read | 入库单分页查询;入库/申请日期独立过滤,主单代理、涉及仓库、流程参与人三类权限取并集 |
|
||||
| `warehouse.in.detail.query` | warehouse | read | 入库单详情;支持调拨单,物料汇总、可选 SN、流程、附件和关联出库索引 |
|
||||
|
||||
分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 `notice` 字段提示。
|
||||
|
||||
|
||||
@@ -519,6 +519,34 @@
|
||||
|
||||
---
|
||||
|
||||
## 17. warehouse.in.list.query
|
||||
|
||||
分页查询入库单,参考旧系统 `POST /myWarehouseIn/allListByPage`,但将入库日期与申请日期分别过滤,
|
||||
并按本页主单 ID 批量加载仓库和待办信息。
|
||||
|
||||
- 分类:warehouse | 类型:读
|
||||
- 数据来源:m2 `warehouse_in`、`warehouse_in_details`、`warehouse_in_audit_process`
|
||||
- 行级权限:总部看全部;代理身份可看所属代理范围、所属或负责仓库涉及、或本人参与处理的入库单。
|
||||
- 单号支持入库/调拨/关联出库/商城/送货/装机/取货/撤机;另支持代理、仓库、入库类型、申请人、
|
||||
状态、同步状态、物流和入库/申请日期范围筛选。
|
||||
- 返回 `total/page/rows/records`;每条记录含 `sourceType`(ALLOCATION/OUTBOUND_LINKED/MALL/NORMAL)、
|
||||
状态文案、仓库集合、待办人、实际入库人和入库日期。
|
||||
|
||||
## 18. warehouse.in.detail.query
|
||||
|
||||
按 `id` 或 `inOrderNo` 查询详情,两者至少传一个;同时传入时必须匹配同一条可见记录。调拨入库单可直接查询。
|
||||
|
||||
- 行级权限与列表完全一致;不存在和无权统一返回 `NOT_FOUND_OR_FORBIDDEN`。
|
||||
- 返回 `record/sceneType/warehouses/items/processes/files/relatedOutOrder`。
|
||||
- `items` 按物料定义、物料索引和仓库汇总;`includeSerials` 默认 false,`serialLimit` 默认 100、上限 500,
|
||||
同时返回 `serialTotal/serialTruncated`。
|
||||
- `files.application/files.confirmation` 仅返回 `id/fileName/bucket/objectKey`。
|
||||
- `relatedOutOrder` 仅给出关联出库单号和查询工具名,不内嵌出库单内容。
|
||||
|
||||
详细设计见 `warehouse-in-mcp-tools-design.md`。
|
||||
|
||||
---
|
||||
|
||||
## 附录:在线查看工具清单
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# 入库单 MCP 工具设计
|
||||
|
||||
## 1. 范围与评审结论
|
||||
|
||||
参考旧系统 `POST /myWarehouseIn/allListByPage` 与 `GET /myWarehouseIn/lookDetail`,新增:
|
||||
|
||||
| 工具 | 分类 | 用途 |
|
||||
|---|---|---|
|
||||
| `warehouse.in.list.query` | warehouse/read | 分页查询入库单 |
|
||||
| `warehouse.in.detail.query` | warehouse/read | 按主键或入库单号查询详情 |
|
||||
|
||||
自评审通过,确认采用以下产品取舍:调拨入库单允许直接查询详情;关联出库单仅返回索引,
|
||||
不内嵌完整出库信息。工具只读 m2,不新增业务表或 Flyway migration;启动后由
|
||||
`ToolSyncService` 自动登记,需由管理员绑定 Scope 后才可调用。
|
||||
|
||||
## 2. 列表
|
||||
|
||||
- 单号:入库、调拨、关联出库、商城、送货、装机、取货、撤机单号。
|
||||
- 归属:代理、仓库、入库类型、申请人。
|
||||
- 状态与物流:订单状态、入库同步状态、物流公司和运单号。
|
||||
- 日期:`inDateFrom/inDateTo` 精确过滤 `in_date`;`applyDateFrom/applyDateTo`
|
||||
过滤 `apply_date`,结束日期含当天。不会复制旧接口将入库日期改写成申请日期的异常逻辑。
|
||||
- 分页:page 默认 1,rows 默认 20、上限 100。
|
||||
|
||||
返回主单、来源类型、状态文案、涉及仓库、待办人、实际入库人及日期、物流和关联单号。
|
||||
本页明细与待办按主单 ID 批量查询,避免 N+1。
|
||||
|
||||
`sourceType` 的优先级为 `ALLOCATION`、`OUTBOUND_LINKED`、`MALL`、`NORMAL`。
|
||||
|
||||
## 3. 详情
|
||||
|
||||
`id`、`inOrderNo` 至少传一个;同时传入时必须匹配同一条可见记录。详情返回:
|
||||
|
||||
- `record`、`sceneType`、涉及仓库;
|
||||
- 按物料定义、物料索引和仓库汇总的 `items`;
|
||||
- 可选 `serials` 以及完整 `serialTotal`、`serialTruncated`;
|
||||
- `schedule_state=2` 的已处理流程;
|
||||
- `file_type=0/1` 的申请、确认入库附件,仅返回 OSS 对象定位,不生成签名 URL;
|
||||
- `relatedOutOrder`:仅出库单号和建议调用的 `warehouse.out.detail.query` 工具名。
|
||||
|
||||
## 4. 权限与安全
|
||||
|
||||
列表和详情强制复用 `PermissionService.applyWarehouseInRowFilter`:
|
||||
|
||||
- 总部身份查看全部;
|
||||
- 代理身份可见范围是主单所属代理范围、所属或负责仓库涉及、本人参与处理三类并集;
|
||||
- 代理范围包含自己、直属下级和 `warehouse_agent_auth` 授权代管代理;
|
||||
- 权限组异常或代理用户未关联代理商时失败关闭;
|
||||
- 详情不存在与无权访问统一返回 `NOT_FOUND_OR_FORBIDDEN`。
|
||||
|
||||
权限使用主查询中的 `EXISTS`,所有业务筛选均只能缩小结果。关联出库不提前判断是否可访问,
|
||||
避免形成旁路探测;调用方若需要内容,必须再调用出库详情工具并接受其独立鉴权。
|
||||
|
||||
## 5. 验收
|
||||
|
||||
- 列表分页、日期校验、仓库过滤、批量富化和状态文案;
|
||||
- 详情双条件、调拨单、物料汇总、SN 默认关闭/截断、附件分类、出库索引;
|
||||
- 总部、代理范围、仓库负责人、流程参与人和异常配置失败关闭;
|
||||
- 固定 `TZ=Asia/Shanghai` 执行全量后端测试,不连接生产数据库、不执行部署。
|
||||
@@ -0,0 +1,56 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 入库单主表(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in")
|
||||
public class WarehouseIn implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private String inOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private LocalDate inDate;
|
||||
private Integer goodsCount;
|
||||
private Integer inCount;
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyDate;
|
||||
private String memo;
|
||||
private Integer auditLevel;
|
||||
private Integer currendAuditLevel;
|
||||
private Integer inSynchronizeState;
|
||||
private Date inSynchronizeTime;
|
||||
private String removeUserName;
|
||||
private Date removeDate;
|
||||
private String outOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long logisticsId;
|
||||
private String logisticsName;
|
||||
private String logisticsWayBill;
|
||||
private String deliveryOrderNo;
|
||||
private String installOrderNo;
|
||||
private String pickupOrderNo;
|
||||
private String withdrawOrderNo;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/** 入库单审批/处理流程(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in_audit_process")
|
||||
public class WarehouseInAuditProcess implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long warehouseInId;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Long userId;
|
||||
private String userName;
|
||||
private Date operationTime;
|
||||
private Integer scheduleState;
|
||||
private Integer auditState;
|
||||
private Integer auditLevel;
|
||||
private Integer operationTheme;
|
||||
private Integer operationType;
|
||||
private String memo;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 入库单物料/SN 明细(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_in_details")
|
||||
public class WarehouseInDetail implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long createId;
|
||||
private Date createTime;
|
||||
private Long modifyId;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private String inOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private Long warehouseInId;
|
||||
private Long materielDefinitionId;
|
||||
private String materialNo;
|
||||
private String materielFullName;
|
||||
private String materielSort;
|
||||
private Long materielIndexId;
|
||||
private String specifications;
|
||||
private String snCode;
|
||||
private Integer inCount;
|
||||
private Integer currentInventory;
|
||||
private LocalDate inDate;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Integer goodInState;
|
||||
private Date inConfirmTime;
|
||||
private String inConfirmUserName;
|
||||
private String outOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Integer assetDisposalType;
|
||||
private Integer synchronizeState;
|
||||
private Long outDetailsId;
|
||||
private String deliveryOrderNo;
|
||||
private String installOrderNo;
|
||||
private String pickupOrderNo;
|
||||
private String withdrawOrderNo;
|
||||
private String memo;
|
||||
private String lineMemo;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package me.iiv.agent.entity.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** 出入库类型(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_type")
|
||||
public class WarehouseType implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Integer warehouseType;
|
||||
private String typeName;
|
||||
private Integer sceneType;
|
||||
private String typeNo;
|
||||
private Integer assetFlag;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseInAuditProcess;
|
||||
|
||||
public interface WarehouseInAuditProcessMapper extends BaseMapper<WarehouseInAuditProcess> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseInDetail;
|
||||
|
||||
public interface WarehouseInDetailMapper extends BaseMapper<WarehouseInDetail> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseIn;
|
||||
|
||||
public interface WarehouseInMapper extends BaseMapper<WarehouseIn> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseType;
|
||||
|
||||
public interface WarehouseTypeMapper extends BaseMapper<WarehouseType> {
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m2.WarehouseIn;
|
||||
import me.iiv.agent.entity.m2.WarehouseInAuditProcess;
|
||||
import me.iiv.agent.entity.m2.WarehouseInDetail;
|
||||
import me.iiv.agent.entity.m2.WarehouseInOutPic;
|
||||
import me.iiv.agent.entity.m2.WarehouseType;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInAuditProcessMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInDetailMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseTypeMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.mcp.ToolObject;
|
||||
import me.iiv.agent.mcp.XqTool;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import me.iiv.agent.security.AuthContextHolder;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.time.format.ResolverStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 入库单查询工具,参考老系统 /myWarehouseIn/allListByPage 与 /lookDetail。 */
|
||||
@Component
|
||||
public class WarehouseInTools implements ToolObject {
|
||||
|
||||
private static final int DEFAULT_ROWS = 20;
|
||||
private static final int MAX_ROWS = 100;
|
||||
private static final int DEFAULT_SERIAL_LIMIT = 100;
|
||||
private static final int MAX_SERIAL_LIMIT = 500;
|
||||
private static final int IN_TYPE = 0;
|
||||
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$");
|
||||
private static final DateTimeFormatter DATE_FORMATTER =
|
||||
DateTimeFormatter.ISO_LOCAL_DATE.withResolverStyle(ResolverStyle.STRICT);
|
||||
private static final DateTimeFormatter TIME_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(BUSINESS_ZONE);
|
||||
|
||||
private static final Map<Integer, String> ORDER_STATE_TEXT = Map.of(
|
||||
0, "待审核", 1, "被驳回", 2, "待入库", 3, "已完成", 4, "已撤销", 5, "作废");
|
||||
private static final Map<Integer, String> SCHEDULE_STATE_TEXT = Map.of(
|
||||
0, "无需待办", 1, "待办", 2, "已办");
|
||||
private static final Map<Integer, String> AUDIT_STATE_TEXT = Map.of(0, "审核驳回", 1, "审核通过");
|
||||
private static final Map<Integer, String> OPERATION_THEME_TEXT = Map.of(
|
||||
0, "审核通过", 1, "审核驳回", 2, "撤销入库", 3, "入库确认",
|
||||
4, "驳回修改提交", 5, "撤销", 6, "入库单确认驳回");
|
||||
private static final Map<Integer, String> OPERATION_TYPE_TEXT = Map.of(
|
||||
0, "审批流操作", 1, "确认入库", 2, "本人修改", 3, "驳回或修改记录");
|
||||
|
||||
final WarehouseInMapper warehouseInMapper;
|
||||
final WarehouseInDetailMapper detailMapper;
|
||||
final WarehouseInAuditProcessMapper processMapper;
|
||||
final WarehouseInOutPicMapper picMapper;
|
||||
final WarehouseTypeMapper warehouseTypeMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public WarehouseInTools(WarehouseInMapper warehouseInMapper,
|
||||
WarehouseInDetailMapper detailMapper,
|
||||
WarehouseInAuditProcessMapper processMapper,
|
||||
WarehouseInOutPicMapper picMapper,
|
||||
WarehouseTypeMapper warehouseTypeMapper,
|
||||
PermissionService permissionService) {
|
||||
this.warehouseInMapper = warehouseInMapper;
|
||||
this.detailMapper = detailMapper;
|
||||
this.processMapper = processMapper;
|
||||
this.picMapper = picMapper;
|
||||
this.warehouseTypeMapper = warehouseTypeMapper;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@XqTool(category = "warehouse", kind = "read")
|
||||
@Tool(name = "warehouse.in.list.query", description = "分页查询入库单,参考老系统 /myWarehouseIn/allListByPage。"
|
||||
+ "总部可看全部;代理身份可看所属代理范围、所属或负责仓库涉及、或本人参与处理的入库单。"
|
||||
+ "入库日期与申请日期分别过滤;日期格式 yyyy-MM-dd,结束日期含当天。"
|
||||
+ "分页 page 默认1,rows 默认20、上限100。orderState:0待审核、1被驳回、2待入库、3已完成、4已撤销、5作废。")
|
||||
public Map<String, Object> warehouseInListQuery(
|
||||
@ToolParam(required = false, description = "入库单号,模糊匹配") String inOrderNo,
|
||||
@ToolParam(required = false, description = "调拨单号,模糊匹配") String allocationOrderNo,
|
||||
@ToolParam(required = false, description = "关联出库单号,模糊匹配") String outOrderNo,
|
||||
@ToolParam(required = false, description = "商城订单号,模糊匹配") String mallOrderNo,
|
||||
@ToolParam(required = false, description = "送货单号,模糊匹配") String deliveryOrderNo,
|
||||
@ToolParam(required = false, description = "装机单号,模糊匹配") String installOrderNo,
|
||||
@ToolParam(required = false, description = "取货工单号,模糊匹配") String pickupOrderNo,
|
||||
@ToolParam(required = false, description = "撤机工单号,模糊匹配") String withdrawOrderNo,
|
||||
@ToolParam(required = false, description = "所属代理商 id") Long agentId,
|
||||
@ToolParam(required = false, description = "明细涉及的仓库 id") Long warehouseId,
|
||||
@ToolParam(required = false, description = "入库类型 id") Long warehouseTypeId,
|
||||
@ToolParam(required = false, description = "申请人 id") Long applyUserId,
|
||||
@ToolParam(required = false, description = "状态:0待审核 1被驳回 2待入库 3已完成 4已撤销 5作废") Integer orderState,
|
||||
@ToolParam(required = false, description = "入库同步状态:0未同步 1已同步 2无需同步") Integer inSynchronizeState,
|
||||
@ToolParam(required = false, description = "物流公司 id") Long logisticsId,
|
||||
@ToolParam(required = false, description = "物流运单号,模糊匹配") String logisticsWayBill,
|
||||
@ToolParam(required = false, description = "入库日期起,yyyy-MM-dd") String inDateFrom,
|
||||
@ToolParam(required = false, description = "入库日期止,yyyy-MM-dd,含当天") String inDateTo,
|
||||
@ToolParam(required = false, description = "申请日期起,yyyy-MM-dd") String applyDateFrom,
|
||||
@ToolParam(required = false, description = "申请日期止,yyyy-MM-dd,含当天") String applyDateTo,
|
||||
@ToolParam(required = false, description = "页码,从1开始,默认1") Integer page,
|
||||
@ToolParam(required = false, description = "每页条数,默认20,上限100") Integer rows) {
|
||||
LocalDate inStart = parseLocalDate(inDateFrom, "inDateFrom");
|
||||
LocalDate inEnd = parseLocalDate(inDateTo, "inDateTo");
|
||||
LocalDate applyStartDate = parseLocalDate(applyDateFrom, "applyDateFrom");
|
||||
LocalDate applyEndDate = parseLocalDate(applyDateTo, "applyDateTo");
|
||||
validateRange(inStart, inEnd, "inDateFrom", "inDateTo");
|
||||
validateRange(applyStartDate, applyEndDate, "applyDateFrom", "applyDateTo");
|
||||
|
||||
int pageNo = page == null || page < 1 ? 1 : page;
|
||||
boolean truncated = rows != null && rows > MAX_ROWS;
|
||||
int pageSize = rows == null || rows < 1 ? DEFAULT_ROWS : Math.min(rows, MAX_ROWS);
|
||||
LambdaQueryWrapper<WarehouseIn> wrapper = new LambdaQueryWrapper<WarehouseIn>()
|
||||
.eq(WarehouseIn::getSoftDelete, 0);
|
||||
like(wrapper, WarehouseIn::getInOrderNo, inOrderNo);
|
||||
like(wrapper, WarehouseIn::getAllocationOrderNo, allocationOrderNo);
|
||||
like(wrapper, WarehouseIn::getOutOrderNo, outOrderNo);
|
||||
like(wrapper, WarehouseIn::getMallOrderNo, mallOrderNo);
|
||||
like(wrapper, WarehouseIn::getDeliveryOrderNo, deliveryOrderNo);
|
||||
like(wrapper, WarehouseIn::getInstallOrderNo, installOrderNo);
|
||||
like(wrapper, WarehouseIn::getPickupOrderNo, pickupOrderNo);
|
||||
like(wrapper, WarehouseIn::getWithdrawOrderNo, withdrawOrderNo);
|
||||
like(wrapper, WarehouseIn::getLogisticsWayBill, logisticsWayBill);
|
||||
wrapper.eq(agentId != null, WarehouseIn::getAgentId, agentId)
|
||||
.eq(warehouseTypeId != null, WarehouseIn::getWarehouseTypeId, warehouseTypeId)
|
||||
.eq(applyUserId != null, WarehouseIn::getApplyUserId, applyUserId)
|
||||
.eq(orderState != null, WarehouseIn::getOrderState, orderState)
|
||||
.eq(inSynchronizeState != null, WarehouseIn::getInSynchronizeState, inSynchronizeState)
|
||||
.eq(logisticsId != null, WarehouseIn::getLogisticsId, logisticsId)
|
||||
.ge(inStart != null, WarehouseIn::getInDate, inStart)
|
||||
.le(inEnd != null, WarehouseIn::getInDate, inEnd)
|
||||
.ge(applyStartDate != null, WarehouseIn::getApplyDate, startOfDay(applyStartDate))
|
||||
.lt(applyEndDate != null, WarehouseIn::getApplyDate, nextDayStart(applyEndDate));
|
||||
if (warehouseId != null) {
|
||||
wrapper.apply("EXISTS (SELECT 1 FROM warehouse_in_details wid_filter "
|
||||
+ "WHERE wid_filter.warehouse_in_id = warehouse_in.id "
|
||||
+ "AND wid_filter.soft_delete = 0 AND wid_filter.warehouse_id = {0})", warehouseId);
|
||||
}
|
||||
User user = AuthContextHolder.get().getUser();
|
||||
permissionService.applyWarehouseInRowFilter(wrapper, user);
|
||||
|
||||
Long total = warehouseInMapper.selectCount(wrapper);
|
||||
wrapper.orderByDesc(WarehouseIn::getId)
|
||||
.last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize);
|
||||
List<WarehouseIn> orders = warehouseInMapper.selectList(wrapper);
|
||||
Map<Long, List<WarehouseInDetail>> details = listDetails(orders);
|
||||
Map<Long, List<WarehouseInAuditProcess>> schedules = listSchedules(orders);
|
||||
List<Map<String, Object>> records = orders.stream().map(order -> listRecord(order,
|
||||
details.getOrDefault(order.getId(), List.of()),
|
||||
schedules.getOrDefault(order.getId(), List.of()))).collect(Collectors.toList());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("total", total);
|
||||
result.put("page", pageNo);
|
||||
result.put("rows", pageSize);
|
||||
if (truncated) {
|
||||
result.put("notice", "rows 超过上限 100,已截断为 100");
|
||||
}
|
||||
result.put("records", records);
|
||||
return result;
|
||||
}
|
||||
|
||||
@XqTool(category = "warehouse", kind = "read")
|
||||
@Tool(name = "warehouse.in.detail.query", description = "按 id 或入库单号查询入库单详情。"
|
||||
+ "行级权限与 warehouse.in.list.query 完全一致,不存在与无权访问统一返回 NOT_FOUND_OR_FORBIDDEN。"
|
||||
+ "返回主单、场景类型、仓库、按物料和仓库汇总的明细、可选 SN、已处理流程和申请/确认附件;调拨入库单可直接查询。"
|
||||
+ "关联出库仅返回索引,不内嵌内容。includeSerials 默认 false;serialLimit 默认100、上限500。")
|
||||
public Map<String, Object> warehouseInDetailQuery(
|
||||
@ToolParam(required = false, description = "入库单主键 id,与 inOrderNo 至少传一个") Long id,
|
||||
@ToolParam(required = false, description = "入库单号,与 id 至少传一个") String inOrderNo,
|
||||
@ToolParam(required = false, description = "是否返回 SN 明细,默认 false") Boolean includeSerials,
|
||||
@ToolParam(required = false, description = "SN 返回上限,默认100、最大500") Integer serialLimit) {
|
||||
if ((id == null || id <= 0) && !StringUtils.hasText(inOrderNo)) {
|
||||
throw new IllegalArgumentException("id、inOrderNo 至少传一个");
|
||||
}
|
||||
if (id != null && id <= 0) {
|
||||
throw new IllegalArgumentException("id 必须为正整数");
|
||||
}
|
||||
int limit = serialLimit == null || serialLimit < 1
|
||||
? DEFAULT_SERIAL_LIMIT : Math.min(serialLimit, MAX_SERIAL_LIMIT);
|
||||
boolean include = Boolean.TRUE.equals(includeSerials);
|
||||
|
||||
LambdaQueryWrapper<WarehouseIn> wrapper = new LambdaQueryWrapper<WarehouseIn>()
|
||||
.eq(WarehouseIn::getSoftDelete, 0);
|
||||
wrapper.eq(id != null, WarehouseIn::getId, id);
|
||||
wrapper.eq(StringUtils.hasText(inOrderNo), WarehouseIn::getInOrderNo,
|
||||
StringUtils.hasText(inOrderNo) ? inOrderNo.trim() : inOrderNo);
|
||||
permissionService.applyWarehouseInRowFilter(wrapper, AuthContextHolder.get().getUser());
|
||||
List<WarehouseIn> visible = warehouseInMapper.selectList(wrapper.orderByDesc(WarehouseIn::getId).last("LIMIT 1"));
|
||||
if (visible.isEmpty()) {
|
||||
throw new ToolCallException(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, "入库单不存在或当前身份无权查看");
|
||||
}
|
||||
WarehouseIn order = visible.get(0);
|
||||
List<WarehouseInDetail> detailRows = detailMapper.selectList(new LambdaQueryWrapper<WarehouseInDetail>()
|
||||
.eq(WarehouseInDetail::getSoftDelete, 0)
|
||||
.eq(WarehouseInDetail::getWarehouseInId, order.getId())
|
||||
.orderByAsc(WarehouseInDetail::getId));
|
||||
List<WarehouseInDetail> serialCandidates = detailRows.stream()
|
||||
.filter(row -> StringUtils.hasText(row.getSnCode())).collect(Collectors.toList());
|
||||
List<WarehouseInDetail> serialRows = include
|
||||
? serialCandidates.stream().limit(limit).collect(Collectors.toList()) : List.of();
|
||||
List<WarehouseInAuditProcess> processes = processMapper.selectList(
|
||||
new LambdaQueryWrapper<WarehouseInAuditProcess>()
|
||||
.eq(WarehouseInAuditProcess::getSoftDelete, 0)
|
||||
.eq(WarehouseInAuditProcess::getWarehouseInId, order.getId())
|
||||
.eq(WarehouseInAuditProcess::getScheduleState, 2)
|
||||
.orderByAsc(WarehouseInAuditProcess::getOperationTime)
|
||||
.orderByAsc(WarehouseInAuditProcess::getId));
|
||||
List<WarehouseInOutPic> pics = picMapper.selectList(new LambdaQueryWrapper<WarehouseInOutPic>()
|
||||
.eq(WarehouseInOutPic::getSoftDelete, 0)
|
||||
.eq(WarehouseInOutPic::getAggregateId, order.getId())
|
||||
.eq(WarehouseInOutPic::getInOutAllocationType, IN_TYPE)
|
||||
.orderByAsc(WarehouseInOutPic::getId));
|
||||
WarehouseType type = order.getWarehouseTypeId() == null
|
||||
? null : warehouseTypeMapper.selectById(order.getWarehouseTypeId());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("record", record(order));
|
||||
result.put("sceneType", type == null || !Integer.valueOf(0).equals(type.getSoftDelete()) ? null : type.getSceneType());
|
||||
result.put("warehouses", warehouseRecords(detailRows));
|
||||
result.put("items", aggregateItems(detailRows));
|
||||
result.put("serials", serialRows.stream().map(this::serialRecord).collect(Collectors.toList()));
|
||||
result.put("serialTotal", (long) serialCandidates.size());
|
||||
result.put("serialTruncated", include && serialCandidates.size() > serialRows.size());
|
||||
result.put("processes", processes.stream().map(this::processRecord).collect(Collectors.toList()));
|
||||
Map<String, Object> files = new LinkedHashMap<>();
|
||||
files.put("application", fileRecords(pics, 0));
|
||||
files.put("confirmation", fileRecords(pics, 1));
|
||||
result.put("files", files);
|
||||
result.put("relatedOutOrder", relatedOutOrder(order));
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Long, List<WarehouseInDetail>> listDetails(List<WarehouseIn> orders) {
|
||||
if (orders.isEmpty()) return Map.of();
|
||||
List<Long> ids = orders.stream().map(WarehouseIn::getId).collect(Collectors.toList());
|
||||
return detailMapper.selectList(new LambdaQueryWrapper<WarehouseInDetail>()
|
||||
.eq(WarehouseInDetail::getSoftDelete, 0)
|
||||
.in(WarehouseInDetail::getWarehouseInId, ids)
|
||||
.orderByAsc(WarehouseInDetail::getId)).stream()
|
||||
.collect(Collectors.groupingBy(WarehouseInDetail::getWarehouseInId,
|
||||
LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
private Map<Long, List<WarehouseInAuditProcess>> listSchedules(List<WarehouseIn> orders) {
|
||||
if (orders.isEmpty()) return Map.of();
|
||||
List<Long> ids = orders.stream().map(WarehouseIn::getId).collect(Collectors.toList());
|
||||
return processMapper.selectList(new LambdaQueryWrapper<WarehouseInAuditProcess>()
|
||||
.eq(WarehouseInAuditProcess::getSoftDelete, 0)
|
||||
.in(WarehouseInAuditProcess::getWarehouseInId, ids)
|
||||
.eq(WarehouseInAuditProcess::getScheduleState, 1)
|
||||
.orderByAsc(WarehouseInAuditProcess::getId)).stream()
|
||||
.collect(Collectors.groupingBy(WarehouseInAuditProcess::getWarehouseInId,
|
||||
LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
private Map<String, Object> listRecord(WarehouseIn order, List<WarehouseInDetail> details,
|
||||
List<WarehouseInAuditProcess> schedules) {
|
||||
Map<String, Object> record = record(order);
|
||||
LinkedHashMap<Long, String> warehouses = warehouseMap(details);
|
||||
record.put("warehouseIds", new ArrayList<>(warehouses.keySet()));
|
||||
record.put("warehouseNames", new ArrayList<>(warehouses.values()));
|
||||
record.put("schedulePeople", distinctNonBlank(schedules.stream()
|
||||
.map(WarehouseInAuditProcess::getUserName).collect(Collectors.toList())));
|
||||
record.put("warehouseInPeople", distinctNonBlank(details.stream()
|
||||
.map(WarehouseInDetail::getInConfirmUserName).collect(Collectors.toList())));
|
||||
record.put("warehouseInTimes", details.stream().map(WarehouseInDetail::getInDate)
|
||||
.filter(Objects::nonNull).map(LocalDate::toString).distinct().collect(Collectors.toList()));
|
||||
return record;
|
||||
}
|
||||
|
||||
private Map<String, Object> record(WarehouseIn order) {
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("id", order.getId());
|
||||
record.put("inOrderNo", order.getInOrderNo());
|
||||
record.put("allocationOrderNo", order.getAllocationOrderNo());
|
||||
record.put("outOrderNo", order.getOutOrderNo());
|
||||
record.put("mallOrderNo", order.getMallOrderNo());
|
||||
record.put("sourceType", sourceType(order));
|
||||
record.put("agentId", order.getAgentId());
|
||||
record.put("agentName", order.getAgentName());
|
||||
record.put("warehouseTypeId", order.getWarehouseTypeId());
|
||||
record.put("warehouseTypeName", order.getWarehouseTypeName());
|
||||
record.put("inDate", order.getInDate() == null ? null : order.getInDate().toString());
|
||||
record.put("goodsCount", order.getGoodsCount());
|
||||
record.put("inCount", order.getInCount());
|
||||
record.put("orderState", order.getOrderState());
|
||||
record.put("orderStateText", text(ORDER_STATE_TEXT, order.getOrderState()));
|
||||
record.put("applyUserId", order.getApplyUserId());
|
||||
record.put("applyUserName", order.getApplyUserName());
|
||||
record.put("applyDate", formatTime(order.getApplyDate()));
|
||||
record.put("inSynchronizeState", order.getInSynchronizeState());
|
||||
record.put("inSynchronizeTime", formatTime(order.getInSynchronizeTime()));
|
||||
record.put("logisticsId", order.getLogisticsId());
|
||||
record.put("logisticsName", order.getLogisticsName());
|
||||
record.put("logisticsWayBill", order.getLogisticsWayBill());
|
||||
record.put("deliveryOrderNo", order.getDeliveryOrderNo());
|
||||
record.put("installOrderNo", order.getInstallOrderNo());
|
||||
record.put("pickupOrderNo", order.getPickupOrderNo());
|
||||
record.put("withdrawOrderNo", order.getWithdrawOrderNo());
|
||||
record.put("memo", order.getMemo());
|
||||
record.put("createTime", formatTime(order.getCreateTime()));
|
||||
record.put("modifyTime", formatTime(order.getModifyTime()));
|
||||
return record;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> aggregateItems(List<WarehouseInDetail> rows) {
|
||||
Map<String, List<WarehouseInDetail>> groups = rows.stream().collect(Collectors.groupingBy(
|
||||
row -> Objects.toString(row.getMaterielDefinitionId(), "") + "|"
|
||||
+ Objects.toString(row.getMaterielIndexId(), "") + "|"
|
||||
+ Objects.toString(row.getWarehouseId(), ""), LinkedHashMap::new, Collectors.toList()));
|
||||
return groups.values().stream().map(group -> {
|
||||
WarehouseInDetail first = group.get(0);
|
||||
Map<String, Object> item = commonDetailRecord(first);
|
||||
item.put("inCount", group.stream().map(WarehouseInDetail::getInCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
|
||||
item.put("currentInventory", first.getCurrentInventory());
|
||||
item.put("inDate", first.getInDate() == null ? null : first.getInDate().toString());
|
||||
item.put("inConfirmUserName", first.getInConfirmUserName());
|
||||
item.put("inConfirmTime", formatTime(first.getInConfirmTime()));
|
||||
item.put("lineMemo", first.getLineMemo());
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> serialRecord(WarehouseInDetail detail) {
|
||||
Map<String, Object> item = commonDetailRecord(detail);
|
||||
item.put("snCode", detail.getSnCode());
|
||||
item.put("goodInState", detail.getGoodInState());
|
||||
item.put("assetDisposalType", detail.getAssetDisposalType());
|
||||
item.put("synchronizeState", detail.getSynchronizeState());
|
||||
item.put("outDetailsId", detail.getOutDetailsId());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> commonDetailRecord(WarehouseInDetail detail) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", detail.getId());
|
||||
item.put("materielDefinitionId", detail.getMaterielDefinitionId());
|
||||
item.put("materielIndexId", detail.getMaterielIndexId());
|
||||
item.put("materialNo", detail.getMaterialNo());
|
||||
item.put("materielFullName", detail.getMaterielFullName());
|
||||
item.put("materielSort", detail.getMaterielSort());
|
||||
item.put("specifications", detail.getSpecifications());
|
||||
item.put("warehouseId", detail.getWarehouseId());
|
||||
item.put("warehouseName", detail.getWarehouseName());
|
||||
return item;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> warehouseRecords(List<WarehouseInDetail> rows) {
|
||||
return warehouseMap(rows).entrySet().stream().map(entry -> {
|
||||
Map<String, Object> warehouse = new LinkedHashMap<>();
|
||||
warehouse.put("id", entry.getKey());
|
||||
warehouse.put("name", entry.getValue());
|
||||
return warehouse;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static LinkedHashMap<Long, String> warehouseMap(List<WarehouseInDetail> rows) {
|
||||
LinkedHashMap<Long, String> warehouses = new LinkedHashMap<>();
|
||||
for (WarehouseInDetail row : rows) {
|
||||
if (row.getWarehouseId() != null) warehouses.putIfAbsent(row.getWarehouseId(), row.getWarehouseName());
|
||||
}
|
||||
return warehouses;
|
||||
}
|
||||
|
||||
private Map<String, Object> processRecord(WarehouseInAuditProcess process) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", process.getId());
|
||||
item.put("warehouseId", process.getWarehouseId());
|
||||
item.put("warehouseName", process.getWarehouseName());
|
||||
item.put("userId", process.getUserId());
|
||||
item.put("userName", process.getUserName());
|
||||
item.put("operationTime", formatTime(process.getOperationTime()));
|
||||
item.put("scheduleState", process.getScheduleState());
|
||||
item.put("scheduleStateText", text(SCHEDULE_STATE_TEXT, process.getScheduleState()));
|
||||
item.put("auditState", process.getAuditState());
|
||||
item.put("auditStateText", text(AUDIT_STATE_TEXT, process.getAuditState()));
|
||||
item.put("auditLevel", process.getAuditLevel());
|
||||
item.put("operationTheme", process.getOperationTheme());
|
||||
item.put("operationThemeText", text(OPERATION_THEME_TEXT, process.getOperationTheme()));
|
||||
item.put("operationType", process.getOperationType());
|
||||
item.put("operationTypeText", text(OPERATION_TYPE_TEXT, process.getOperationType()));
|
||||
item.put("memo", process.getMemo());
|
||||
return item;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> fileRecords(List<WarehouseInOutPic> pics, int fileType) {
|
||||
return pics.stream().filter(pic -> Integer.valueOf(fileType).equals(pic.getFileType()))
|
||||
.map(pic -> {
|
||||
Map<String, Object> file = new LinkedHashMap<>();
|
||||
file.put("id", pic.getId());
|
||||
file.put("fileName", pic.getFileName());
|
||||
file.put("bucket", pic.getOssbucketname());
|
||||
file.put("objectKey", nullToEmpty(pic.getOssfolder()) + nullToEmpty(pic.getFileName()));
|
||||
return file;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Map<String, Object> relatedOutOrder(WarehouseIn order) {
|
||||
if (!StringUtils.hasText(order.getOutOrderNo())) return null;
|
||||
Map<String, Object> related = new LinkedHashMap<>();
|
||||
related.put("outOrderNo", order.getOutOrderNo());
|
||||
related.put("queryTool", "warehouse.out.detail.query");
|
||||
return related;
|
||||
}
|
||||
|
||||
private static void like(LambdaQueryWrapper<WarehouseIn> wrapper,
|
||||
SFunction<WarehouseIn, ?> column, String value) {
|
||||
if (StringUtils.hasText(value)) wrapper.like(column, value.trim());
|
||||
}
|
||||
|
||||
private static List<String> distinctNonBlank(List<String> values) {
|
||||
Set<String> distinct = values.stream().filter(StringUtils::hasText)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
return new ArrayList<>(distinct);
|
||||
}
|
||||
|
||||
private static String sourceType(WarehouseIn order) {
|
||||
if (StringUtils.hasText(order.getAllocationOrderNo())) return "ALLOCATION";
|
||||
if (StringUtils.hasText(order.getOutOrderNo())) return "OUTBOUND_LINKED";
|
||||
if (StringUtils.hasText(order.getMallOrderNo())) return "MALL";
|
||||
return "NORMAL";
|
||||
}
|
||||
|
||||
private static String text(Map<Integer, String> texts, Integer code) {
|
||||
return code == null ? null : texts.getOrDefault(code, "未知");
|
||||
}
|
||||
|
||||
private static String formatTime(Date date) {
|
||||
return date == null ? null : TIME_FORMATTER.format(date.toInstant());
|
||||
}
|
||||
|
||||
private static String nullToEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private static LocalDate parseLocalDate(String value, String name) {
|
||||
if (!StringUtils.hasText(value)) return null;
|
||||
String trimmed = value.trim();
|
||||
if (!DATE_PATTERN.matcher(trimmed).matches()) {
|
||||
throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd");
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(trimmed, DATE_FORMATTER);
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd");
|
||||
}
|
||||
}
|
||||
|
||||
private static Date startOfDay(LocalDate date) {
|
||||
return date == null ? null : Date.from(date.atStartOfDay(BUSINESS_ZONE).toInstant());
|
||||
}
|
||||
|
||||
private static Date nextDayStart(LocalDate date) {
|
||||
return date == null ? null : Date.from(date.plusDays(1).atStartOfDay(BUSINESS_ZONE).toInstant());
|
||||
}
|
||||
|
||||
private static <T extends Comparable<T>> void validateRange(T from, T to, String fromName, String toName) {
|
||||
if (from != null && to != null && from.compareTo(to) > 0) {
|
||||
throw new IllegalArgumentException(fromName + " 不能晚于 " + toName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.MallGoodsOrder;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
|
||||
import me.iiv.agent.entity.m2.WarehouseIn;
|
||||
import me.iiv.agent.entity.m2.WarehouseOut;
|
||||
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
@@ -159,6 +160,37 @@ public class PermissionService {
|
||||
.or().apply(participantExists, user.getId()));
|
||||
}
|
||||
|
||||
/** 入库单权限:主单代理、可管理仓库、本人参与流程三类可见范围取并集。 */
|
||||
public void applyWarehouseInRowFilter(LambdaQueryWrapper<WarehouseIn> wrapper, User user) {
|
||||
List<Long> agentIds = visibleAgentIds(user);
|
||||
if (agentIds == null) {
|
||||
return;
|
||||
}
|
||||
if (agentIds.isEmpty()) {
|
||||
wrapper.eq(WarehouseIn::getId, 0L);
|
||||
return;
|
||||
}
|
||||
|
||||
String firstIds = placeholders(0, agentIds.size());
|
||||
String secondIds = placeholders(agentIds.size(), agentIds.size());
|
||||
Object[] warehouseArgs = new Object[agentIds.size() * 2];
|
||||
for (int i = 0; i < agentIds.size(); i++) {
|
||||
warehouseArgs[i] = agentIds.get(i);
|
||||
warehouseArgs[i + agentIds.size()] = agentIds.get(i);
|
||||
}
|
||||
String visibleWarehouseExists = "EXISTS (SELECT 1 FROM warehouse_in_details wid "
|
||||
+ "JOIN warehouse w ON w.id = wid.warehouse_id AND w.soft_delete = 0 "
|
||||
+ "WHERE wid.warehouse_in_id = warehouse_in.id AND wid.soft_delete = 0 "
|
||||
+ "AND (w.agent_id IN (" + firstIds + ") OR w.manager_agent_id IN (" + secondIds + ")))";
|
||||
String participantExists = "EXISTS (SELECT 1 FROM warehouse_in_audit_process wiap "
|
||||
+ "WHERE wiap.warehouse_in_id = warehouse_in.id AND wiap.soft_delete = 0 "
|
||||
+ "AND wiap.user_id = {0})";
|
||||
|
||||
wrapper.and(w -> w.in(WarehouseIn::getAgentId, agentIds)
|
||||
.or().apply(visibleWarehouseExists, warehouseArgs)
|
||||
.or().apply(participantExists, user.getId()));
|
||||
}
|
||||
|
||||
private static String placeholders(int offset, int size) {
|
||||
return IntStream.range(offset, offset + size)
|
||||
.mapToObj(i -> "{" + i + "}")
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m2.WarehouseIn;
|
||||
import me.iiv.agent.entity.m2.WarehouseInAuditProcess;
|
||||
import me.iiv.agent.entity.m2.WarehouseInDetail;
|
||||
import me.iiv.agent.entity.m2.WarehouseInOutPic;
|
||||
import me.iiv.agent.entity.m2.WarehouseType;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInAuditProcessMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInDetailMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseTypeMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import me.iiv.agent.security.AuthContext;
|
||||
import me.iiv.agent.security.AuthContextHolder;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WarehouseInToolsTest {
|
||||
|
||||
@Mock WarehouseInMapper warehouseInMapper;
|
||||
@Mock WarehouseInDetailMapper detailMapper;
|
||||
@Mock WarehouseInAuditProcessMapper processMapper;
|
||||
@Mock WarehouseInOutPicMapper picMapper;
|
||||
@Mock WarehouseTypeMapper warehouseTypeMapper;
|
||||
@Mock PermissionService permissionService;
|
||||
|
||||
WarehouseInTools tools;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseIn.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseInDetail.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseInAuditProcess.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseType.class);
|
||||
tools = new WarehouseInTools(warehouseInMapper, detailMapper, processMapper, picMapper,
|
||||
warehouseTypeMapper, permissionService);
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
AuthContextHolder.set(new AuthContext(null, user));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
AuthContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAppliesPermissionAndBatchLoadsRelations() {
|
||||
WarehouseIn order = order();
|
||||
when(warehouseInMapper.selectCount(any())).thenReturn(1L);
|
||||
when(warehouseInMapper.selectList(any())).thenReturn(List.of(order));
|
||||
WarehouseInDetail detail = detail("SN-1", 2);
|
||||
when(detailMapper.selectList(any())).thenReturn(List.of(detail));
|
||||
WarehouseInAuditProcess schedule = new WarehouseInAuditProcess();
|
||||
schedule.setWarehouseInId(20L);
|
||||
schedule.setUserName("王五");
|
||||
when(processMapper.selectList(any())).thenReturn(List.of(schedule));
|
||||
|
||||
Map<String, Object> result = list(0, 500);
|
||||
|
||||
assertEquals(1L, result.get("total"));
|
||||
assertEquals(1, result.get("page"));
|
||||
assertEquals(100, result.get("rows"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> record = ((List<Map<String, Object>>) result.get("records")).get(0);
|
||||
assertEquals("ALLOCATION", record.get("sourceType"));
|
||||
assertEquals("已完成", record.get("orderStateText"));
|
||||
assertEquals(List.of(8L), record.get("warehouseIds"));
|
||||
assertEquals(List.of("一号仓"), record.get("warehouseNames"));
|
||||
assertEquals(List.of("李四"), record.get("warehouseInPeople"));
|
||||
assertEquals(List.of("王五"), record.get("schedulePeople"));
|
||||
verify(permissionService).applyWarehouseInRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listRejectsInvalidAndReversedDates() {
|
||||
assertThrows(IllegalArgumentException.class, () -> listWithDates("2026-02-30", null));
|
||||
assertThrows(IllegalArgumentException.class, () -> listWithDates("2026-09-16", "2026-09-15"));
|
||||
verifyNoInteractions(warehouseInMapper, permissionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailUsesSamePermissionAndHidesExistence() {
|
||||
when(warehouseInMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
ToolCallException error = assertThrows(ToolCallException.class,
|
||||
() -> tools.warehouseInDetailQuery(20L, null, false, null));
|
||||
|
||||
assertEquals(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, error.getCode());
|
||||
verify(permissionService).applyWarehouseInRowFilter(any(), any());
|
||||
verifyNoInteractions(detailMapper, processMapper, picMapper, warehouseTypeMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailAllowsAllocationAndReturnsAggregatesSerialsFilesAndOutIndex() {
|
||||
WarehouseIn order = order();
|
||||
order.setOutOrderNo("CK-001");
|
||||
when(warehouseInMapper.selectList(any())).thenReturn(List.of(order));
|
||||
WarehouseInDetail first = detail("SN-1", 1);
|
||||
WarehouseInDetail second = detail("SN-2", 2);
|
||||
when(detailMapper.selectList(any())).thenReturn(List.of(first, second));
|
||||
WarehouseInAuditProcess process = new WarehouseInAuditProcess();
|
||||
process.setOperationTheme(3);
|
||||
process.setOperationType(1);
|
||||
when(processMapper.selectList(any())).thenReturn(List.of(process));
|
||||
when(picMapper.selectList(any())).thenReturn(List.of(pic(1L, 0), pic(2L, 1)));
|
||||
WarehouseType type = new WarehouseType();
|
||||
type.setSoftDelete(0);
|
||||
type.setSceneType(1);
|
||||
when(warehouseTypeMapper.selectById(3L)).thenReturn(type);
|
||||
|
||||
Map<String, Object> result = tools.warehouseInDetailQuery(20L, "RK-001", true, 1);
|
||||
|
||||
assertEquals(1, result.get("sceneType"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) result.get("items");
|
||||
assertEquals(1, items.size());
|
||||
assertEquals(3, items.get(0).get("inCount"));
|
||||
assertEquals(2L, result.get("serialTotal"));
|
||||
assertTrue((Boolean) result.get("serialTruncated"));
|
||||
assertEquals(1, ((List<?>) result.get("serials")).size());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> files = (Map<String, Object>) result.get("files");
|
||||
assertEquals(1, ((List<?>) files.get("application")).size());
|
||||
assertEquals(1, ((List<?>) files.get("confirmation")).size());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> related = (Map<String, Object>) result.get("relatedOutOrder");
|
||||
assertEquals("CK-001", related.get("outOrderNo"));
|
||||
assertEquals("warehouse.out.detail.query", related.get("queryTool"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailDoesNotReturnSerialRowsUnlessRequested() {
|
||||
WarehouseIn order = order();
|
||||
order.setOutOrderNo(null);
|
||||
when(warehouseInMapper.selectList(any())).thenReturn(List.of(order));
|
||||
when(detailMapper.selectList(any())).thenReturn(List.of(detail("SN-1", 1)));
|
||||
when(processMapper.selectList(any())).thenReturn(List.of());
|
||||
when(picMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
Map<String, Object> result = tools.warehouseInDetailQuery(20L, null, false, null);
|
||||
|
||||
assertEquals(List.of(), result.get("serials"));
|
||||
assertEquals(1L, result.get("serialTotal"));
|
||||
assertFalse((Boolean) result.get("serialTruncated"));
|
||||
assertNull(result.get("relatedOutOrder"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void springAiPublishesBothWarehouseInToolSchemas() {
|
||||
var callbacks = org.springframework.ai.tool.method.MethodToolCallbackProvider.builder()
|
||||
.toolObjects(tools).build().getToolCallbacks();
|
||||
Map<String, String> schemas = Arrays.stream(callbacks).collect(java.util.stream.Collectors.toMap(
|
||||
callback -> callback.getToolDefinition().name(), callback -> callback.getToolDefinition().inputSchema()));
|
||||
|
||||
assertEquals(2, schemas.size());
|
||||
assertTrue(schemas.get("warehouse.in.list.query").contains("inDateFrom"));
|
||||
assertTrue(schemas.get("warehouse.in.list.query").contains("withdrawOrderNo"));
|
||||
assertTrue(schemas.get("warehouse.in.detail.query").contains("includeSerials"));
|
||||
}
|
||||
|
||||
private Map<String, Object> list(Integer page, Integer rows) {
|
||||
return tools.warehouseInListQuery(null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, 3, null, null, null,
|
||||
"2026-09-01", "2026-09-30", null, null, page, rows);
|
||||
}
|
||||
|
||||
private Map<String, Object> listWithDates(String from, String to) {
|
||||
return tools.warehouseInListQuery(null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
from, to, null, null, 1, 20);
|
||||
}
|
||||
|
||||
private static WarehouseIn order() {
|
||||
WarehouseIn order = new WarehouseIn();
|
||||
order.setId(20L);
|
||||
order.setInOrderNo("RK-001");
|
||||
order.setAllocationOrderNo("DB-001");
|
||||
order.setWarehouseTypeId(3L);
|
||||
order.setOrderState(3);
|
||||
return order;
|
||||
}
|
||||
|
||||
private static WarehouseInDetail detail(String sn, int count) {
|
||||
WarehouseInDetail detail = new WarehouseInDetail();
|
||||
detail.setId(100L + count);
|
||||
detail.setWarehouseInId(20L);
|
||||
detail.setWarehouseId(8L);
|
||||
detail.setWarehouseName("一号仓");
|
||||
detail.setMaterielDefinitionId(9L);
|
||||
detail.setMaterielIndexId(10L);
|
||||
detail.setMaterialNo("MAT-1");
|
||||
detail.setSnCode(sn);
|
||||
detail.setInCount(count);
|
||||
detail.setInConfirmUserName("李四");
|
||||
detail.setInDate(LocalDate.of(2026, 9, 15));
|
||||
detail.setInConfirmTime(new Date(0));
|
||||
return detail;
|
||||
}
|
||||
|
||||
private static WarehouseInOutPic pic(long id, int fileType) {
|
||||
WarehouseInOutPic pic = new WarehouseInOutPic();
|
||||
pic.setId(id);
|
||||
pic.setFileType(fileType);
|
||||
pic.setInOutAllocationType(0);
|
||||
pic.setFileName("file-" + id + ".jpg");
|
||||
pic.setOssfolder("warehouse/");
|
||||
return pic;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package me.iiv.agent.permission;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import me.iiv.agent.entity.m0.Agent;
|
||||
import me.iiv.agent.entity.m0.Authority;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
|
||||
import me.iiv.agent.entity.m2.WarehouseIn;
|
||||
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
import me.iiv.agent.mapper.m0.AuthorityMapper;
|
||||
import me.iiv.agent.mapper.m0.ProperytMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseAgentAuthMapper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PermissionServiceWarehouseInRowFilterTest {
|
||||
@Mock AuthorityMapper authorityMapper;
|
||||
@Mock AgentMapper agentMapper;
|
||||
@Mock WarehouseAgentAuthMapper warehouseAgentAuthMapper;
|
||||
@Mock AgentManageDistrictAuthMapper agentManageDistrictAuthMapper;
|
||||
@Mock ProperytMapper properytMapper;
|
||||
@Mock StringRedisTemplate redisTemplate;
|
||||
PermissionService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), WarehouseIn.class);
|
||||
service = new PermissionService(authorityMapper, agentMapper, warehouseAgentAuthMapper,
|
||||
agentManageDistrictAuthMapper, properytMapper, redisTemplate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void headquartersAddsNoFilter() {
|
||||
User user = user(7L, 9L, null);
|
||||
when(authorityMapper.selectById(9L)).thenReturn(authority(9L, 0));
|
||||
LambdaQueryWrapper<WarehouseIn> wrapper = new LambdaQueryWrapper<>();
|
||||
service.applyWarehouseInRowFilter(wrapper, user);
|
||||
assertTrue(wrapper.getSqlSegment().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentFilterCombinesAgentWarehouseAndParticipantVisibility() {
|
||||
User user = user(7L, 9L, 10L);
|
||||
when(authorityMapper.selectById(9L)).thenReturn(authority(9L, 1));
|
||||
Agent child = new Agent();
|
||||
child.setId(11L);
|
||||
when(agentMapper.selectList(any())).thenReturn(List.of(child));
|
||||
WarehouseAgentAuth managed = new WarehouseAgentAuth();
|
||||
managed.setManagerAgentId(12L);
|
||||
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managed));
|
||||
LambdaQueryWrapper<WarehouseIn> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyWarehouseInRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
Collection<Object> values = wrapper.getParamNameValuePairs().values();
|
||||
assertTrue(sql.contains("agent_id IN"));
|
||||
assertTrue(sql.contains("warehouse_in_details"));
|
||||
assertTrue(sql.contains("warehouse_in_audit_process"));
|
||||
assertTrue(sql.contains("OR"));
|
||||
assertTrue(values.containsAll(List.of(7L, 10L, 11L, 12L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentWithoutAgentIdFailsClosed() {
|
||||
User user = user(7L, 9L, null);
|
||||
when(authorityMapper.selectById(9L)).thenReturn(authority(9L, 1));
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> service.applyWarehouseInRowFilter(new LambdaQueryWrapper<>(), user));
|
||||
}
|
||||
|
||||
private static User user(Long id, Long authorityId, Long agentId) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setAuthorityId(authorityId);
|
||||
user.setAgentId(agentId);
|
||||
return user;
|
||||
}
|
||||
|
||||
private static Authority authority(Long id, int isAgent) {
|
||||
Authority authority = new Authority();
|
||||
authority.setId(id);
|
||||
authority.setIsagent(isAgent);
|
||||
authority.setSoftDelete(0);
|
||||
return authority;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user