[fix] 出入库工具复查修复:调拨待办漏本人修改/待办行串位/调拨派生入库单凭证回查 (REQ-20260919-0003) #22

Merged
wangheng merged 1 commits from fix/warehouse-mcp-review-20260919 into main 2026-09-19 22:28:03 +08:00
9 changed files with 195 additions and 19 deletions
@@ -540,7 +540,9 @@
- 返回 `record/sceneType/warehouses/items/processes/files/relatedOutOrder`。
- `items` 按物料定义、物料索引和仓库汇总;`includeSerials` 默认 false,`serialLimit` 默认 100、上限 500,
同时返回 `serialTotal/serialTruncated`。
- `files.application/files.confirmation` 仅返回 `id/fileName/bucket/objectKey`。
- `files.application/files.confirmation/files.allocationAudit` 仅返回 `id/fileName/bucket/objectKey`。
调拨派生的入库单其申请附件与审批流图片挂在调拨单维度(`aggregate_id=调拨单id, in_out_allocation_type=2`),
已按 `allocationOrderNo` 自动回查合并:申请附件并入 `application`,审批流图片(fileType=3)在 `allocationAudit`。
- `relatedOutOrder` 仅给出关联出库单号和查询工具名,不内嵌出库单内容。
详细设计见 `warehouse-in-mcp-tools-design.md`。
@@ -555,8 +557,10 @@
- 分类:warehouse | 类型:读
- 数据来源:m2 流程视图 `warehouse_out_process` / `warehouse_in_process` / `warehouse_allocation_process`
- 权限:待办是"人"的语义,只按流程表 `user_id = 当前登录人` 过滤,不叠加代理商行级权限。
- 待办判定:`user_id = 当前登录人 AND soft_delete = 0 AND operation_type IN (0,1,2)`
(3=纯日志排除)。`todoOnly` 默认 true 再限定 `schedule_state=1` 真待办。
- 待办判定:`user_id = 当前登录人 AND soft_delete = 0`;出入库视图排除
`operation_type=3`(纯日志),调拨视图不过滤 `operation_type`(其 3=本人修改是真实待办)。
`todoOnly` 默认 true 再限定 `schedule_state=1` 真待办,且该条件下推到第二阶段组装查询,
代表行不串到已办审批行。
- 同一笔调拨最多以调拨审批/确认、出库执行、入库执行 3 个视角出现,不去重;
派生自调拨的真实出入库单带 `derivedFrom="allocation"`,可按 `allocationOrderNo` 归并。
- 分页实现:先全量查符合条件流程行按单据 id 内存去重 → 内存分页 → 按页 id 批量查视图行组装,防 N+1。
@@ -36,6 +36,9 @@
- 可选 `serials` 以及完整 `serialTotal`、`serialTruncated`;
- `schedule_state=2` 的已处理流程;
- `file_type=0/1` 的申请、确认入库附件,仅返回 OSS 对象定位,不生成签名 URL;
调拨派生入库单的申请附件(fileType=0)与审批流图片(fileType=3)挂在调拨单维度
(`aggregate_id=调拨单id, in_out_allocation_type=2`),详情按 `allocationOrderNo` 回查合并:
前者并入 `files.application`,后者在 `files.allocationAudit`;
- `relatedOutOrder`:仅出库单号和建议调用的 `warehouse.out.detail.query` 工具名。
## 4. 权限与安全
@@ -17,11 +17,13 @@
流程视图一行 = 单据 × 处理人 × 待办环节。待办判定:
```
user_id = 当前登录人 AND soft_delete = 0 AND operation_type IN (0, 1, 2)
user_id = 当前登录人 AND soft_delete = 0
```
- `operation_type=3` 为"驳回或修改记录",纯日志不需要待办,排除(出入库视图);
调拨视图 `operation_type=4`(撤销)同为纯日志,也被 IN (0,1,2) 排除。
- 出入库视图再排除 `operation_type=3`("驳回或修改记录",纯日志不需要待办);
调拨视图**不过滤** `operation_type`——其 `3=本人修改` 是真实待办环节(调拨被驳回后
申请人需修改重新提交),老系统调拨待办接口同样不过滤(`warehouseAllocationProcessListByPage`
仅按 user_id + soft_delete 过滤)。`4=撤销` 在调拨视图也视为可操作环节一并返回。
- 不按代理商过滤:待办是"人"的语义,不是代理商数据权限语义。
老系统出入库待办同样只按 `user_id` 过滤(`input.setUserId(user.getId())`),
只有调拨待办显式 `eq("user_id", user.getId())` 且代理行级过滤被注释掉,
@@ -34,8 +36,8 @@ user_id = 当前登录人 AND soft_delete = 0 AND operation_type IN (0, 1, 2)
| 0 | 审批流操作 | 审批流操作 |
| 1 | 确认出库 / 确认入库 | 确认入库操作 |
| 2 | 本人修改(需申请人待办) | 确认出库操作 |
| 3 | 驳回或修改操作(纯日志,排除) | 本人修改 |
| 4 | — | 撤销(纯日志,排除) |
| 3 | 驳回或修改操作(纯日志,排除) | 本人修改(真实待办,保留) |
| 4 | — | 撤销(保留) |
`schedule_state`:0=无需待办,1=待办,2=已办。
@@ -0,0 +1,26 @@
package me.iiv.agent.entity.m2;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
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;
/** 调拨/采购/划转单 warehouse_allocation(m2,只读):op_type 0调拨 1采购 2划转。 */
@Getter
@Setter
@ToString
@TableName("warehouse_allocation")
public class WarehouseAllocation implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Integer softDelete;
@TableField("allocation_order_no")
private String allocationOrderNo;
private Integer opType;
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m2;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m2.WarehouseAllocation;
public interface WarehouseAllocationMapper extends BaseMapper<WarehouseAllocation> {
}
@@ -3,11 +3,13 @@ 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.WarehouseAllocation;
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.WarehouseAllocationMapper;
import me.iiv.agent.mapper.m2.WarehouseInAuditProcessMapper;
import me.iiv.agent.mapper.m2.WarehouseInDetailMapper;
import me.iiv.agent.mapper.m2.WarehouseInMapper;
@@ -49,6 +51,7 @@ public class WarehouseInTools implements ToolObject {
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 int ALLOCATION_TYPE = 2;
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 =
@@ -71,6 +74,7 @@ public class WarehouseInTools implements ToolObject {
final WarehouseInDetailMapper detailMapper;
final WarehouseInAuditProcessMapper processMapper;
final WarehouseInOutPicMapper picMapper;
final WarehouseAllocationMapper allocationMapper;
final WarehouseTypeMapper warehouseTypeMapper;
final PermissionService permissionService;
@@ -78,12 +82,14 @@ public class WarehouseInTools implements ToolObject {
WarehouseInDetailMapper detailMapper,
WarehouseInAuditProcessMapper processMapper,
WarehouseInOutPicMapper picMapper,
WarehouseAllocationMapper allocationMapper,
WarehouseTypeMapper warehouseTypeMapper,
PermissionService permissionService) {
this.warehouseInMapper = warehouseInMapper;
this.detailMapper = detailMapper;
this.processMapper = processMapper;
this.picMapper = picMapper;
this.allocationMapper = allocationMapper;
this.warehouseTypeMapper = warehouseTypeMapper;
this.permissionService = permissionService;
}
@@ -180,6 +186,8 @@ public class WarehouseInTools implements ToolObject {
@Tool(name = "warehouse.in.detail.query", description = "按 id 或入库单号查询入库单详情。"
+ "行级权限与 warehouse.in.list.query 完全一致,不存在与无权访问统一返回 NOT_FOUND_OR_FORBIDDEN。"
+ "返回主单、场景类型、仓库、按物料和仓库汇总的明细、可选 SN、已处理流程和申请/确认附件;调拨入库单可直接查询。"
+ "files 含 application(申请附件)/confirmation(入库确认)/allocationAudit(调拨审批流图片);"
+ "调拨派生的入库单其申请附件与审批图片挂在调拨单维度,已按调拨单号自动回查合并。"
+ "关联出库仅返回索引,不内嵌内容。includeSerials 默认 false;serialLimit 默认100、上限500。")
public Map<String, Object> warehouseInDetailQuery(
@ToolParam(required = false, description = "入库单主键 id,与 inOrderNo 至少传一个") Long id,
@@ -222,11 +230,19 @@ public class WarehouseInTools implements ToolObject {
.eq(WarehouseInAuditProcess::getScheduleState, 2)
.orderByAsc(WarehouseInAuditProcess::getOperationTime)
.orderByAsc(WarehouseInAuditProcess::getId));
List<WarehouseInOutPic> pics = picMapper.selectList(new LambdaQueryWrapper<WarehouseInOutPic>()
List<WarehouseInOutPic> pics = new ArrayList<>(picMapper.selectList(new LambdaQueryWrapper<WarehouseInOutPic>()
.eq(WarehouseInOutPic::getSoftDelete, 0)
.eq(WarehouseInOutPic::getAggregateId, order.getId())
.eq(WarehouseInOutPic::getInOutAllocationType, IN_TYPE)
.orderByAsc(WarehouseInOutPic::getId));
.orderByAsc(WarehouseInOutPic::getId)));
// 调拨派生的入库单:申请附件(fileType=0)和审批流图片(fileType=3)挂在调拨单维度
// (aggregate_id=调拨单id, in_out_allocation_type=2),按调拨单号回查补齐,否则这两类凭证恒为空。
List<WarehouseInOutPic> allocationPics = allocationPics(order);
if (!allocationPics.isEmpty()) {
Set<Long> seen = pics.stream().map(WarehouseInOutPic::getId).collect(Collectors.toSet());
allocationPics.stream().filter(pic -> pic.getId() == null || seen.add(pic.getId()))
.forEach(pics::add);
}
WarehouseType type = order.getWarehouseTypeId() == null
? null : warehouseTypeMapper.selectById(order.getWarehouseTypeId());
@@ -242,6 +258,7 @@ public class WarehouseInTools implements ToolObject {
Map<String, Object> files = new LinkedHashMap<>();
files.put("application", fileRecords(pics, 0));
files.put("confirmation", fileRecords(pics, 1));
files.put("allocationAudit", fileRecords(pics, 3));
result.put("files", files);
result.put("relatedOutOrder", relatedOutOrder(order));
return result;
@@ -413,8 +430,26 @@ public class WarehouseInTools implements ToolObject {
}).collect(Collectors.toList());
}
private Map<String, Object> relatedOutOrder(WarehouseIn order) {
if (!StringUtils.hasText(order.getOutOrderNo())) return null;
/** 调拨派生入库单的申请/审批凭证挂在调拨单维度,按调拨单号回查;调拨单不存在或无调拨单号时返回空。 */
private List<WarehouseInOutPic> allocationPics(WarehouseIn order) {
if (!StringUtils.hasText(order.getAllocationOrderNo())) {
return List.of();
}
WarehouseAllocation allocation = allocationMapper.selectOne(new LambdaQueryWrapper<WarehouseAllocation>()
.eq(WarehouseAllocation::getSoftDelete, 0)
.eq(WarehouseAllocation::getAllocationOrderNo, order.getAllocationOrderNo())
.last("LIMIT 1"));
if (allocation == null) {
return List.of();
}
return picMapper.selectList(new LambdaQueryWrapper<WarehouseInOutPic>()
.eq(WarehouseInOutPic::getSoftDelete, 0)
.eq(WarehouseInOutPic::getAggregateId, allocation.getId())
.eq(WarehouseInOutPic::getInOutAllocationType, ALLOCATION_TYPE)
.orderByAsc(WarehouseInOutPic::getId));
}
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");
@@ -40,7 +40,7 @@ public class WarehouseTodoTools implements ToolObject {
private static final DateTimeFormatter TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(BUSINESS_ZONE);
/** 待办环节只保留可办理的操作类型,3(驳回或修改记录)为纯日志,排除。 */
/** 待办环节只保留可办理的操作类型,3(驳回或修改记录)为纯日志,排除。仅适用于出入库视图,调拨视图不过滤。 */
private static final List<Integer> TODO_OPERATION_TYPES = List.of(0, 1, 2);
private static final Map<Integer, String> SCHEDULE_STATE_TEXT = Map.of(
@@ -76,8 +76,9 @@ public class WarehouseTodoTools implements ToolObject {
@Tool(name = "warehouse.todo.list.query", description = "分页查询当前用户的仓库待办,统一老系统 /myWarehouseOut/needToBeDealtWith、"
+ "/myWarehouseIn/needToBeDealtWith、/warehouseAllocation/warehouseAllocationProcessListByPage 三个待办接口。"
+ "数据来自 m2 流程视图 warehouse_out_process / warehouse_in_process / warehouse_allocation_process:"
+ "待办语义 = 视图里存在当前用户的任务行(user_id = 当前登录人 AND soft_delete = 0 AND operation_type IN (0,1,2),"
+ "3=纯日志排除),不按代理商过滤。同一笔调拨最多以调拨审批/确认、出库执行、入库执行 3 个视角出现,不去重;"
+ "待办语义 = 视图里存在当前用户的任务行(user_id = 当前登录人 AND soft_delete = 0,"
+ "出库/入库视图排除 operation_type=3 纯日志;调拨视图不过滤 operation_type,其 3=本人修改 是真实待办),不按代理商过滤。"
+ "同一笔调拨最多以调拨审批/确认、出库执行、入库执行 3 个视角出现,不去重;"
+ "派生自调拨的真实出入库单带 derivedFrom=\"allocation\" 标记。"
+ "docType:out=出库单 in=入库单 allocation=调拨/采购/划转单(按 op_type 区分) all=全部,默认 all。"
+ "todoOnly 默认 true(只返回 schedule_state=1 真待办),false 返回当前用户参与过的全部单据。"
@@ -174,11 +175,11 @@ public class WarehouseTodoTools implements ToolObject {
}
private List<TodoStub> allocationStubs(Long userId, boolean onlyTodo, Integer orderState, String orderNo) {
// 老系统调拨待办接口不过滤 operation_type(3=本人修改 是真实待办环节),与出入库视图口径不同
LambdaQueryWrapper<WarehouseAllocationProcess> wrapper = new LambdaQueryWrapper<WarehouseAllocationProcess>()
.select(WarehouseAllocationProcess::getWarehouseAllocationId, WarehouseAllocationProcess::getApplyDate)
.eq(WarehouseAllocationProcess::getSoftDelete, 0)
.eq(WarehouseAllocationProcess::getUserId, userId)
.in(WarehouseAllocationProcess::getOperationType, TODO_OPERATION_TYPES)
.eq(onlyTodo, WarehouseAllocationProcess::getScheduleState, 1)
.eq(orderState != null, WarehouseAllocationProcess::getOrderState, orderState)
.like(StringUtils.hasText(orderNo), WarehouseAllocationProcess::getAllocationOrderNo,
@@ -206,6 +207,7 @@ public class WarehouseTodoTools implements ToolObject {
.eq(WarehouseOutProcess::getSoftDelete, 0)
.eq(WarehouseOutProcess::getUserId, userId)
.in(WarehouseOutProcess::getOperationType, TODO_OPERATION_TYPES)
.eq(onlyTodo, WarehouseOutProcess::getScheduleState, 1)
.in(WarehouseOutProcess::getWarehouseOutId, ids)
.orderByAsc(WarehouseOutProcess::getId)).stream()
.collect(Collectors.groupingBy(WarehouseOutProcess::getWarehouseOutId,
@@ -221,6 +223,7 @@ public class WarehouseTodoTools implements ToolObject {
.eq(WarehouseInProcess::getSoftDelete, 0)
.eq(WarehouseInProcess::getUserId, userId)
.in(WarehouseInProcess::getOperationType, TODO_OPERATION_TYPES)
.eq(onlyTodo, WarehouseInProcess::getScheduleState, 1)
.in(WarehouseInProcess::getWarehouseInId, ids)
.orderByAsc(WarehouseInProcess::getId)).stream()
.collect(Collectors.groupingBy(WarehouseInProcess::getWarehouseInId,
@@ -235,7 +238,7 @@ public class WarehouseTodoTools implements ToolObject {
new LambdaQueryWrapper<WarehouseAllocationProcess>()
.eq(WarehouseAllocationProcess::getSoftDelete, 0)
.eq(WarehouseAllocationProcess::getUserId, userId)
.in(WarehouseAllocationProcess::getOperationType, TODO_OPERATION_TYPES)
.eq(onlyTodo, WarehouseAllocationProcess::getScheduleState, 1)
.in(WarehouseAllocationProcess::getWarehouseAllocationId, ids)
.orderByAsc(WarehouseAllocationProcess::getId)).stream()
.collect(Collectors.groupingBy(WarehouseAllocationProcess::getWarehouseAllocationId,
@@ -263,7 +266,8 @@ public class WarehouseTodoTools implements ToolObject {
}
}
/** 取代表当前待办环节的行:优先 schedule_state=1 的真待办行,否则取首行(已办/无需待办)。 */
/** 取代表当前待办环节的行:onlyTodo 时 assemble 已下推 schedule_state=1,取首行即可;
* 否则优先 schedule_state=1 的真待办行,再退回首行(已办/无需待办)。 */
private static <T> T pickPending(List<T> rows, boolean onlyTodo) {
if (onlyTodo) {
return rows.get(0);
@@ -3,11 +3,13 @@ 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.WarehouseAllocation;
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.WarehouseAllocationMapper;
import me.iiv.agent.mapper.m2.WarehouseInAuditProcessMapper;
import me.iiv.agent.mapper.m2.WarehouseInDetailMapper;
import me.iiv.agent.mapper.m2.WarehouseInMapper;
@@ -49,6 +51,7 @@ class WarehouseInToolsTest {
@Mock WarehouseInDetailMapper detailMapper;
@Mock WarehouseInAuditProcessMapper processMapper;
@Mock WarehouseInOutPicMapper picMapper;
@Mock WarehouseAllocationMapper allocationMapper;
@Mock WarehouseTypeMapper warehouseTypeMapper;
@Mock PermissionService permissionService;
@@ -61,9 +64,10 @@ class WarehouseInToolsTest {
TableInfoHelper.initTableInfo(assistant, WarehouseInDetail.class);
TableInfoHelper.initTableInfo(assistant, WarehouseInAuditProcess.class);
TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class);
TableInfoHelper.initTableInfo(assistant, WarehouseAllocation.class);
TableInfoHelper.initTableInfo(assistant, WarehouseType.class);
tools = new WarehouseInTools(warehouseInMapper, detailMapper, processMapper, picMapper,
warehouseTypeMapper, permissionService);
allocationMapper, warehouseTypeMapper, permissionService);
User user = new User();
user.setId(7L);
AuthContextHolder.set(new AuthContext(null, user));
@@ -153,12 +157,54 @@ class WarehouseInToolsTest {
Map<String, Object> files = (Map<String, Object>) result.get("files");
assertEquals(1, ((List<?>) files.get("application")).size());
assertEquals(1, ((List<?>) files.get("confirmation")).size());
assertEquals(0, ((List<?>) files.get("allocationAudit")).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 detailMergesAllocationDimensionPicsForDerivedInOrder() {
WarehouseIn order = order();
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());
WarehouseAllocation allocation = new WarehouseAllocation();
allocation.setId(40L);
allocation.setAllocationOrderNo("DB-001");
when(allocationMapper.selectOne(any())).thenReturn(allocation);
WarehouseInOutPic own = pic(1L, 0);
WarehouseInOutPic allocApply = pic(2L, 0);
allocApply.setInOutAllocationType(2);
WarehouseInOutPic allocAudit = pic(3L, 3);
allocAudit.setInOutAllocationType(2);
when(picMapper.selectList(any())).thenReturn(List.of(own), List.of(allocApply, allocAudit));
Map<String, Object> result = tools.warehouseInDetailQuery(20L, null, false, null);
@SuppressWarnings("unchecked")
Map<String, Object> files = (Map<String, Object>) result.get("files");
assertEquals(2, ((List<?>) files.get("application")).size());
assertEquals(0, ((List<?>) files.get("confirmation")).size());
assertEquals(1, ((List<?>) files.get("allocationAudit")).size());
}
@Test
void detailSkipsAllocationLookupWhenOrderIsNotAllocationDerived() {
WarehouseIn order = order();
order.setAllocationOrderNo(null);
when(warehouseInMapper.selectList(any())).thenReturn(List.of(order));
when(detailMapper.selectList(any())).thenReturn(List.of());
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(0, ((List<?>) ((Map<?, ?>) result.get("files")).get("application")).size());
verifyNoInteractions(allocationMapper);
}
@Test
void detailDoesNotReturnSerialRowsUnlessRequested() {
WarehouseIn order = order();
@@ -142,6 +142,55 @@ class WarehouseTodoToolsTest {
assertEquals("确认出库", records.get(0).get("operationTypeText"));
}
@Test
void todoOnlyTruePushesScheduleStateDownToAssembleQuery() {
WarehouseOutProcess stub = new WarehouseOutProcess();
stub.setWarehouseOutId(20L);
stub.setApplyDate(date(2026, 9, 10));
WarehouseOutProcess pendingRow = outRow(20L, null);
when(outProcessMapper.selectList(any())).thenReturn(List.of(stub), List.of(pendingRow));
tools.warehouseTodoListQuery("out", true, null, null, 1, 20);
var captor = org.mockito.ArgumentCaptor.forClass(
com.baomidou.mybatisplus.core.conditions.Wrapper.class);
verify(outProcessMapper, times(2)).selectList(captor.capture());
String assembleSql = captor.getAllValues().get(1).getSqlSegment();
assertTrue(assembleSql.contains("schedule_state"), assembleSql);
tools.warehouseTodoListQuery("out", false, null, null, 1, 20);
var captor2 = org.mockito.ArgumentCaptor.forClass(
com.baomidou.mybatisplus.core.conditions.Wrapper.class);
verify(outProcessMapper, times(4)).selectList(captor2.capture());
String assembleSqlAll = captor2.getAllValues().get(3).getSqlSegment();
assertTrue(!assembleSqlAll.contains("schedule_state"), assembleSqlAll);
}
@Test
void allocationTodoIncludesModifyOperationTypeAndSkipsOperationTypeFilter() {
WarehouseAllocationProcess stub = new WarehouseAllocationProcess();
stub.setWarehouseAllocationId(40L);
stub.setApplyDate(date(2026, 9, 12));
WarehouseAllocationProcess modifyRow = allocationRow(40L);
modifyRow.setOperationType(3);
when(allocationProcessMapper.selectList(any())).thenReturn(List.of(stub), List.of(modifyRow));
Map<String, Object> result = tools.warehouseTodoListQuery("allocation", true, null, null, 1, 20);
assertEquals(1, result.get("total"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> records = (List<Map<String, Object>>) result.get("records");
assertEquals(1, records.size());
assertEquals(3, records.get(0).get("operationType"));
assertEquals("本人修改", records.get(0).get("operationTypeText"));
var captor = org.mockito.ArgumentCaptor.forClass(
com.baomidou.mybatisplus.core.conditions.Wrapper.class);
verify(allocationProcessMapper, times(2)).selectList(captor.capture());
String stageOneSql = captor.getAllValues().get(0).getSqlSegment();
assertTrue(!stageOneSql.contains("operation_type"), stageOneSql);
}
@Test
void listRejectsInvalidDocType() {
assertThrows(IllegalArgumentException.class,