diff --git a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md index 86d96b9..a0c0460 100644 --- a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md +++ b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md @@ -594,7 +594,7 @@ - `orderType`:0=装机单送货,1=出库单送货;`orderState`:0=派送中,1=已完成,2=已取消; `sendMode`:0=快递公司,1=自行派送。 - 申请日期按业务字段 `apply_time` 查询,不沿用旧接口的 `create_time`;申请与完成日期可同时过滤。 -- 行级权限:总部看全部;代理身份可看申请代理或所属代理在自己、直属下级、授权代管范围内的工单, +- 行级权限:总部看全部;代理身份可看申请代理或所属代理在自己、授权代管范围内的工单(不含直属下级), 以及授权代管物业和本人参与流程的工单;销售非负责人按其可见物业范围过滤。 - 返回 `total/page/rows/records`,固定按 ID 倒序;记录同时返回状态、类型、配送方式的码值与文案。 @@ -730,7 +730,10 @@ - `withdrawDetails`:`withdraw_order_details` 中 `in_count>0` 的实际取回物品明细。 - `maintenanceUsers/cooperateUsers`:`install_order_user type=1` 的运维/协同人员。 - `processes`:`order_type=1`、`schedule_state=2` 且非抄送(theme=12)的流程,按操作时间、ID 正序,主题/类型给 code+文案。 -- `certificates`:流程凭证附件,限定 `in_out_allocation_type=12`,仅返回 `id/fileName/bucket/objectKey`。 +- `certificates`:凭证附件,两个来源——取货/入库签收凭证查派生取货工单的 + `pickup_order_process`(operation_theme 1/2)再按 `aggregate_id + in_out_allocation_type=12` + 查 `warehouse_in_out_pic`(`source=pickup_process`);发货附件查 m1 + `mall_logistics_pic(order_no=取货单号)`(`source=logistics`)。仅返回 `id/fileName/bucket/objectKey`。 - 撤机结单后自动生成取货工单(`pickup_order.type=2`),可用 `pickup.order.list.query` 的 `withdrawOrderNo` 参数串联查取货/入库进度。 详细设计见 `withdraw-order-mcp-tools-design.md`。 diff --git a/xiaoqu-agent-service/doc/delivery-order-mcp-tools-design.md b/xiaoqu-agent-service/doc/delivery-order-mcp-tools-design.md index f946346..9da9910 100644 --- a/xiaoqu-agent-service/doc/delivery-order-mcp-tools-design.md +++ b/xiaoqu-agent-service/doc/delivery-order-mcp-tools-design.md @@ -29,9 +29,10 @@ 列表和详情统一调用 `PermissionService.applyDeliveryOrderRowFilter`: 1. 总部身份不增加过滤。 -2. 销售非负责人按自己创建及代管代理名下的物业范围过滤,同时保留本人流程参与记录。 -3. 其他代理身份可见申请代理或所属代理在自己、直属下级和授权代管范围内的工单;另包含代管物业、 - 本人参与 `delivery_order_process` 的工单。 +2. 销售非负责人按自己创建及代管代理名下的物业范围过滤(与老系统 getWorkOrderAuth(type=1) + 的 saleFlag 分支一致,不再额外保留本人流程参与记录)。 +3. 其他代理身份可见申请代理或所属代理在自己和授权代管范围内的工单(**不含直属下级代理**, + 与其他 4 组工单工具及老系统一致);另包含代管物业、本人参与 `delivery_order_process` 的工单。 4. 详情不存在与无权查看统一返回 `NOT_FOUND_OR_FORBIDDEN`。 ## 查询与输出约束 diff --git a/xiaoqu-agent-service/doc/withdraw-order-mcp-tools-design.md b/xiaoqu-agent-service/doc/withdraw-order-mcp-tools-design.md index dbccb96..a32fad4 100644 --- a/xiaoqu-agent-service/doc/withdraw-order-mcp-tools-design.md +++ b/xiaoqu-agent-service/doc/withdraw-order-mcp-tools-design.md @@ -52,17 +52,20 @@ - `maintenanceUsers / cooperateUsers`:来自 `install_order_user`(type=1,user_type 0/1) - `processes`:`order_process`(order_type=1,schedule_state=2 已办、排除抄送 theme=12, 按 operationTime+id 升序),主题/类型同时给 code+文案 -- `certificates`:凭证附件 `warehouse_in_out_pic`(in_out_allocation_type=12), - 输出 `{id, fileName, bucket, objectKey}` 三元组(不拼 URL,与现有 4 组工单一致),按流程 id 批量 IN 查询防 N+1 +- `certificates`:凭证附件,1:1 对齐老系统 lookDetails 的两个来源(见下方说明), + 输出 `{id, fileName, bucket, objectKey, source}`(不拼 URL,与现有 4 组工单一致) 不存在或无权统一抛 `NOT_FOUND_OR_FORBIDDEN`(不泄露存在性)。 -### 凭证口径说明(近似) +### 凭证口径(2026-09-18 修正) -老系统取货/入库凭证(theme 6/7)挂在 `pickup_order_process` 上(经取货单号二次查询), -MCP 侧简化为按本工单 `order_process` 流程 id 批量查 `warehouse_in_out_pic(in_out_allocation_type=12)`。 -两者 aggregate_id 语义不完全等价,若线上凭证挂接方式不同需再校准(可与 -`pickup.order.detail.query` 的 `withdrawOrderNo` 串联作为兜底)。 +老系统取货/入库凭证(theme 6/7)的 `aggregate_id` 挂在**派生取货工单**的 +`pickup_order_process`(经取货单号二次查询,operation_theme 1=取货、2=入库确认), +与共享 `order_process` 是两套各自自增的 id,不能按 order_process.id 混查(会查不到 +或把其他取货工单的凭证挂到本单)。MCP 侧按取货单号查 `pickup_order_process` 取 id 集合 +再批量 IN 查 `warehouse_in_out_pic(in_out_allocation_type=12)`(`source=pickup_process`); +结单节点的发货附件来自 m1 `mall_logistics_pic(order_no=取货单号)`(`source=logistics`)。 +无取货单号时返回空。 ## 3. 与老系统字段映射 diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/InstallOrderTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/InstallOrderTools.java index 4ccf9b9..5626a94 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/InstallOrderTools.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/InstallOrderTools.java @@ -230,7 +230,6 @@ public class InstallOrderTools implements ToolObject { List users = userMapper.selectList(new LambdaQueryWrapper() .eq(InstallOrderUser::getSoftDelete, 0) .eq(InstallOrderUser::getInstallOrderNo, no) - .eq(InstallOrderUser::getType, 0) .orderByAsc(InstallOrderUser::getId)); Long processTotal = include ? processMapper.selectCount(processBase(no)) : 0L; List processes = include ? processMapper.selectList(processBase(no) diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/WithdrawOrderTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/WithdrawOrderTools.java index 6ba48e7..ad9ca02 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/WithdrawOrderTools.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/WithdrawOrderTools.java @@ -3,13 +3,17 @@ 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.m1.MallLogisticsPic; import me.iiv.agent.entity.m2.InstallOrderUser; import me.iiv.agent.entity.m2.OrderProcess; +import me.iiv.agent.entity.m2.PickupOrderProcess; import me.iiv.agent.entity.m2.WarehouseInOutPic; import me.iiv.agent.entity.m2.WithdrawOrder; import me.iiv.agent.entity.m2.WithdrawOrderDetails; +import me.iiv.agent.mapper.m1.MallLogisticsPicMapper; import me.iiv.agent.mapper.m2.InstallOrderUserMapper; import me.iiv.agent.mapper.m2.OrderProcessMapper; +import me.iiv.agent.mapper.m2.PickupOrderProcessMapper; import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper; import me.iiv.agent.mapper.m2.WithdrawOrderDetailsMapper; import me.iiv.agent.mapper.m2.WithdrawOrderMapper; @@ -29,6 +33,7 @@ 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.List; @@ -73,6 +78,8 @@ public class WithdrawOrderTools implements ToolObject { final OrderProcessMapper processMapper; final InstallOrderUserMapper userMapper; final WarehouseInOutPicMapper picMapper; + final PickupOrderProcessMapper pickupProcessMapper; + final MallLogisticsPicMapper mallPicMapper; final PermissionService permissionService; public WithdrawOrderTools(WithdrawOrderMapper orderMapper, @@ -80,12 +87,16 @@ public class WithdrawOrderTools implements ToolObject { OrderProcessMapper processMapper, InstallOrderUserMapper userMapper, WarehouseInOutPicMapper picMapper, + PickupOrderProcessMapper pickupProcessMapper, + MallLogisticsPicMapper mallPicMapper, PermissionService permissionService) { this.orderMapper = orderMapper; this.detailsMapper = detailsMapper; this.processMapper = processMapper; this.userMapper = userMapper; this.picMapper = picMapper; + this.pickupProcessMapper = pickupProcessMapper; + this.mallPicMapper = mallPicMapper; this.permissionService = permissionService; } @@ -179,7 +190,9 @@ public class WithdrawOrderTools implements ToolObject { @Tool(name = "withdraw.order.detail.query", description = "按 id 或撤机工单号查询详情。" + "两者至少传一个,同时传入时必须匹配同一条记录;行级权限与列表一致。" + "不存在或无权查看统一返回 NOT_FOUND_OR_FORBIDDEN。返回主单、撤机物品明细(in_count>0)、" - + "运维/协同人员、已办流程(共享 order_process,order_type=1,排除抄送)与凭证附件;附件仅返回 bucket/objectKey/fileName。") + + "运维/协同人员、已办流程(共享 order_process,order_type=1,排除抄送)与凭证附件;" + + "凭证附件含取货/入库签收凭证(source=pickup_process)与发货附件(source=logistics)," + + "仅返回 bucket/objectKey/fileName。") public Map withdrawOrderDetailQuery( @ToolParam(required = false, description = "撤机工单主键 id,与 withdrawOrderNo 至少传一个") Long id, @ToolParam(required = false, description = "撤机工单号,与 id 至少传一个") String withdrawOrderNo) { @@ -224,7 +237,6 @@ public class WithdrawOrderTools implements ToolObject { .orderByAsc(OrderProcess::getOperationTime) .orderByAsc(OrderProcess::getId)); Map> processUsers = processUsers(processes); - List certificates = certificates(processes); Map result = new LinkedHashMap<>(); result.put("record", orderRecord(order)); @@ -233,8 +245,7 @@ public class WithdrawOrderTools implements ToolObject { result.put("cooperateUsers", personnel(users, 1)); result.put("processes", processes.stream().map(process -> processRecord(process, processUsers.getOrDefault(process.getId(), List.of()))).collect(Collectors.toList())); - result.put("certificates", certificates.stream().map(this::fileRecord) - .collect(Collectors.toList())); + result.put("certificates", certificates(order.getPickupOrderNo())); return result; } @@ -250,14 +261,50 @@ public class WithdrawOrderTools implements ToolObject { LinkedHashMap::new, Collectors.toList())); } - private List certificates(List processes) { - if (processes.isEmpty()) return List.of(); - List ids = processes.stream().map(OrderProcess::getId).collect(Collectors.toList()); - return picMapper.selectList(new LambdaQueryWrapper() - .eq(WarehouseInOutPic::getSoftDelete, 0) - .in(WarehouseInOutPic::getAggregateId, ids) - .eq(WarehouseInOutPic::getInOutAllocationType, PICKUP_AGGREGATE_TYPE) - .orderByAsc(WarehouseInOutPic::getId)); + /** + * 凭证附件,1:1 对齐老系统 lookDetails:取货/入库签收凭证挂在派生取货工单的 + * pickup_order_process(operation_theme 1=取货、2=入库确认,aggregate_id 指向其 id, + * 与共享 order_process 是两套自增 id,不能混查),结单节点的发货附件来自 + * mall_logistics_pic(order_no = 取货单号)。无取货单号时返回空。 + */ + private List> certificates(String pickupOrderNo) { + if (!StringUtils.hasText(pickupOrderNo)) { + return List.of(); + } + List> records = new ArrayList<>(); + List pickupProcesses = pickupProcessMapper.selectList( + new LambdaQueryWrapper() + .eq(PickupOrderProcess::getSoftDelete, 0) + .eq(PickupOrderProcess::getPickupOrderNo, pickupOrderNo) + .in(PickupOrderProcess::getOperationTheme, 1, 2)); + if (!pickupProcesses.isEmpty()) { + List processIds = pickupProcesses.stream().map(PickupOrderProcess::getId) + .collect(Collectors.toList()); + picMapper.selectList(new LambdaQueryWrapper() + .eq(WarehouseInOutPic::getSoftDelete, 0) + .in(WarehouseInOutPic::getAggregateId, processIds) + .eq(WarehouseInOutPic::getInOutAllocationType, PICKUP_AGGREGATE_TYPE) + .orderByAsc(WarehouseInOutPic::getId)) + .forEach(pic -> { + Map record = fileRecord(pic); + record.put("source", "pickup_process"); + records.add(record); + }); + } + mallPicMapper.selectList(new LambdaQueryWrapper() + .eq(MallLogisticsPic::getSoftDelete, 0) + .eq(MallLogisticsPic::getOrderNo, pickupOrderNo) + .orderByAsc(MallLogisticsPic::getId)) + .forEach(pic -> { + Map record = new LinkedHashMap<>(); + record.put("id", pic.getId()); + record.put("fileName", pic.getFileName()); + record.put("bucket", pic.getOssbucketname()); + record.put("objectKey", nullToEmpty(pic.getOssfolder()) + nullToEmpty(pic.getFileName())); + record.put("source", "logistics"); + records.add(record); + }); + return records; } private Map orderRecord(WithdrawOrder order) { diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java index 983476b..55b24d8 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java @@ -198,28 +198,52 @@ public class PermissionService { } /** - * 送货工单权限:总部看全部;销售非负责人按自己创建/代管物业;其余代理身份 - * 可看申请代理、所属代理、代管物业范围,以及本人参与过流程的工单。 + * 送货工单权限:1:1 复刻老系统 getWorkOrderAuth(type=1)。总部看全部;其余代理身份 + * 可看申请代理、所属代理(本代理+授权代管,不含直属下级)、授权代管物业范围, + * 以及本人出现在 delivery_order_process 的工单;销售非负责人只看自己创建及 + * 代管代理名下物业,空集塞 0。 * 列表和详情必须复用本方法,避免详情绕过行权限。 */ public void applyDeliveryOrderRowFilter(LambdaQueryWrapper wrapper, User user) { - RowScope scope = resolveRowScope(user); - if (scope.headquarters) { + if (!isAgentAuthority(user.getAuthorityId())) { + return; + } + if (user.getAgentId() == null) { + throw new IllegalStateException("代理商用户未关联代理商,拒绝访问业务数据"); + } + List auths = managedAuths(user.getId()); + List agentIds = new ArrayList<>(); + agentIds.add(user.getAgentId()); + agentIds.addAll(auths.stream().map(WarehouseAgentAuth::getManagerAgentId) + .collect(Collectors.toList())); + if (isSalesNonLeader(user)) { + List propertyIds = properytMapper.selectList(new LambdaQueryWrapper() + .eq(Properyt::getSoftDelete, 0) + .eq(Properyt::getCreateId, user.getId()) + .select(Properyt::getId)) + .stream().map(Properyt::getId).collect(Collectors.toList()); + List managedAgentIds = auths.stream().map(WarehouseAgentAuth::getManagerAgentId) + .collect(Collectors.toList()); + if (!managedAgentIds.isEmpty()) { + propertyIds.addAll(properytMapper.selectList(new LambdaQueryWrapper() + .eq(Properyt::getSoftDelete, 0) + .in(Properyt::getAgentId, managedAgentIds) + .select(Properyt::getId)) + .stream().map(Properyt::getId).collect(Collectors.toList())); + } + wrapper.in(DeliveryOrder::getPropertyId, + propertyIds.isEmpty() ? List.of(0L) : new LinkedHashSet<>(propertyIds)); return; } String participantExists = "EXISTS (SELECT 1 FROM delivery_order_process dop " + "WHERE dop.delivery_order_no = delivery_order.delivery_order_no " + "AND dop.soft_delete = 0 AND dop.user_id = {0})"; - if (scope.salesPropertyIds != null) { - wrapper.and(w -> w.in(DeliveryOrder::getPropertyId, scope.salesPropertyIds) - .or().apply(participantExists, user.getId())); - return; - } + List managedPropertyIds = managedPropertyIds(agentIds); wrapper.and(w -> { - w.in(DeliveryOrder::getApplyAgentId, scope.ids) - .or().in(DeliveryOrder::getAgentId, scope.ids); - if (!scope.managedPropertyIds.isEmpty()) { - w.or().in(DeliveryOrder::getPropertyId, scope.managedPropertyIds); + w.in(DeliveryOrder::getApplyAgentId, agentIds) + .or().in(DeliveryOrder::getAgentId, agentIds); + if (!managedPropertyIds.isEmpty()) { + w.or().in(DeliveryOrder::getPropertyId, managedPropertyIds); } w.or().apply(participantExists, user.getId()); }); @@ -494,7 +518,7 @@ public class PermissionService { /** * 销售部门非负责人判定(老系统 getByIsAgent 内 saleFlag 逻辑,:703-710): - * Redis db5 Dictionary hash 的 authDeptId 命中 ",deptId," 且 authDeptPostId 不命中 "postId,"。 + * Redis db5 Dictionary hash 的 authDeptId 命中 ",deptId," 且 authDeptPostId 不命中 ",postId,"。 * 字典缺失时拒绝访问,避免权限配置异常导致数据范围放宽。 */ public boolean isSalesNonLeader(User user) { @@ -508,7 +532,7 @@ public class PermissionService { throw new IllegalStateException("销售数据权限配置不可用,拒绝访问业务数据"); } return authDeptId.toString().contains("," + user.getDeptId() + ",") - && !authDeptPostId.toString().contains(user.getPostId() + ","); + && !authDeptPostId.toString().contains("," + user.getPostId() + ","); } /** diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/InstallOrderToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/InstallOrderToolsTest.java index b58ea20..f3d8254 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/InstallOrderToolsTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/InstallOrderToolsTest.java @@ -181,13 +181,11 @@ class InstallOrderToolsTest { worker.setId(1L); worker.setInstallOrderNo("ZJ-001"); worker.setUserType(0); - worker.setType(0); worker.setUserName("运维甲"); InstallOrderUser collaborator = new InstallOrderUser(); collaborator.setId(2L); collaborator.setInstallOrderNo("ZJ-001"); collaborator.setUserType(1); - collaborator.setType(0); collaborator.setUserName("协同乙"); when(userMapper.selectList(any())).thenReturn(List.of(worker, collaborator), List.of()); InstallOrderProcess process = new InstallOrderProcess(); diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/WithdrawOrderToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/WithdrawOrderToolsTest.java index 9a8d1a8..582f5ef 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/WithdrawOrderToolsTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/WithdrawOrderToolsTest.java @@ -3,13 +3,17 @@ 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.m1.MallLogisticsPic; import me.iiv.agent.entity.m2.InstallOrderUser; import me.iiv.agent.entity.m2.OrderProcess; +import me.iiv.agent.entity.m2.PickupOrderProcess; import me.iiv.agent.entity.m2.WarehouseInOutPic; import me.iiv.agent.entity.m2.WithdrawOrder; import me.iiv.agent.entity.m2.WithdrawOrderDetails; +import me.iiv.agent.mapper.m1.MallLogisticsPicMapper; import me.iiv.agent.mapper.m2.InstallOrderUserMapper; import me.iiv.agent.mapper.m2.OrderProcessMapper; +import me.iiv.agent.mapper.m2.PickupOrderProcessMapper; import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper; import me.iiv.agent.mapper.m2.WithdrawOrderDetailsMapper; import me.iiv.agent.mapper.m2.WithdrawOrderMapper; @@ -47,6 +51,8 @@ class WithdrawOrderToolsTest { @Mock OrderProcessMapper processMapper; @Mock InstallOrderUserMapper userMapper; @Mock WarehouseInOutPicMapper picMapper; + @Mock PickupOrderProcessMapper pickupProcessMapper; + @Mock MallLogisticsPicMapper mallPicMapper; @Mock PermissionService permissionService; WithdrawOrderTools tools; @@ -59,8 +65,10 @@ class WithdrawOrderToolsTest { TableInfoHelper.initTableInfo(assistant, OrderProcess.class); TableInfoHelper.initTableInfo(assistant, InstallOrderUser.class); TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class); + TableInfoHelper.initTableInfo(assistant, PickupOrderProcess.class); + TableInfoHelper.initTableInfo(assistant, MallLogisticsPic.class); tools = new WithdrawOrderTools(orderMapper, detailsMapper, processMapper, userMapper, - picMapper, permissionService); + picMapper, pickupProcessMapper, mallPicMapper, permissionService); User user = new User(); user.setId(7L); AuthContextHolder.set(new AuthContext(null, user)); @@ -119,7 +127,8 @@ class WithdrawOrderToolsTest { assertEquals(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, error.getCode()); verify(permissionService).applyWithdrawOrderRowFilter(any(), any()); - verifyNoInteractions(detailsMapper, processMapper, userMapper, picMapper); + verifyNoInteractions(detailsMapper, processMapper, userMapper, picMapper, + pickupProcessMapper, mallPicMapper); } @Test @@ -132,7 +141,9 @@ class WithdrawOrderToolsTest { @Test void detailReturnsDetailsPersonnelProcessesAndCertificates() { - when(orderMapper.selectList(any())).thenReturn(List.of(order())); + WithdrawOrder order = order(); + order.setPickupOrderNo("QH-001"); + when(orderMapper.selectList(any())).thenReturn(List.of(order)); WithdrawOrderDetails detail = new WithdrawOrderDetails(); detail.setId(3L); detail.setMaterialNo("M-001"); @@ -162,14 +173,28 @@ class WithdrawOrderToolsTest { process.setScheduleState(2); process.setOrderType(1); when(processMapper.selectList(any())).thenReturn(List.of(process)); + // 凭证挂在派生取货工单的 pickup_order_process(id 空间与共享 order_process 独立), + // 故意用 50L 区别于 order_process 的 5L,防止实现回退到按 order_process.id 混查 + PickupOrderProcess pickupProcess = new PickupOrderProcess(); + pickupProcess.setId(50L); + pickupProcess.setPickupOrderNo("QH-001"); + pickupProcess.setOperationTheme(1); + when(pickupProcessMapper.selectList(any())).thenReturn(List.of(pickupProcess)); WarehouseInOutPic pic = new WarehouseInOutPic(); pic.setId(7L); - pic.setAggregateId(5L); + pic.setAggregateId(50L); pic.setFileName("finish.jpg"); pic.setOssbucketname("bucket-a"); pic.setOssfolder("withdraw/"); pic.setInOutAllocationType(12); when(picMapper.selectList(any())).thenReturn(List.of(pic)); + MallLogisticsPic logisticsPic = new MallLogisticsPic(); + logisticsPic.setId(8L); + logisticsPic.setOrderNo("QH-001"); + logisticsPic.setFileName("delivery.jpg"); + logisticsPic.setOssbucketname("bucket-b"); + logisticsPic.setOssfolder("logistics/"); + when(mallPicMapper.selectList(any())).thenReturn(List.of(logisticsPic)); Map result = tools.withdrawOrderDetailQuery(20L, "CJGD-001"); @@ -186,9 +211,27 @@ class WithdrawOrderToolsTest { assertEquals("结单", processResult.get("operationTypeText")); assertEquals(1, ((List) processResult.get("maintenanceUsers")).size()); @SuppressWarnings("unchecked") - Map certificate = ((List>) result.get("certificates")).get(0); - assertEquals("withdraw/finish.jpg", certificate.get("objectKey")); - assertEquals("bucket-a", certificate.get("bucket")); + List> certificates = (List>) result.get("certificates"); + assertEquals(2, certificates.size()); + assertEquals("pickup_process", certificates.get(0).get("source")); + assertEquals("withdraw/finish.jpg", certificates.get(0).get("objectKey")); + assertEquals("bucket-a", certificates.get(0).get("bucket")); + assertEquals("logistics", certificates.get(1).get("source")); + assertEquals("logistics/delivery.jpg", certificates.get(1).get("objectKey")); + assertEquals("bucket-b", certificates.get(1).get("bucket")); + } + + @Test + void detailSkipsCertificateQueriesWhenNoPickupOrderNo() { + when(orderMapper.selectList(any())).thenReturn(List.of(order())); + when(detailsMapper.selectList(any())).thenReturn(List.of()); + when(userMapper.selectList(any())).thenReturn(List.of()); + when(processMapper.selectList(any())).thenReturn(List.of()); + + Map result = tools.withdrawOrderDetailQuery(20L, null); + + assertTrue(((List) result.get("certificates")).isEmpty()); + verifyNoInteractions(pickupProcessMapper, mallPicMapper, picMapper); } @Test diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceDeliveryOrderRowFilterTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceDeliveryOrderRowFilterTest.java index ad63603..bd72a00 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceDeliveryOrderRowFilterTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceDeliveryOrderRowFilterTest.java @@ -3,7 +3,6 @@ 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.AgentManageDistrictAuth; import me.iiv.agent.entity.m0.Authority; import me.iiv.agent.entity.m0.User; @@ -25,6 +24,7 @@ import org.springframework.data.redis.core.StringRedisTemplate; import java.util.Collection; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -60,11 +60,8 @@ class PermissionServiceDeliveryOrderRowFilterTest { } @Test - void agentCombinesApplyAgentOwnerManagedPropertyAndParticipant() { + void agentCombinesOwnManagedPropertyAndParticipantWithoutSubordinates() { when(authorityMapper.selectById(9L)).thenReturn(authority(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)); @@ -81,7 +78,9 @@ class PermissionServiceDeliveryOrderRowFilterTest { assertTrue(sql.contains("agent_id IN")); assertTrue(sql.contains("property_id IN")); assertTrue(sql.contains("delivery_order_process")); - assertTrue(values.containsAll(List.of(7L, 10L, 11L, 12L, 30L))); + // 与老系统 getWorkOrderAuth(type=1) 一致:代理范围只含本代理+授权代管,不含直属下级 + assertTrue(values.containsAll(List.of(7L, 10L, 12L, 30L))); + assertFalse(values.contains(11L)); } @Test diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceTest.java index d8c6ea6..f48f987 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceTest.java @@ -145,6 +145,25 @@ class PermissionServiceTest { () -> permissionService.isSalesNonLeader(user)); } + @Test + void salesLeaderPostMatchRequiresExactCommaWrappedSegment() { + // 回归:authDeptPostId 必须以 ",postId," 整段命中。字典 ",21,3," 时 postId=1 + // 是前缀子串误命中("1," 出现在 "21," 里),普通销售不能被误判为负责人跳过物业范围限制 + User prefixUser = user(1000L, 108L, 1L); + prefixUser.setDeptId(5L); + prefixUser.setPostId(1L); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",5,"); + when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn(",21,3,"); + + assertTrue(permissionService.isSalesNonLeader(prefixUser)); + + User leaderUser = user(1000L, 108L, 1L); + leaderUser.setDeptId(5L); + leaderUser.setPostId(21L); + assertFalse(permissionService.isSalesNonLeader(leaderUser)); + } + @Test void districtFilterKeepsAgentOrAuthorizedPropertyBoundary() { User user = user(1000L, 108L, 1L);