feat(mcp): add warehouse outbound query tools #14
@@ -450,6 +450,75 @@
|
||||
|
||||
---
|
||||
|
||||
## 15. warehouse.out.list.query
|
||||
|
||||
分页查询出库单,参考旧系统 `POST /myWarehouseOut/allListByPage`,但修复其出库日期误映射为申请日期、
|
||||
部分渠道未过滤和逐单查询明细的问题。
|
||||
|
||||
- 分类:warehouse | 类型:读
|
||||
- 数据来源:m2 `warehouse_out`、`warehouse_out_details`、`warehouse_out_audit_process`
|
||||
- 行级权限:总部看全部;代理身份可看主单所属代理为自己/直属下级/授权代管代理的出库单,或明细涉及
|
||||
上述代理所属/负责管理仓库的出库单,或本人参与审核/处理的出库单。三类条件取并集;筛选条件只能缩小结果。
|
||||
- 列表关联数据按本页主单 ID 批量查询,不逐单 N+1 查询。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| outOrderNo / allocationOrderNo / mallOrderNo | string | 否 | 出库/调拨/商城单号,模糊匹配 |
|
||||
| deliveryOrderNo / installOrderNo / pickupOrderNo | string | 否 | 送货/装机/取货工单号,模糊匹配 |
|
||||
| agentId / warehouseId / warehouseTypeId / applyUserId | number | 否 | 所属代理/涉及仓库/出库类型/申请人 ID,精确匹配 |
|
||||
| orderState | number | 否 | 0=待审核 1=被驳回 2=待出库 3=已完成 4=已撤销 5=作废 |
|
||||
| sendMode | number | 否 | 0=快递公司 1=自行派送 |
|
||||
| outSynchronizeState | number | 否 | 0=未同步 1=已同步 2=无需同步 |
|
||||
| assetSynchronizeState | number | 否 | -1=无需同步 0=未同步 1=已同步 |
|
||||
| useCustomerType / useCustomerId | number | 否 | 客户类型(0内部/1渠道商/2直营)和客户 ID |
|
||||
| useCustomerName | string | 否 | 客户名称,模糊匹配 |
|
||||
| outDateFrom / outDateTo | string | 否 | 出库日期范围,`yyyy-MM-dd`,结束日期含当天 |
|
||||
| applyDateFrom / applyDateTo | string | 否 | 申请日期范围,`yyyy-MM-dd`,结束日期含当天 |
|
||||
| page / rows | number | 否 | 页码默认 1;每页默认 20、上限 100 |
|
||||
|
||||
**出参 records 字段**:主单 `id/outOrderNo/allocationOrderNo/mallOrderNo`、`sourceType`
|
||||
(NORMAL/ALLOCATION/MALL)、代理和出库类型、`warehouseIds/warehouseNames`、出库/商品数量、状态码与文案、
|
||||
申请人/时间、客户、`schedulePeople`、`warehouseOutPeople/warehouseOutTimes`、发货物流、同步状态、
|
||||
关联送货/装机/取货单号、备注及创建/修改时间。
|
||||
|
||||
---
|
||||
|
||||
## 16. warehouse.out.detail.query
|
||||
|
||||
按主键或出库单号查询完整出库单详情;调拨出库单可直接查询,以 `sourceType=ALLOCATION` 标识。
|
||||
|
||||
- 分类:warehouse | 类型:读
|
||||
- 行级权限与 `warehouse.out.list.query` 完全一致,并加入同一个主表查询。
|
||||
- 不存在或无权访问统一返回 `NOT_FOUND_OR_FORBIDDEN`,不泄露记录是否存在。
|
||||
|
||||
**入参**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | number | 条件必填 | 出库单主键;与 outOrderNo 至少传一个 |
|
||||
| outOrderNo | string | 条件必填 | 出库单号;与 id 至少传一个;同时传入时必须匹配同一条记录 |
|
||||
| includeSerials | boolean | 否 | 是否返回 SN 子行,默认 false |
|
||||
| serialLimit | number | 否 | SN 返回上限,默认 100、最大 500 |
|
||||
|
||||
**出参**:
|
||||
|
||||
- `record`:主单、来源类型、状态文案、客户、同步状态及关联工单号。
|
||||
- `warehouses`:物料汇总行涉及的仓库去重集合。
|
||||
- `items`:`parent_id=0` 物料汇总行,含物料、仓库、数量、价格和出库确认信息。
|
||||
- `serials/serialTotal/serialTruncated`:可选 SN 子行、完整总数和截断标记;未请求 SN 时
|
||||
`serials=[]`、`serialTruncated=false`。
|
||||
- `processes`:已处理记录(`schedule_state=2`),按操作时间、ID 正序,状态码与文案同时返回。
|
||||
- `address/logistics`:m1 收货地址及发货物流;无地址时 `address=null`。
|
||||
- `files.application/files.confirmation`:申请/确认图片,返回 `id/fileName/bucket/objectKey`;服务不持有
|
||||
OSS 签名密钥,不生成临时访问 URL。
|
||||
- `certificate`:出库凭证备注及凭证文件;均不存在时为 null。
|
||||
|
||||
详细设计与取舍见 `warehouse-out-mcp-tools-design.md`。
|
||||
|
||||
---
|
||||
|
||||
## 附录:在线查看工具清单
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# 出库单 MCP 工具设计
|
||||
|
||||
## 1. 背景与范围
|
||||
|
||||
参考旧系统 `POST /myWarehouseOut/allListByPage` 和 `GET /myWarehouseOut/lookDetail`,在
|
||||
`xiaoqu-agent-service` 新增两个只读工具:
|
||||
|
||||
| 工具 | 分类 | 用途 |
|
||||
|---|---|---|
|
||||
| `warehouse.out.list.query` | warehouse/read | 分页查询出库单 |
|
||||
| `warehouse.out.detail.query` | warehouse/read | 按主键或出库单号查询完整详情 |
|
||||
|
||||
只读取 m0/m1/m2,主要数据表为 m2 的 `warehouse_out`、`warehouse_out_details`、
|
||||
`warehouse`、`warehouse_out_audit_process`、`warehouse_in_out_pic`、
|
||||
`warehouse_out_certificate`;收货地址和物流信息按需读取 m1。
|
||||
|
||||
本次不返回关联入库单详情,不修改旧模块和业务表,不新增 Flyway migration。新工具由现有
|
||||
`ToolSyncService` 自动登记,初始不属于任何 Scope,发布后需在管理后台授权。
|
||||
|
||||
## 2. 列表工具
|
||||
|
||||
### 2.1 入参
|
||||
|
||||
- 单号模糊查询:`outOrderNo`、`allocationOrderNo`、`mallOrderNo`、`deliveryOrderNo`、
|
||||
`installOrderNo`、`pickupOrderNo`。
|
||||
- 归属精确查询:`agentId`、`warehouseId`、`warehouseTypeId`、`applyUserId`。
|
||||
- 状态精确查询:`orderState`、`sendMode`、`outSynchronizeState`、`assetSynchronizeState`。
|
||||
- 客户查询:`useCustomerType`、`useCustomerId` 精确查询,`useCustomerName` 模糊查询。
|
||||
- 日期:`outDateFrom/outDateTo` 过滤 `out_date`;`applyDateFrom/applyDateTo` 过滤
|
||||
`apply_date`。日期格式固定为 `yyyy-MM-dd`,业务时区为 `Asia/Shanghai`,结束日期包含当天。
|
||||
- 分页:`page` 默认 1;`rows` 默认 20、上限 100,超出上限截断并返回 `notice`。
|
||||
|
||||
旧接口错误地把出库日期条件改写为申请日期条件,本工具不复制该行为。
|
||||
|
||||
### 2.2 出参
|
||||
|
||||
顶层统一返回 `total/page/rows/records`。每条记录包含主单号、来源类型、代理商、出库类型、
|
||||
仓库集合、数量、状态及状态文案、申请人及申请时间、客户、待办人、出库确认人及日期、物流、
|
||||
同步状态、关联工单号和备注。
|
||||
|
||||
`sourceType`:有调拨单号为 `ALLOCATION`,否则有商城订单号为 `MALL`,其余为 `NORMAL`。
|
||||
|
||||
`orderState`:0=待审核、1=被驳回、2=待出库、3=已完成、4=已撤销、5=作废。
|
||||
|
||||
列表页的仓库、待办人、出库人均按本页主单 ID 批量查询和内存聚合,禁止逐单 N+1 查询。
|
||||
|
||||
## 3. 详情工具
|
||||
|
||||
### 3.1 入参
|
||||
|
||||
- `id`、`outOrderNo` 至少传一个;同时传入时必须指向同一条可见记录。
|
||||
- `includeSerials` 默认 `false`。
|
||||
- `serialLimit` 默认 100、最大 500;只限制返回数量,`serialTotal` 始终返回完整数量。
|
||||
|
||||
### 3.2 出参
|
||||
|
||||
- `record`:主单字段、来源、状态文案、客户、同步状态和关联工单号。
|
||||
- `warehouses`:出库明细涉及的仓库 ID/名称去重集合。
|
||||
- `items`:`parent_id=0` 的物料汇总行,含物料编号、名称、分类、规格、仓库、数量、单价、
|
||||
总额、出库人及时间。
|
||||
- `serials`:仅 `includeSerials=true` 返回 SN 子行;另返回 `serialTotal/serialTruncated`。
|
||||
- `processes`:`schedule_state=2` 的已处理记录,按操作时间、ID 正序,状态码与文案同时返回。
|
||||
- `address`:商城订单地址或主单地址。
|
||||
- `logistics`:发货方式、快递公司、运单号、配送人和收货信息。
|
||||
- `files.application/files.confirmation`:申请图片和确认出库图片。服务不持有 OSS 签名密钥,
|
||||
返回文件 ID、文件名及存储对象定位信息,不生成临时访问 URL。
|
||||
- `certificate`:凭证备注与 `file_type=4/in_out_allocation_type=99` 的凭证文件。
|
||||
|
||||
调拨出库单允许直接查询,并以 `sourceType=ALLOCATION` 标识。
|
||||
|
||||
## 4. 行级权限
|
||||
|
||||
列表和详情使用完全相同的主查询权限条件,业务筛选只能缩小结果:
|
||||
|
||||
- 总部身份(权限组 `isagent=0`):不过滤。
|
||||
- 代理商身份(权限组 `isagent=1`):可见范围为以下条件的并集:
|
||||
- 主单 `agent_id` 属于自己、直属下级或 `warehouse_agent_auth` 授权代管代理;
|
||||
- 出库明细涉及的仓库,其 `warehouse.agent_id` 或 `warehouse.manager_agent_id` 属于上述代理范围;
|
||||
- 当前用户存在该主单的未删除审核/处理记录。
|
||||
- 代理商用户未关联代理商、权限组不存在或配置异常时失败关闭。
|
||||
|
||||
详情不存在与无权访问统一抛出 `NOT_FOUND_OR_FORBIDDEN`,防止通过 ID/单号探测数据。
|
||||
|
||||
权限通过主表查询中的关联 `EXISTS` 条件完成,不先查询大量 ID 再拼接 `IN`。
|
||||
|
||||
## 5. 测试与验收
|
||||
|
||||
- 列表:分页上限、各类日期/状态/仓库条件、批量聚合和状态文案。
|
||||
- 详情:ID/单号参数校验、双条件一致性、调拨单、SN 默认关闭与截断、附件/凭证分组。
|
||||
- 权限:总部、代理范围、负责管理仓库、处理人、无代理失败关闭;详情查询必须复用相同过滤器。
|
||||
- 构建:固定 `TZ=Asia/Shanghai` 执行后端测试;不连接生产数据库、不执行部署。
|
||||
@@ -0,0 +1,27 @@
|
||||
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_in_out_pic")
|
||||
public class WarehouseInOutPic implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long aggregateId;
|
||||
private String fileName;
|
||||
private String ossbucketname;
|
||||
private String ossfolder;
|
||||
private Integer fileType;
|
||||
private Integer inOutAllocationType;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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_out")
|
||||
public class WarehouseOut 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 outOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long agentId;
|
||||
private String agentName;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private LocalDate outDate;
|
||||
private Integer goodsCount;
|
||||
private Integer outCount;
|
||||
private Integer orderState;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyDate;
|
||||
private String memo;
|
||||
private Integer auditLevel;
|
||||
private Integer currendAuditLevel;
|
||||
private Integer useCustomerType;
|
||||
private Long useCustomerId;
|
||||
private String useCustomerName;
|
||||
private Integer outSynchronizeState;
|
||||
private Date outSynchronizeTime;
|
||||
private String removeUserName;
|
||||
private Date removeDate;
|
||||
private Integer sendMode;
|
||||
private Long logisticsId;
|
||||
private String logisticsName;
|
||||
private String logisticsOrderNo;
|
||||
private String logisticsWayBill;
|
||||
private Long addressId;
|
||||
private Integer assetSynchronizeState;
|
||||
private String deliveryOrderNo;
|
||||
private String installOrderNo;
|
||||
private String pickupOrderNo;
|
||||
}
|
||||
+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_out_audit_process")
|
||||
public class WarehouseOutAuditProcess implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Integer softDelete;
|
||||
private Long warehouseOutId;
|
||||
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;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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_out_certificate")
|
||||
public class WarehouseOutCertificate implements Serializable {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long outOrderId;
|
||||
private String outOrderNo;
|
||||
private String memo;
|
||||
private Date createTime;
|
||||
private Date modifyTime;
|
||||
private Integer softDelete;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 出库单物料/SN 明细(m2,只读)。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@TableName("warehouse_out_details")
|
||||
public class WarehouseOutDetail 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 Long warehouseId;
|
||||
private String warehouseName;
|
||||
private Integer outCount;
|
||||
private String outOrderNo;
|
||||
private String allocationOrderNo;
|
||||
private String mallOrderNo;
|
||||
private Long warehouseOutId;
|
||||
private Long materielDefinitionId;
|
||||
private String materialNo;
|
||||
private String materielFullName;
|
||||
private String materielSort;
|
||||
private Long materielIndexId;
|
||||
private String specifications;
|
||||
private String snCode;
|
||||
private LocalDate outDate;
|
||||
private Long warehouseTypeId;
|
||||
private String warehouseTypeName;
|
||||
private Integer orderState;
|
||||
private Integer queryFlag;
|
||||
private Long parentId;
|
||||
private Date outConfirmTime;
|
||||
private String outConfirmUserName;
|
||||
private BigDecimal price;
|
||||
private BigDecimal allMoney;
|
||||
private String threeAttribute;
|
||||
private String assetDisposalType;
|
||||
private Integer assetSynchronizeState;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseInOutPic;
|
||||
|
||||
public interface WarehouseInOutPicMapper extends BaseMapper<WarehouseInOutPic> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutAuditProcess;
|
||||
|
||||
public interface WarehouseOutAuditProcessMapper extends BaseMapper<WarehouseOutAuditProcess> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutCertificate;
|
||||
|
||||
public interface WarehouseOutCertificateMapper extends BaseMapper<WarehouseOutCertificate> {
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutDetail;
|
||||
|
||||
public interface WarehouseOutDetailMapper extends BaseMapper<WarehouseOutDetail> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WarehouseOut;
|
||||
|
||||
public interface WarehouseOutMapper extends BaseMapper<WarehouseOut> {
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.MallGoodsOrder;
|
||||
import me.iiv.agent.entity.m1.MallOrderLogistics;
|
||||
import me.iiv.agent.entity.m1.MallShippingAddress;
|
||||
import me.iiv.agent.entity.m2.WarehouseInOutPic;
|
||||
import me.iiv.agent.entity.m2.WarehouseOut;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutAuditProcess;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutCertificate;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutDetail;
|
||||
import me.iiv.agent.mapper.m1.MallGoodsOrderMapper;
|
||||
import me.iiv.agent.mapper.m1.MallOrderLogisticsMapper;
|
||||
import me.iiv.agent.mapper.m1.MallShippingAddressMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutAuditProcessMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutCertificateMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutDetailMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutMapper;
|
||||
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;
|
||||
|
||||
/** 出库单查询工具,参考老系统 /myWarehouseOut/allListByPage 与 /lookDetail。 */
|
||||
@Component
|
||||
public class WarehouseOutTools 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 OUT_TYPE = 1;
|
||||
private static final int CERT_FILE_TYPE = 4;
|
||||
private static final int CERT_TYPE = 99;
|
||||
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> CUSTOMER_TYPE_TEXT = Map.of(
|
||||
0, "内部人员", 1, "渠道商", 2, "直营客户");
|
||||
private static final Map<Integer, String> SEND_MODE_TEXT = Map.of(0, "快递公司", 1, "自行派送");
|
||||
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 WarehouseOutMapper warehouseOutMapper;
|
||||
final WarehouseOutDetailMapper detailMapper;
|
||||
final WarehouseOutAuditProcessMapper processMapper;
|
||||
final WarehouseInOutPicMapper picMapper;
|
||||
final WarehouseOutCertificateMapper certificateMapper;
|
||||
final MallGoodsOrderMapper mallGoodsOrderMapper;
|
||||
final MallShippingAddressMapper shippingAddressMapper;
|
||||
final MallOrderLogisticsMapper logisticsMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public WarehouseOutTools(WarehouseOutMapper warehouseOutMapper,
|
||||
WarehouseOutDetailMapper detailMapper,
|
||||
WarehouseOutAuditProcessMapper processMapper,
|
||||
WarehouseInOutPicMapper picMapper,
|
||||
WarehouseOutCertificateMapper certificateMapper,
|
||||
MallGoodsOrderMapper mallGoodsOrderMapper,
|
||||
MallShippingAddressMapper shippingAddressMapper,
|
||||
MallOrderLogisticsMapper logisticsMapper,
|
||||
PermissionService permissionService) {
|
||||
this.warehouseOutMapper = warehouseOutMapper;
|
||||
this.detailMapper = detailMapper;
|
||||
this.processMapper = processMapper;
|
||||
this.picMapper = picMapper;
|
||||
this.certificateMapper = certificateMapper;
|
||||
this.mallGoodsOrderMapper = mallGoodsOrderMapper;
|
||||
this.shippingAddressMapper = shippingAddressMapper;
|
||||
this.logisticsMapper = logisticsMapper;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@XqTool(category = "warehouse", kind = "read")
|
||||
@Tool(name = "warehouse.out.list.query", description = "分页查询出库单,参考老系统 /myWarehouseOut/allListByPage。"
|
||||
+ "数据来自 m2 warehouse_out 及关联明细/流程。总部可看全部;代理身份可看所属代理范围、所属或负责仓库涉及、或本人参与处理的出库单。"
|
||||
+ "出库日期与申请日期分别过滤;日期格式 yyyy-MM-dd,结束日期含当天。分页 page 默认1,rows 默认20、上限100。"
|
||||
+ "orderState:0待审核、1被驳回、2待出库、3已完成、4已撤销、5作废。")
|
||||
public Map<String, Object> warehouseOutListQuery(
|
||||
@ToolParam(required = false, description = "出库单号,模糊匹配") String outOrderNo,
|
||||
@ToolParam(required = false, description = "调拨单号,模糊匹配") String allocationOrderNo,
|
||||
@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 = "所属代理商 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自行派送") Integer sendMode,
|
||||
@ToolParam(required = false, description = "出库同步状态:0未同步 1已同步 2无需同步") Integer outSynchronizeState,
|
||||
@ToolParam(required = false, description = "资产同步状态:-1无需同步 0未同步 1已同步") Integer assetSynchronizeState,
|
||||
@ToolParam(required = false, description = "客户类型:0内部人员 1渠道商 2直营客户") Integer useCustomerType,
|
||||
@ToolParam(required = false, description = "客户 id") Long useCustomerId,
|
||||
@ToolParam(required = false, description = "客户名称,模糊匹配") String useCustomerName,
|
||||
@ToolParam(required = false, description = "出库日期起,yyyy-MM-dd") String outDateFrom,
|
||||
@ToolParam(required = false, description = "出库日期止,yyyy-MM-dd,含当天") String outDateTo,
|
||||
@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 outStart = parseLocalDate(outDateFrom, "outDateFrom");
|
||||
LocalDate outEnd = parseLocalDate(outDateTo, "outDateTo");
|
||||
LocalDate applyStartDate = parseLocalDate(applyDateFrom, "applyDateFrom");
|
||||
LocalDate applyEndDate = parseLocalDate(applyDateTo, "applyDateTo");
|
||||
Date applyStart = startOfDay(applyStartDate);
|
||||
Date applyEndExclusive = nextDayStart(applyEndDate);
|
||||
validateRange(outStart, outEnd, "outDateFrom", "outDateTo");
|
||||
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<WarehouseOut> wrapper = baseWrapper(outOrderNo, allocationOrderNo, mallOrderNo,
|
||||
deliveryOrderNo, installOrderNo, pickupOrderNo, agentId, warehouseId, warehouseTypeId,
|
||||
applyUserId, orderState, sendMode, outSynchronizeState, assetSynchronizeState,
|
||||
useCustomerType, useCustomerId, useCustomerName, outStart, outEnd, applyStart, applyEndExclusive);
|
||||
User user = AuthContextHolder.get().getUser();
|
||||
permissionService.applyWarehouseOutRowFilter(wrapper, user);
|
||||
|
||||
Long total = warehouseOutMapper.selectCount(wrapper);
|
||||
wrapper.orderByDesc(WarehouseOut::getId)
|
||||
.last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize);
|
||||
List<WarehouseOut> orders = warehouseOutMapper.selectList(wrapper);
|
||||
Map<Long, List<WarehouseOutDetail>> details = listDetails(orders);
|
||||
Map<Long, List<WarehouseOutAuditProcess>> 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.out.detail.query", description = "按 id 或出库单号查询出库单详情。"
|
||||
+ "行级权限与 warehouse.out.list.query 完全一致,不存在与无权访问统一返回 NOT_FOUND_OR_FORBIDDEN。"
|
||||
+ "返回主单、仓库、物料、可选 SN、已处理流程、地址、物流、申请/确认附件和出库凭证;调拨出库单可直接查询。"
|
||||
+ "includeSerials 默认 false;serialLimit 默认100、上限500。")
|
||||
public Map<String, Object> warehouseOutDetailQuery(
|
||||
@ToolParam(required = false, description = "出库单主键 id,与 outOrderNo 至少传一个") Long id,
|
||||
@ToolParam(required = false, description = "出库单号,与 id 至少传一个") String outOrderNo,
|
||||
@ToolParam(required = false, description = "是否返回 SN 子明细,默认 false") Boolean includeSerials,
|
||||
@ToolParam(required = false, description = "SN 返回上限,默认100、最大500") Integer serialLimit) {
|
||||
if ((id == null || id <= 0) && !StringUtils.hasText(outOrderNo)) {
|
||||
throw new IllegalArgumentException("id、outOrderNo 至少传一个");
|
||||
}
|
||||
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<WarehouseOut> wrapper = new LambdaQueryWrapper<WarehouseOut>()
|
||||
.eq(WarehouseOut::getSoftDelete, 0);
|
||||
if (id != null) {
|
||||
wrapper.eq(WarehouseOut::getId, id);
|
||||
}
|
||||
if (StringUtils.hasText(outOrderNo)) {
|
||||
wrapper.eq(WarehouseOut::getOutOrderNo, outOrderNo.trim());
|
||||
}
|
||||
permissionService.applyWarehouseOutRowFilter(wrapper, AuthContextHolder.get().getUser());
|
||||
List<WarehouseOut> visible = warehouseOutMapper.selectList(wrapper.orderByDesc(WarehouseOut::getId).last("LIMIT 1"));
|
||||
if (visible.isEmpty()) {
|
||||
throw new ToolCallException(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, "出库单不存在或当前身份无权查看");
|
||||
}
|
||||
WarehouseOut order = visible.get(0);
|
||||
|
||||
List<WarehouseOutDetail> items = detailMapper.selectList(new LambdaQueryWrapper<WarehouseOutDetail>()
|
||||
.eq(WarehouseOutDetail::getSoftDelete, 0)
|
||||
.eq(WarehouseOutDetail::getWarehouseOutId, order.getId())
|
||||
.eq(WarehouseOutDetail::getParentId, 0L)
|
||||
.orderByAsc(WarehouseOutDetail::getId));
|
||||
LambdaQueryWrapper<WarehouseOutDetail> serialBase = new LambdaQueryWrapper<WarehouseOutDetail>()
|
||||
.eq(WarehouseOutDetail::getSoftDelete, 0)
|
||||
.eq(WarehouseOutDetail::getWarehouseOutId, order.getId())
|
||||
.ne(WarehouseOutDetail::getParentId, 0L);
|
||||
long serialTotal = detailMapper.selectCount(serialBase);
|
||||
List<WarehouseOutDetail> serialRows = include
|
||||
? detailMapper.selectList(serialBase.orderByAsc(WarehouseOutDetail::getId).last("LIMIT " + limit))
|
||||
: List.of();
|
||||
|
||||
List<WarehouseOutAuditProcess> processes = processMapper.selectList(
|
||||
new LambdaQueryWrapper<WarehouseOutAuditProcess>()
|
||||
.eq(WarehouseOutAuditProcess::getSoftDelete, 0)
|
||||
.eq(WarehouseOutAuditProcess::getWarehouseOutId, order.getId())
|
||||
.eq(WarehouseOutAuditProcess::getScheduleState, 2)
|
||||
.orderByAsc(WarehouseOutAuditProcess::getOperationTime)
|
||||
.orderByAsc(WarehouseOutAuditProcess::getId));
|
||||
List<WarehouseInOutPic> pics = picMapper.selectList(new LambdaQueryWrapper<WarehouseInOutPic>()
|
||||
.eq(WarehouseInOutPic::getSoftDelete, 0)
|
||||
.eq(WarehouseInOutPic::getAggregateId, order.getId())
|
||||
.orderByAsc(WarehouseInOutPic::getId));
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("record", detailRecord(order));
|
||||
result.put("warehouses", warehouseRecords(items));
|
||||
result.put("items", items.stream().map(this::itemRecord).collect(Collectors.toList()));
|
||||
result.put("serials", serialRows.stream().map(this::serialRecord).collect(Collectors.toList()));
|
||||
result.put("serialTotal", serialTotal);
|
||||
result.put("serialTruncated", include && serialTotal > serialRows.size());
|
||||
result.put("processes", processes.stream().map(this::processRecord).collect(Collectors.toList()));
|
||||
result.put("address", addressRecord(order));
|
||||
result.put("logistics", logisticsRecord(order));
|
||||
Map<String, Object> files = new LinkedHashMap<>();
|
||||
files.put("application", fileRecords(pics, 0, OUT_TYPE));
|
||||
files.put("confirmation", fileRecords(pics, 2, OUT_TYPE));
|
||||
result.put("files", files);
|
||||
result.put("certificate", certificateRecord(order, pics));
|
||||
return result;
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<WarehouseOut> baseWrapper(
|
||||
String outOrderNo, String allocationOrderNo, String mallOrderNo,
|
||||
String deliveryOrderNo, String installOrderNo, String pickupOrderNo,
|
||||
Long agentId, Long warehouseId, Long warehouseTypeId, Long applyUserId,
|
||||
Integer orderState, Integer sendMode, Integer outSynchronizeState, Integer assetSynchronizeState,
|
||||
Integer useCustomerType, Long useCustomerId, String useCustomerName,
|
||||
LocalDate outStart, LocalDate outEnd, Date applyStart, Date applyEndExclusive) {
|
||||
LambdaQueryWrapper<WarehouseOut> wrapper = new LambdaQueryWrapper<WarehouseOut>()
|
||||
.eq(WarehouseOut::getSoftDelete, 0);
|
||||
like(wrapper, WarehouseOut::getOutOrderNo, outOrderNo);
|
||||
like(wrapper, WarehouseOut::getAllocationOrderNo, allocationOrderNo);
|
||||
like(wrapper, WarehouseOut::getMallOrderNo, mallOrderNo);
|
||||
like(wrapper, WarehouseOut::getDeliveryOrderNo, deliveryOrderNo);
|
||||
like(wrapper, WarehouseOut::getInstallOrderNo, installOrderNo);
|
||||
like(wrapper, WarehouseOut::getPickupOrderNo, pickupOrderNo);
|
||||
wrapper.eq(agentId != null, WarehouseOut::getAgentId, agentId)
|
||||
.eq(warehouseTypeId != null, WarehouseOut::getWarehouseTypeId, warehouseTypeId)
|
||||
.eq(applyUserId != null, WarehouseOut::getApplyUserId, applyUserId)
|
||||
.eq(orderState != null, WarehouseOut::getOrderState, orderState)
|
||||
.eq(sendMode != null, WarehouseOut::getSendMode, sendMode)
|
||||
.eq(outSynchronizeState != null, WarehouseOut::getOutSynchronizeState, outSynchronizeState)
|
||||
.eq(assetSynchronizeState != null, WarehouseOut::getAssetSynchronizeState, assetSynchronizeState)
|
||||
.eq(useCustomerType != null, WarehouseOut::getUseCustomerType, useCustomerType)
|
||||
.eq(useCustomerId != null, WarehouseOut::getUseCustomerId, useCustomerId)
|
||||
.like(StringUtils.hasText(useCustomerName), WarehouseOut::getUseCustomerName,
|
||||
StringUtils.hasText(useCustomerName) ? useCustomerName.trim() : useCustomerName)
|
||||
.ge(outStart != null, WarehouseOut::getOutDate, outStart)
|
||||
.le(outEnd != null, WarehouseOut::getOutDate, outEnd)
|
||||
.ge(applyStart != null, WarehouseOut::getApplyDate, applyStart)
|
||||
.lt(applyEndExclusive != null, WarehouseOut::getApplyDate, applyEndExclusive);
|
||||
if (warehouseId != null) {
|
||||
wrapper.apply("EXISTS (SELECT 1 FROM warehouse_out_details wod_filter "
|
||||
+ "WHERE wod_filter.warehouse_out_id = warehouse_out.id "
|
||||
+ "AND wod_filter.soft_delete = 0 AND wod_filter.warehouse_id = {0})", warehouseId);
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private static void like(LambdaQueryWrapper<WarehouseOut> wrapper,
|
||||
com.baomidou.mybatisplus.core.toolkit.support.SFunction<WarehouseOut, ?> column,
|
||||
String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
wrapper.like(column, value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, List<WarehouseOutDetail>> listDetails(List<WarehouseOut> orders) {
|
||||
if (orders.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Long> ids = orders.stream().map(WarehouseOut::getId).collect(Collectors.toList());
|
||||
return detailMapper.selectList(new LambdaQueryWrapper<WarehouseOutDetail>()
|
||||
.eq(WarehouseOutDetail::getSoftDelete, 0)
|
||||
.in(WarehouseOutDetail::getWarehouseOutId, ids)
|
||||
.eq(WarehouseOutDetail::getParentId, 0L)
|
||||
.orderByAsc(WarehouseOutDetail::getId)).stream()
|
||||
.collect(Collectors.groupingBy(WarehouseOutDetail::getWarehouseOutId,
|
||||
LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
private Map<Long, List<WarehouseOutAuditProcess>> listSchedules(List<WarehouseOut> orders) {
|
||||
if (orders.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<Long> ids = orders.stream().map(WarehouseOut::getId).collect(Collectors.toList());
|
||||
return processMapper.selectList(new LambdaQueryWrapper<WarehouseOutAuditProcess>()
|
||||
.eq(WarehouseOutAuditProcess::getSoftDelete, 0)
|
||||
.in(WarehouseOutAuditProcess::getWarehouseOutId, ids)
|
||||
.eq(WarehouseOutAuditProcess::getScheduleState, 1)
|
||||
.orderByAsc(WarehouseOutAuditProcess::getId)).stream()
|
||||
.collect(Collectors.groupingBy(WarehouseOutAuditProcess::getWarehouseOutId,
|
||||
LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
private Map<String, Object> listRecord(WarehouseOut order, List<WarehouseOutDetail> details,
|
||||
List<WarehouseOutAuditProcess> schedules) {
|
||||
Map<String, Object> record = detailRecord(order);
|
||||
LinkedHashMap<Long, String> warehouses = new LinkedHashMap<>();
|
||||
for (WarehouseOutDetail detail : details) {
|
||||
if (detail.getWarehouseId() != null) {
|
||||
warehouses.putIfAbsent(detail.getWarehouseId(), detail.getWarehouseName());
|
||||
}
|
||||
}
|
||||
record.put("warehouseIds", new ArrayList<>(warehouses.keySet()));
|
||||
record.put("warehouseNames", new ArrayList<>(warehouses.values()));
|
||||
record.put("schedulePeople", distinctNonBlank(schedules.stream()
|
||||
.map(WarehouseOutAuditProcess::getUserName).collect(Collectors.toList())));
|
||||
record.put("warehouseOutPeople", distinctNonBlank(details.stream()
|
||||
.map(WarehouseOutDetail::getOutConfirmUserName).collect(Collectors.toList())));
|
||||
record.put("warehouseOutTimes", details.stream().map(WarehouseOutDetail::getOutDate)
|
||||
.filter(Objects::nonNull).map(LocalDate::toString).distinct().collect(Collectors.toList()));
|
||||
return record;
|
||||
}
|
||||
|
||||
private Map<String, Object> detailRecord(WarehouseOut order) {
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("id", order.getId());
|
||||
record.put("outOrderNo", order.getOutOrderNo());
|
||||
record.put("allocationOrderNo", order.getAllocationOrderNo());
|
||||
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("outDate", order.getOutDate() == null ? null : order.getOutDate().toString());
|
||||
record.put("goodsCount", order.getGoodsCount());
|
||||
record.put("outCount", order.getOutCount());
|
||||
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("useCustomerType", order.getUseCustomerType());
|
||||
record.put("useCustomerTypeText", text(CUSTOMER_TYPE_TEXT, order.getUseCustomerType()));
|
||||
record.put("useCustomerId", order.getUseCustomerId());
|
||||
record.put("useCustomerName", order.getUseCustomerName());
|
||||
record.put("sendMode", order.getSendMode());
|
||||
record.put("sendModeText", text(SEND_MODE_TEXT, order.getSendMode()));
|
||||
record.put("logisticsId", order.getLogisticsId());
|
||||
record.put("logisticsName", order.getLogisticsName());
|
||||
record.put("logisticsOrderNo", order.getLogisticsOrderNo());
|
||||
record.put("logisticsWayBill", order.getLogisticsWayBill());
|
||||
record.put("outSynchronizeState", order.getOutSynchronizeState());
|
||||
record.put("outSynchronizeTime", formatTime(order.getOutSynchronizeTime()));
|
||||
record.put("assetSynchronizeState", order.getAssetSynchronizeState());
|
||||
record.put("deliveryOrderNo", order.getDeliveryOrderNo());
|
||||
record.put("installOrderNo", order.getInstallOrderNo());
|
||||
record.put("pickupOrderNo", order.getPickupOrderNo());
|
||||
record.put("memo", order.getMemo());
|
||||
record.put("createTime", formatTime(order.getCreateTime()));
|
||||
record.put("modifyTime", formatTime(order.getModifyTime()));
|
||||
return record;
|
||||
}
|
||||
|
||||
private Map<String, Object> itemRecord(WarehouseOutDetail detail) {
|
||||
Map<String, Object> item = commonDetailRecord(detail);
|
||||
item.put("outCount", detail.getOutCount());
|
||||
item.put("price", detail.getPrice());
|
||||
item.put("allMoney", detail.getAllMoney());
|
||||
item.put("outConfirmUserName", detail.getOutConfirmUserName());
|
||||
item.put("outConfirmTime", formatTime(detail.getOutConfirmTime()));
|
||||
item.put("outDate", detail.getOutDate() == null ? null : detail.getOutDate().toString());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> serialRecord(WarehouseOutDetail detail) {
|
||||
Map<String, Object> item = commonDetailRecord(detail);
|
||||
item.put("parentId", detail.getParentId());
|
||||
item.put("snCode", detail.getSnCode());
|
||||
item.put("assetDisposalType", detail.getAssetDisposalType());
|
||||
item.put("assetSynchronizeState", detail.getAssetSynchronizeState());
|
||||
return item;
|
||||
}
|
||||
|
||||
private Map<String, Object> commonDetailRecord(WarehouseOutDetail detail) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("id", detail.getId());
|
||||
item.put("materielDefinitionId", detail.getMaterielDefinitionId());
|
||||
item.put("materialNo", detail.getMaterialNo());
|
||||
item.put("materielFullName", detail.getMaterielFullName());
|
||||
item.put("materielSort", detail.getMaterielSort());
|
||||
item.put("specifications", detail.getSpecifications());
|
||||
item.put("threeAttribute", detail.getThreeAttribute());
|
||||
item.put("warehouseId", detail.getWarehouseId());
|
||||
item.put("warehouseName", detail.getWarehouseName());
|
||||
return item;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> warehouseRecords(List<WarehouseOutDetail> items) {
|
||||
LinkedHashMap<Long, String> warehouses = new LinkedHashMap<>();
|
||||
for (WarehouseOutDetail item : items) {
|
||||
if (item.getWarehouseId() != null) {
|
||||
warehouses.putIfAbsent(item.getWarehouseId(), item.getWarehouseName());
|
||||
}
|
||||
}
|
||||
return warehouses.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 Map<String, Object> processRecord(WarehouseOutAuditProcess 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 Map<String, Object> addressRecord(WarehouseOut order) {
|
||||
Long addressId = order.getAddressId();
|
||||
if (StringUtils.hasText(order.getMallOrderNo())) {
|
||||
List<MallGoodsOrder> mallOrders = mallGoodsOrderMapper.selectList(new LambdaQueryWrapper<MallGoodsOrder>()
|
||||
.eq(MallGoodsOrder::getSoftDelete, 0)
|
||||
.eq(MallGoodsOrder::getOrderNo, order.getMallOrderNo())
|
||||
.orderByDesc(MallGoodsOrder::getId).last("LIMIT 1"));
|
||||
if (!mallOrders.isEmpty()) {
|
||||
addressId = mallOrders.get(0).getAddressId();
|
||||
}
|
||||
}
|
||||
if (addressId == null || addressId == 0) {
|
||||
return null;
|
||||
}
|
||||
MallShippingAddress address = shippingAddressMapper.selectById(addressId);
|
||||
if (address == null || !Integer.valueOf(0).equals(address.getSoftDelete())) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("id", address.getId());
|
||||
record.put("reveiveName", address.getReveiveName());
|
||||
record.put("reveiveNumber", address.getReveiveNumber());
|
||||
record.put("province", address.getProvince());
|
||||
record.put("city", address.getCity());
|
||||
record.put("countyseat", address.getCountyseat());
|
||||
record.put("address", address.getAddress());
|
||||
record.put("fullAddress", joinAddress(address));
|
||||
record.put("memo", address.getMemo());
|
||||
return record;
|
||||
}
|
||||
|
||||
private Map<String, Object> logisticsRecord(WarehouseOut order) {
|
||||
String businessNo = StringUtils.hasText(order.getMallOrderNo())
|
||||
? order.getMallOrderNo() : order.getOutOrderNo();
|
||||
List<MallOrderLogistics> rows = logisticsMapper.selectList(new LambdaQueryWrapper<MallOrderLogistics>()
|
||||
.eq(MallOrderLogistics::getSoftDelete, 0)
|
||||
.eq(MallOrderLogistics::getOrderNo, businessNo)
|
||||
.orderByDesc(MallOrderLogistics::getId).last("LIMIT 1"));
|
||||
MallOrderLogistics logistics = rows.isEmpty() ? null : rows.get(0);
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("businessNo", businessNo);
|
||||
record.put("sendMode", order.getSendMode());
|
||||
record.put("sendModeText", text(SEND_MODE_TEXT, order.getSendMode()));
|
||||
record.put("logisticsId", order.getLogisticsId());
|
||||
record.put("logisticsName", order.getLogisticsName());
|
||||
record.put("logisticsOrderNo", order.getLogisticsOrderNo());
|
||||
record.put("logisticsWayBill", order.getLogisticsWayBill());
|
||||
if (logistics != null) {
|
||||
record.put("state", logistics.getState());
|
||||
record.put("deliverName", logistics.getDeliverName());
|
||||
record.put("deliverNumber", logistics.getDeliverNumber());
|
||||
record.put("sendTime", formatTime(logistics.getModifyTime()));
|
||||
record.put("receivingGoodsName", logistics.getReceivingGoodsName());
|
||||
record.put("reveiveName", logistics.getReveiveName());
|
||||
record.put("reveiveNumber", logistics.getReveiveNumber());
|
||||
record.put("address", logistics.getAddress());
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
private Map<String, Object> certificateRecord(WarehouseOut order, List<WarehouseInOutPic> pics) {
|
||||
List<WarehouseOutCertificate> rows = certificateMapper.selectList(
|
||||
new LambdaQueryWrapper<WarehouseOutCertificate>()
|
||||
.eq(WarehouseOutCertificate::getSoftDelete, 0)
|
||||
.eq(WarehouseOutCertificate::getOutOrderId, order.getId())
|
||||
.orderByDesc(WarehouseOutCertificate::getId).last("LIMIT 1"));
|
||||
List<Map<String, Object>> certFiles = fileRecords(pics, CERT_FILE_TYPE, CERT_TYPE);
|
||||
if (rows.isEmpty() && certFiles.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
WarehouseOutCertificate certificate = rows.isEmpty() ? null : rows.get(0);
|
||||
record.put("id", certificate == null ? null : certificate.getId());
|
||||
record.put("memo", certificate == null ? null : certificate.getMemo());
|
||||
record.put("createTime", certificate == null ? null : formatTime(certificate.getCreateTime()));
|
||||
record.put("modifyTime", certificate == null ? null : formatTime(certificate.getModifyTime()));
|
||||
record.put("files", certFiles);
|
||||
return record;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> fileRecords(List<WarehouseInOutPic> pics, int fileType, int aggregateType) {
|
||||
return pics.stream().filter(pic -> Integer.valueOf(fileType).equals(pic.getFileType())
|
||||
&& Integer.valueOf(aggregateType).equals(pic.getInOutAllocationType()))
|
||||
.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 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(WarehouseOut order) {
|
||||
if (StringUtils.hasText(order.getAllocationOrderNo())) {
|
||||
return "ALLOCATION";
|
||||
}
|
||||
return StringUtils.hasText(order.getMallOrderNo()) ? "MALL" : "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 joinAddress(MallShippingAddress address) {
|
||||
return nullToEmpty(address.getProvince()) + nullToEmpty(address.getCity())
|
||||
+ nullToEmpty(address.getCountyseat()) + nullToEmpty(address.getAddress());
|
||||
}
|
||||
|
||||
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.WarehouseOut;
|
||||
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
|
||||
import me.iiv.agent.mapper.m0.AgentMapper;
|
||||
import me.iiv.agent.mapper.m0.AuthorityMapper;
|
||||
@@ -25,6 +26,7 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -121,6 +123,48 @@ public class PermissionService {
|
||||
return new ArrayList<>(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库单权限:总部看全部;代理身份可看所属代理范围内的主单、其代理范围所属/负责仓库
|
||||
* 涉及的主单,以及本人参与审批或处理的主单。关联范围使用 EXISTS 落在同一个主查询中,
|
||||
* 列表和详情必须复用本方法,避免先取主键集合及详情越权。
|
||||
*/
|
||||
public void applyWarehouseOutRowFilter(LambdaQueryWrapper<WarehouseOut> wrapper, User user) {
|
||||
List<Long> agentIds = visibleAgentIds(user);
|
||||
if (agentIds == null) {
|
||||
return;
|
||||
}
|
||||
if (agentIds.isEmpty()) {
|
||||
wrapper.eq(WarehouseOut::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_out_details wod "
|
||||
+ "JOIN warehouse w ON w.id = wod.warehouse_id AND w.soft_delete = 0 "
|
||||
+ "WHERE wod.warehouse_out_id = warehouse_out.id AND wod.soft_delete = 0 "
|
||||
+ "AND (w.agent_id IN (" + firstIds + ") OR w.manager_agent_id IN (" + secondIds + ")))";
|
||||
String participantExists = "EXISTS (SELECT 1 FROM warehouse_out_audit_process woap "
|
||||
+ "WHERE woap.warehouse_out_id = warehouse_out.id AND woap.soft_delete = 0 "
|
||||
+ "AND woap.user_id = {0})";
|
||||
|
||||
wrapper.and(w -> w.in(WarehouseOut::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 + "}")
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
/** 直属下级代理 id + 自己 agentId(老 :682-695,supervisor_id=自己,只取一层) */
|
||||
private List<Long> subordinateAndSelfIds(User user) {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package me.iiv.agent.mcp.tool;
|
||||
|
||||
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.User;
|
||||
import me.iiv.agent.entity.m1.MallGoodsOrder;
|
||||
import me.iiv.agent.entity.m1.MallOrderLogistics;
|
||||
import me.iiv.agent.entity.m1.MallShippingAddress;
|
||||
import me.iiv.agent.entity.m2.WarehouseInOutPic;
|
||||
import me.iiv.agent.entity.m2.WarehouseOut;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutAuditProcess;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutCertificate;
|
||||
import me.iiv.agent.entity.m2.WarehouseOutDetail;
|
||||
import me.iiv.agent.mapper.m1.MallGoodsOrderMapper;
|
||||
import me.iiv.agent.mapper.m1.MallOrderLogisticsMapper;
|
||||
import me.iiv.agent.mapper.m1.MallShippingAddressMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutAuditProcessMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutCertificateMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutDetailMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseOutMapper;
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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 WarehouseOutToolsTest {
|
||||
|
||||
@Mock WarehouseOutMapper warehouseOutMapper;
|
||||
@Mock WarehouseOutDetailMapper detailMapper;
|
||||
@Mock WarehouseOutAuditProcessMapper processMapper;
|
||||
@Mock WarehouseInOutPicMapper picMapper;
|
||||
@Mock WarehouseOutCertificateMapper certificateMapper;
|
||||
@Mock MallGoodsOrderMapper mallGoodsOrderMapper;
|
||||
@Mock MallShippingAddressMapper shippingAddressMapper;
|
||||
@Mock MallOrderLogisticsMapper logisticsMapper;
|
||||
@Mock PermissionService permissionService;
|
||||
|
||||
WarehouseOutTools tools;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseOut.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseOutDetail.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseOutAuditProcess.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseOutCertificate.class);
|
||||
tools = new WarehouseOutTools(warehouseOutMapper, detailMapper, processMapper, picMapper,
|
||||
certificateMapper, mallGoodsOrderMapper, shippingAddressMapper, logisticsMapper,
|
||||
permissionService);
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
AuthContextHolder.set(new AuthContext(null, user));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
AuthContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAppliesFiltersAndBatchAggregatesRelations() {
|
||||
WarehouseOut order = order();
|
||||
when(warehouseOutMapper.selectCount(any())).thenReturn(1L);
|
||||
when(warehouseOutMapper.selectList(any())).thenReturn(List.of(order));
|
||||
WarehouseOutDetail detail = detail(20L, 8L, 0L);
|
||||
detail.setWarehouseName("一号仓");
|
||||
detail.setOutConfirmUserName("李四");
|
||||
detail.setOutDate(LocalDate.of(2026, 9, 15));
|
||||
when(detailMapper.selectList(any())).thenReturn(List.of(detail));
|
||||
WarehouseOutAuditProcess schedule = new WarehouseOutAuditProcess();
|
||||
schedule.setWarehouseOutId(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"));
|
||||
assertEquals("rows 超过上限 100,已截断为 100", result.get("notice"));
|
||||
@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("warehouseOutPeople"));
|
||||
assertEquals(List.of("王五"), record.get("schedulePeople"));
|
||||
verify(permissionService).applyWarehouseOutRowFilter(any(), any());
|
||||
verify(detailMapper).selectList(any());
|
||||
verify(processMapper).selectList(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listRejectsInvalidAndReversedDates() {
|
||||
assertThrows(IllegalArgumentException.class, () -> listWithDates("2026-02-30", null));
|
||||
assertThrows(IllegalArgumentException.class, () -> listWithDates("2026-09-16", "2026-09-15"));
|
||||
verifyNoInteractions(warehouseOutMapper, permissionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailUsesSamePermissionAndHidesExistence() {
|
||||
when(warehouseOutMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
ToolCallException error = assertThrows(ToolCallException.class,
|
||||
() -> tools.warehouseOutDetailQuery(20L, null, false, null));
|
||||
|
||||
assertEquals(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, error.getCode());
|
||||
verify(permissionService).applyWarehouseOutRowFilter(any(), any());
|
||||
verifyNoInteractions(detailMapper, processMapper, picMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsAllocationItemsSerialsFilesCertificateAddressAndLogistics() {
|
||||
WarehouseOut order = order();
|
||||
order.setAddressId(30L);
|
||||
when(warehouseOutMapper.selectList(any())).thenReturn(List.of(order));
|
||||
WarehouseOutDetail item = detail(20L, 8L, 0L);
|
||||
item.setMaterialNo("MAT-1");
|
||||
item.setMaterielFullName("纸巾机");
|
||||
item.setOutCount(2);
|
||||
item.setPrice(new BigDecimal("10.00"));
|
||||
item.setAllMoney(new BigDecimal("20.00"));
|
||||
WarehouseOutDetail serial = detail(20L, 8L, 101L);
|
||||
serial.setSnCode("SN-001");
|
||||
when(detailMapper.selectList(any())).thenReturn(List.of(item), List.of(serial));
|
||||
when(detailMapper.selectCount(any())).thenReturn(2L);
|
||||
|
||||
WarehouseOutAuditProcess process = new WarehouseOutAuditProcess();
|
||||
process.setId(4L);
|
||||
process.setWarehouseOutId(20L);
|
||||
process.setScheduleState(2);
|
||||
process.setAuditState(1);
|
||||
process.setOperationTheme(3);
|
||||
process.setOperationType(1);
|
||||
when(processMapper.selectList(any())).thenReturn(List.of(process));
|
||||
|
||||
WarehouseInOutPic application = pic(1L, 0, 1, "apply.jpg");
|
||||
WarehouseInOutPic confirmation = pic(2L, 2, 1, "confirm.jpg");
|
||||
WarehouseInOutPic certificateFile = pic(3L, 4, 99, "cert.jpg");
|
||||
when(picMapper.selectList(any())).thenReturn(List.of(application, confirmation, certificateFile));
|
||||
WarehouseOutCertificate certificate = new WarehouseOutCertificate();
|
||||
certificate.setId(9L);
|
||||
certificate.setMemo("签收凭证");
|
||||
when(certificateMapper.selectList(any())).thenReturn(List.of(certificate));
|
||||
|
||||
MallShippingAddress address = new MallShippingAddress();
|
||||
address.setId(30L);
|
||||
address.setSoftDelete(0);
|
||||
address.setProvince("广东省");
|
||||
address.setCity("深圳市");
|
||||
address.setCountyseat("南山区");
|
||||
address.setAddress("科技园");
|
||||
when(shippingAddressMapper.selectById(30L)).thenReturn(address);
|
||||
MallOrderLogistics logistics = new MallOrderLogistics();
|
||||
logistics.setState(7);
|
||||
logistics.setDeliverName("配送员");
|
||||
when(logisticsMapper.selectList(any())).thenReturn(List.of(logistics));
|
||||
|
||||
Map<String, Object> result = tools.warehouseOutDetailQuery(20L, "CK-001", true, 1);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> record = (Map<String, Object>) result.get("record");
|
||||
assertEquals("ALLOCATION", record.get("sourceType"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> items = (List<Map<String, Object>>) result.get("items");
|
||||
assertEquals("MAT-1", items.get(0).get("materialNo"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> serials = (List<Map<String, Object>>) result.get("serials");
|
||||
assertEquals("SN-001", serials.get(0).get("snCode"));
|
||||
assertEquals(2L, result.get("serialTotal"));
|
||||
assertTrue((Boolean) result.get("serialTruncated"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> addressMap = (Map<String, Object>) result.get("address");
|
||||
assertEquals("广东省深圳市南山区科技园", addressMap.get("fullAddress"));
|
||||
@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> certificateMap = (Map<String, Object>) result.get("certificate");
|
||||
assertEquals("签收凭证", certificateMap.get("memo"));
|
||||
assertEquals(1, ((List<?>) certificateMap.get("files")).size());
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> processes = (List<Map<String, Object>>) result.get("processes");
|
||||
assertEquals("出库确认", processes.get(0).get("operationThemeText"));
|
||||
assertEquals("确认出库", processes.get(0).get("operationTypeText"));
|
||||
verify(permissionService).applyWarehouseOutRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailDoesNotReturnSerialRowsUnlessRequested() {
|
||||
when(warehouseOutMapper.selectList(any())).thenReturn(List.of(order()));
|
||||
when(detailMapper.selectList(any())).thenReturn(List.of());
|
||||
when(detailMapper.selectCount(any())).thenReturn(3L);
|
||||
when(processMapper.selectList(any())).thenReturn(List.of());
|
||||
when(picMapper.selectList(any())).thenReturn(List.of());
|
||||
when(certificateMapper.selectList(any())).thenReturn(List.of());
|
||||
when(logisticsMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
Map<String, Object> result = tools.warehouseOutDetailQuery(20L, null, false, 1);
|
||||
|
||||
assertEquals(List.of(), result.get("serials"));
|
||||
assertEquals(3L, result.get("serialTotal"));
|
||||
assertFalse((Boolean) result.get("serialTruncated"));
|
||||
assertNull(result.get("address"));
|
||||
assertNull(result.get("certificate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void springAiPublishesBothWarehouseOutToolSchemas() {
|
||||
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.out.list.query").contains("outDateFrom"));
|
||||
assertTrue(schemas.get("warehouse.out.list.query").contains("pickupOrderNo"));
|
||||
assertTrue(schemas.get("warehouse.out.detail.query").contains("includeSerials"));
|
||||
}
|
||||
|
||||
private Map<String, Object> list(Integer page, Integer rows) {
|
||||
return tools.warehouseOutListQuery(null, null, null, null, null, null,
|
||||
null, null, null, null, 3, null, null, null,
|
||||
null, null, null, "2026-09-01", "2026-09-30",
|
||||
null, null, page, rows);
|
||||
}
|
||||
|
||||
private Map<String, Object> listWithDates(String from, String to) {
|
||||
return tools.warehouseOutListQuery(null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, from, to, 1, 20);
|
||||
}
|
||||
|
||||
private static WarehouseOut order() {
|
||||
WarehouseOut order = new WarehouseOut();
|
||||
order.setId(20L);
|
||||
order.setOutOrderNo("CK-001");
|
||||
order.setAllocationOrderNo("DB-001");
|
||||
order.setOrderState(3);
|
||||
order.setSendMode(0);
|
||||
return order;
|
||||
}
|
||||
|
||||
private static WarehouseOutDetail detail(Long orderId, Long warehouseId, Long parentId) {
|
||||
WarehouseOutDetail detail = new WarehouseOutDetail();
|
||||
detail.setId(parentId == 0 ? 101L : 102L);
|
||||
detail.setWarehouseOutId(orderId);
|
||||
detail.setWarehouseId(warehouseId);
|
||||
detail.setWarehouseName("一号仓");
|
||||
detail.setParentId(parentId);
|
||||
return detail;
|
||||
}
|
||||
|
||||
private static WarehouseInOutPic pic(Long id, int fileType, int aggregateType, String fileName) {
|
||||
WarehouseInOutPic pic = new WarehouseInOutPic();
|
||||
pic.setId(id);
|
||||
pic.setFileType(fileType);
|
||||
pic.setInOutAllocationType(aggregateType);
|
||||
pic.setFileName(fileName);
|
||||
pic.setOssbucketname("bucket");
|
||||
pic.setOssfolder("warehouse/");
|
||||
return pic;
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
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.WarehouseOut;
|
||||
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 PermissionServiceWarehouseOutRowFilterTest {
|
||||
|
||||
@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(), ""), WarehouseOut.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<WarehouseOut> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyWarehouseOutRowFilter(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<WarehouseOut> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyWarehouseOutRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
Collection<Object> values = wrapper.getParamNameValuePairs().values();
|
||||
assertTrue(sql.contains("agent_id IN"));
|
||||
assertTrue(sql.contains("warehouse_out_details"));
|
||||
assertTrue(sql.contains("warehouse_out_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.applyWarehouseOutRowFilter(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