From c369ba12342ed50429aa86d6ff6f69b1f41c589b Mon Sep 17 00:00:00 2001 From: qiudl Date: Thu, 17 Sep 2026 11:06:17 +0200 Subject: [PATCH] feat(mcp): add relocation order query tools --- .../doc/agent-mcp-tools-reference.md | 29 ++ .../doc/relocation-order-mcp-tools-design.md | 55 +++ .../me/iiv/agent/entity/m2/OrderProcess.java | 37 ++ .../iiv/agent/entity/m2/RelocationOrder.java | 57 +++ .../entity/m2/RelocationOrderDetail.java | 40 ++ .../agent/mapper/m2/OrderProcessMapper.java | 7 + .../m2/RelocationOrderDetailMapper.java | 7 + .../mapper/m2/RelocationOrderMapper.java | 7 + .../agent/mcp/tool/RelocationOrderTools.java | 406 ++++++++++++++++++ .../agent/permission/PermissionService.java | 59 +++ .../mcp/tool/RelocationOrderToolsTest.java | 189 ++++++++ ...onServiceRelocationOrderRowFilterTest.java | 126 ++++++ 12 files changed, 1019 insertions(+) create mode 100644 xiaoqu-agent-service/doc/relocation-order-mcp-tools-design.md create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/OrderProcess.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrder.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrderDetail.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/OrderProcessMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderDetailMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/RelocationOrderTools.java create mode 100644 xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/RelocationOrderToolsTest.java create mode 100644 xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceRelocationOrderRowFilterTest.java diff --git a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md index 2bccbaa..20c7d60 100644 --- a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md +++ b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md @@ -680,6 +680,35 @@ --- +## 26. relocation.order.list.query + +分页查询移机工单,对应旧系统 `POST /relocationOrder/pageList`。 + +- 分类:workorder | 类型:读 +- 数据来源:m2 `relocation_order`。 +- 入参:`relocationOrderNo`、`agentId`、`districtName`、`applyUserName`、 + `maintenanceUserName`、`cooperateUserName`、`orderState`,申请/审核/调度/完成日期范围及 `page/rows`。 +- 状态:0=待提交、1=待审核、2=被驳回、3=调度中、4=作业中、5=已完成、6=已取消。 +- 日期格式为 `yyyy-MM-dd`,结束日期含当天;分页默认 1/20、上限 100,固定按 ID 倒序。 +- 行级权限:总部看全部;代理身份按本代理、授权代管代理/物业过滤;销售非负责人按自己创建及代管代理物业过滤; + 流程参与者和 `install_order_user.type=2` 的移机/协同人员也可查看。 + +## 27. relocation.order.detail.query + +按 `id` 或 `relocationOrderNo` 查询详情,两者至少传一个;同时传入时必须匹配同一条可见记录。 + +- 行级权限与列表完全一致;不存在和无权统一返回 `NOT_FOUND_OR_FORBIDDEN`。 +- `record`:移机主单及状态文案。 +- `relocationDetails`:设备、源/目标位置、卫生间类型、执行状态和备注。 +- `maintenanceUsers/cooperateUsers`:当前移机和协同人员。 +- `processes`:`order_type=4`、`schedule_state=2` 且非抄送的流程,按操作时间、ID 正序,并附流程人员。 +- `completionCertificates`:结单流程的 `warehouse_in_out_pic` 凭证,限定 + `in_out_allocation_type=15`,仅返回 `id/fileName/bucket/objectKey`。 + +详细设计见 `relocation-order-mcp-tools-design.md`。 + +--- + ## 附录:在线查看工具清单 ```bash diff --git a/xiaoqu-agent-service/doc/relocation-order-mcp-tools-design.md b/xiaoqu-agent-service/doc/relocation-order-mcp-tools-design.md new file mode 100644 index 0000000..c3aeb5a --- /dev/null +++ b/xiaoqu-agent-service/doc/relocation-order-mcp-tools-design.md @@ -0,0 +1,55 @@ +# 移机工单 MCP 查询工具设计 + +对应 AI-Proj 需求:`REQ-20260917-0010`。 + +## 工具 + +- `relocation.order.list.query`:对齐老系统 `POST /relocationOrder/pageList`,提供稳定分页和结构化状态文案。 +- `relocation.order.detail.query`:按主键或移机工单号返回主单、移机明细、人员、已办流程和结单凭证。 + +两个工具均为 `workorder/read`,只读访问 m2 数据源。 + +## 数据来源 + +| 数据 | 表 | 关联方式 | +|---|---|---| +| 主单 | `relocation_order` | `id` / `order_no` | +| 移机明细 | `relocation_order_details` | `order_no` | +| 已办流程 | `order_process` | `order_no`,并限定 `order_type=4`、`schedule_state=2` | +| 当前及流程人员 | `install_order_user` | `install_order_no` / `install_order_process_id`,并限定 `type=2` | +| 结单凭证 | `warehouse_in_out_pic` | `aggregate_id=结单流程.id`、`in_out_allocation_type=15` | + +附件仅返回 `id/fileName/bucket/objectKey`,服务不生成公开 URL。 + +## 列表契约 + +支持工单号、所属代理、项目、申请人、移机人员、协同人员、状态及申请/审核/调度/完成日期范围。 +日期格式固定为 `yyyy-MM-dd`,结束日期按次日零点开区间实现“包含当天”。分页默认 `1/20`,每页上限 +100,固定按 `id DESC`,避免旧接口未显式排序导致翻页漂移。 + +状态:0 待提交、1 待审核、2 被驳回、3 调度中、4 作业中、5 已完成、6 已取消。 + +## 详情契约 + +`id` 与 `relocationOrderNo` 至少传一个;同时传入时作为同一主查询的 AND 条件,必须命中同一记录。 +不存在和无权访问均抛出 `NOT_FOUND_OR_FORBIDDEN`。 + +返回: + +- `record`:主单及状态文案; +- `relocationDetails`:设备、源/目标位置、卫生间类型、执行状态与备注; +- `maintenanceUsers/cooperateUsers`:当前人员; +- `processes`:非抄送的已办流程,按操作时间、ID 正序,并附流程对应人员; +- `completionCertificates`:所有结单流程关联凭证。 + +## 行级权限 + +列表和详情均复用 `PermissionService.applyRelocationOrderRowFilter`: + +- 总部身份查看全部; +- 普通代理查看本代理、授权代管代理、授权代管物业范围; +- 销售非负责人查看自己创建及授权代管代理名下物业; +- 当前用户参与过 `order_process` 且 `order_type=4` 的工单可见; +- 当前用户出现在 `install_order_user` 且 `type=2` 的工单可见,覆盖调度后尚未生成流程记录的作业人员。 + +代理身份未绑定代理商时失败关闭。 diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/OrderProcess.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/OrderProcess.java new file mode 100644 index 0000000..a39b76a --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/OrderProcess.java @@ -0,0 +1,37 @@ +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("order_process") +public class OrderProcess 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 orderNo; + 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; + private Integer orderType; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrder.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrder.java new file mode 100644 index 0000000..37620a6 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrder.java @@ -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.util.Date; + +/** 移机工单(m2,只读)。 */ +@Getter +@Setter +@ToString +@TableName("relocation_order") +public class RelocationOrder 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 applyAgentId; + private String applyAgentName; + private String orderNo; + private Long agentId; + private Long propertyId; + private Long districtId; + private String agentName; + private String propertyName; + private String districtName; + private String hopeRelocationDate; + private String trueRelocationDate; + private String relocationReason; + private String customerContact; + private String contactMobile; + private String projectAddress; + private String projectMemo; + private Integer orderState; + private Long applyUserId; + private String applyUserName; + private Date applyTime; + private Date auditTime; + private Date dispatchTime; + private Date finishTime; + private Date cancelTime; + private String auditUserName; + private String dispatchUserName; + private String maintenanceUserName; + private String cooperateUserName; + private String scheduleUserName; + private Integer hopeRelocationCount; + private Integer trueRelocationCount; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrderDetail.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrderDetail.java new file mode 100644 index 0000000..ecd4797 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/RelocationOrderDetail.java @@ -0,0 +1,40 @@ +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("relocation_order_details") +public class RelocationOrderDetail 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 orderNo; + private Integer deviceType; + private String snCode; + private Integer sourcePositionType; + private Long sourcePositionId; + private String sourcePosition; + private Integer sourceGender; + private String targetSourceSnCode; + private Integer targetPositionType; + private Long targetPositionId; + private String targetPosition; + private Integer targetGender; + private Integer state; + private String memo; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/OrderProcessMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/OrderProcessMapper.java new file mode 100644 index 0000000..b9458dc --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/OrderProcessMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m2; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m2.OrderProcess; + +public interface OrderProcessMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderDetailMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderDetailMapper.java new file mode 100644 index 0000000..f640573 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderDetailMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m2; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m2.RelocationOrderDetail; + +public interface RelocationOrderDetailMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderMapper.java new file mode 100644 index 0000000..d8c32d8 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/RelocationOrderMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m2; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m2.RelocationOrder; + +public interface RelocationOrderMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/RelocationOrderTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/RelocationOrderTools.java new file mode 100644 index 0000000..5eb2e27 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/RelocationOrderTools.java @@ -0,0 +1,406 @@ +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.InstallOrderUser; +import me.iiv.agent.entity.m2.OrderProcess; +import me.iiv.agent.entity.m2.RelocationOrder; +import me.iiv.agent.entity.m2.RelocationOrderDetail; +import me.iiv.agent.entity.m2.WarehouseInOutPic; +import me.iiv.agent.mapper.m2.InstallOrderUserMapper; +import me.iiv.agent.mapper.m2.OrderProcessMapper; +import me.iiv.agent.mapper.m2.RelocationOrderDetailMapper; +import me.iiv.agent.mapper.m2.RelocationOrderMapper; +import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper; +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.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** 移机工单查询工具,对齐老系统 /relocationOrder/pageList 与 /lookDetails。 */ +@Component +public class RelocationOrderTools implements ToolObject { + + private static final int DEFAULT_ROWS = 20; + private static final int MAX_ROWS = 100; + private static final int RELOCATION_ORDER_TYPE = 4; + private static final int RELOCATION_USER_TYPE = 2; + private static final int COMPLETION_AGGREGATE_TYPE = 15; + private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); + 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 ORDER_STATE_TEXT = Map.of( + 0, "待提交", 1, "待审核", 2, "被驳回", 3, "调度中", + 4, "作业中", 5, "已完成", 6, "已取消"); + private static final Map DEVICE_TYPE_TEXT = Map.of( + 0, "出纸机", 1, "空感", 2, "泡沫机"); + private static final Map POSITION_TYPE_TEXT = Map.of(0, "厕所", 1, "厕位"); + private static final Map GENDER_TEXT = Map.of( + 0, "男厕", 1, "女厕", 2, "母婴室", 3, "残障卫生间", + 4, "第三卫生间", 5, "其他"); + private static final Map DETAIL_STATE_TEXT = Map.of( + 0, "未执行", 1, "已执行", 2, "执行失败"); + private static final Map SCHEDULE_STATE_TEXT = Map.of( + 0, "无需待办", 1, "待办", 2, "已办"); + private static final Map AUDIT_STATE_TEXT = Map.of(0, "审核驳回", 1, "审核通过"); + private static final Map OPERATION_THEME_TEXT = Map.of( + 0, "移机申请", 1, "部门内审核", 2, "运营调度", + 3, "移机协同", 6, "结单", 7, "取消"); + + final RelocationOrderMapper orderMapper; + final RelocationOrderDetailMapper detailMapper; + final OrderProcessMapper processMapper; + final InstallOrderUserMapper userMapper; + final WarehouseInOutPicMapper picMapper; + final PermissionService permissionService; + + public RelocationOrderTools(RelocationOrderMapper orderMapper, + RelocationOrderDetailMapper detailMapper, + OrderProcessMapper processMapper, + InstallOrderUserMapper userMapper, + WarehouseInOutPicMapper picMapper, + PermissionService permissionService) { + this.orderMapper = orderMapper; + this.detailMapper = detailMapper; + this.processMapper = processMapper; + this.userMapper = userMapper; + this.picMapper = picMapper; + this.permissionService = permissionService; + } + + @XqTool(category = "workorder", kind = "read") + @Tool(name = "relocation.order.list.query", description = "分页查询当前用户可见的移机工单,对齐老系统 /relocationOrder/pageList。" + + "支持工单号、代理、项目、申请/移机/协同人员、状态及申请/审核/调度/完成日期范围。" + + "日期格式 yyyy-MM-dd,结束日期含当天;page 默认1,rows 默认20、上限100;固定按 id 倒序。") + public Map relocationOrderListQuery( + @ToolParam(required = false, description = "移机工单号,模糊匹配") String relocationOrderNo, + @ToolParam(required = false, description = "所属代理商 id,精确匹配") Long agentId, + @ToolParam(required = false, description = "项目名称,模糊匹配") String districtName, + @ToolParam(required = false, description = "申请人,模糊匹配") String applyUserName, + @ToolParam(required = false, description = "移机人员,模糊匹配") String maintenanceUserName, + @ToolParam(required = false, description = "协同人员,模糊匹配") String cooperateUserName, + @ToolParam(required = false, description = "状态:0待提交 1待审核 2被驳回 3调度中 4作业中 5已完成 6已取消") Integer orderState, + @ToolParam(required = false, description = "申请日期起,yyyy-MM-dd") String applyDateFrom, + @ToolParam(required = false, description = "申请日期止,yyyy-MM-dd,含当天") String applyDateTo, + @ToolParam(required = false, description = "审核日期起,yyyy-MM-dd") String auditDateFrom, + @ToolParam(required = false, description = "审核日期止,yyyy-MM-dd,含当天") String auditDateTo, + @ToolParam(required = false, description = "调度日期起,yyyy-MM-dd") String dispatchDateFrom, + @ToolParam(required = false, description = "调度日期止,yyyy-MM-dd,含当天") String dispatchDateTo, + @ToolParam(required = false, description = "完成日期起,yyyy-MM-dd") String finishDateFrom, + @ToolParam(required = false, description = "完成日期止,yyyy-MM-dd,含当天") String finishDateTo, + @ToolParam(required = false, description = "页码,从1开始,默认1") Integer page, + @ToolParam(required = false, description = "每页条数,默认20,上限100") Integer rows) { + validateCode(orderState, Set.of(0, 1, 2, 3, 4, 5, 6), "orderState"); + LocalDate applyFrom = parseDate(applyDateFrom, "applyDateFrom"); + LocalDate applyTo = parseDate(applyDateTo, "applyDateTo"); + LocalDate auditFrom = parseDate(auditDateFrom, "auditDateFrom"); + LocalDate auditTo = parseDate(auditDateTo, "auditDateTo"); + LocalDate dispatchFrom = parseDate(dispatchDateFrom, "dispatchDateFrom"); + LocalDate dispatchTo = parseDate(dispatchDateTo, "dispatchDateTo"); + LocalDate finishFrom = parseDate(finishDateFrom, "finishDateFrom"); + LocalDate finishTo = parseDate(finishDateTo, "finishDateTo"); + validateRange(applyFrom, applyTo, "applyDateFrom", "applyDateTo"); + validateRange(auditFrom, auditTo, "auditDateFrom", "auditDateTo"); + validateRange(dispatchFrom, dispatchTo, "dispatchDateFrom", "dispatchDateTo"); + validateRange(finishFrom, finishTo, "finishDateFrom", "finishDateTo"); + + 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 wrapper = new LambdaQueryWrapper() + .eq(RelocationOrder::getSoftDelete, 0) + .eq(agentId != null, RelocationOrder::getAgentId, agentId) + .eq(orderState != null, RelocationOrder::getOrderState, orderState) + .ge(applyFrom != null, RelocationOrder::getApplyTime, startOfDay(applyFrom)) + .lt(applyTo != null, RelocationOrder::getApplyTime, nextDayStart(applyTo)) + .ge(auditFrom != null, RelocationOrder::getAuditTime, startOfDay(auditFrom)) + .lt(auditTo != null, RelocationOrder::getAuditTime, nextDayStart(auditTo)) + .ge(dispatchFrom != null, RelocationOrder::getDispatchTime, startOfDay(dispatchFrom)) + .lt(dispatchTo != null, RelocationOrder::getDispatchTime, nextDayStart(dispatchTo)) + .ge(finishFrom != null, RelocationOrder::getFinishTime, startOfDay(finishFrom)) + .lt(finishTo != null, RelocationOrder::getFinishTime, nextDayStart(finishTo)); + like(wrapper, RelocationOrder::getOrderNo, relocationOrderNo); + like(wrapper, RelocationOrder::getDistrictName, districtName); + like(wrapper, RelocationOrder::getApplyUserName, applyUserName); + like(wrapper, RelocationOrder::getMaintenanceUserName, maintenanceUserName); + like(wrapper, RelocationOrder::getCooperateUserName, cooperateUserName); + permissionService.applyRelocationOrderRowFilter(wrapper, currentUser()); + + long total = orderMapper.selectCount(wrapper); + List orders = orderMapper.selectList(wrapper.orderByDesc(RelocationOrder::getId) + .last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize)); + Map 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", orders.stream().map(this::orderRecord).collect(Collectors.toList())); + return result; + } + + @XqTool(category = "workorder", kind = "read") + @Tool(name = "relocation.order.detail.query", description = "按 id 或移机工单号查询详情。" + + "两者至少传一个,同时传入时必须匹配同一条记录;行级权限与列表一致。" + + "不存在或无权查看统一返回 NOT_FOUND_OR_FORBIDDEN。返回主单、设备明细、人员、已办流程与结单凭证;附件仅返回 bucket/objectKey/fileName。") + public Map relocationOrderDetailQuery( + @ToolParam(required = false, description = "移机工单主键 id,与 relocationOrderNo 至少传一个") Long id, + @ToolParam(required = false, description = "移机工单号,与 id 至少传一个") String relocationOrderNo) { + if ((id == null || id <= 0) && !StringUtils.hasText(relocationOrderNo)) { + throw new IllegalArgumentException("id、relocationOrderNo 至少传一个"); + } + if (id != null && id <= 0) { + throw new IllegalArgumentException("id 必须为正整数"); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(RelocationOrder::getSoftDelete, 0) + .eq(id != null, RelocationOrder::getId, id) + .eq(StringUtils.hasText(relocationOrderNo), RelocationOrder::getOrderNo, + StringUtils.hasText(relocationOrderNo) ? relocationOrderNo.trim() : relocationOrderNo); + permissionService.applyRelocationOrderRowFilter(wrapper, currentUser()); + List visible = orderMapper.selectList( + wrapper.orderByDesc(RelocationOrder::getId).last("LIMIT 1")); + if (visible.isEmpty()) { + throw new ToolCallException(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, + "移机工单不存在或当前身份无权查看"); + } + + RelocationOrder order = visible.get(0); + String orderNo = order.getOrderNo(); + List details = detailMapper.selectList( + new LambdaQueryWrapper() + .eq(RelocationOrderDetail::getSoftDelete, 0) + .eq(RelocationOrderDetail::getOrderNo, orderNo) + .orderByAsc(RelocationOrderDetail::getId)); + List users = userMapper.selectList(new LambdaQueryWrapper() + .eq(InstallOrderUser::getSoftDelete, 0) + .eq(InstallOrderUser::getInstallOrderNo, orderNo) + .eq(InstallOrderUser::getType, RELOCATION_USER_TYPE) + .orderByAsc(InstallOrderUser::getId)); + List processes = processMapper.selectList(new LambdaQueryWrapper() + .eq(OrderProcess::getSoftDelete, 0) + .eq(OrderProcess::getOrderNo, orderNo) + .eq(OrderProcess::getOrderType, RELOCATION_ORDER_TYPE) + .eq(OrderProcess::getScheduleState, 2) + .ne(OrderProcess::getOperationTheme, 12) + .orderByAsc(OrderProcess::getOperationTime) + .orderByAsc(OrderProcess::getId)); + Map> processUsers = processUsers(processes); + List certificates = completionCertificates(processes); + + Map result = new LinkedHashMap<>(); + result.put("record", orderRecord(order)); + result.put("relocationDetails", details.stream().map(this::detailRecord).collect(Collectors.toList())); + result.put("maintenanceUsers", personnel(users, 0)); + 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("completionCertificates", certificates.stream().map(this::fileRecord) + .collect(Collectors.toList())); + return result; + } + + private Map> processUsers(List processes) { + if (processes.isEmpty()) return Map.of(); + List ids = processes.stream().map(OrderProcess::getId).collect(Collectors.toList()); + return userMapper.selectList(new LambdaQueryWrapper() + .eq(InstallOrderUser::getSoftDelete, 0) + .eq(InstallOrderUser::getType, RELOCATION_USER_TYPE) + .in(InstallOrderUser::getInstallOrderProcessId, ids) + .orderByAsc(InstallOrderUser::getId)).stream() + .collect(Collectors.groupingBy(InstallOrderUser::getInstallOrderProcessId, + LinkedHashMap::new, Collectors.toList())); + } + + private List completionCertificates(List processes) { + List completionIds = processes.stream() + .filter(process -> Integer.valueOf(6).equals(process.getOperationTheme())) + .map(OrderProcess::getId).collect(Collectors.toList()); + if (completionIds.isEmpty()) return List.of(); + return picMapper.selectList(new LambdaQueryWrapper() + .eq(WarehouseInOutPic::getSoftDelete, 0) + .in(WarehouseInOutPic::getAggregateId, completionIds) + .eq(WarehouseInOutPic::getInOutAllocationType, COMPLETION_AGGREGATE_TYPE) + .orderByAsc(WarehouseInOutPic::getId)); + } + + private Map orderRecord(RelocationOrder order) { + Map record = new LinkedHashMap<>(); + record.put("id", order.getId()); + record.put("relocationOrderNo", order.getOrderNo()); + record.put("orderState", order.getOrderState()); + record.put("orderStateText", text(ORDER_STATE_TEXT, order.getOrderState())); + record.put("applyAgentId", order.getApplyAgentId()); + record.put("applyAgentName", order.getApplyAgentName()); + record.put("agentId", order.getAgentId()); + record.put("agentName", order.getAgentName()); + record.put("propertyId", order.getPropertyId()); + record.put("propertyName", order.getPropertyName()); + record.put("districtId", order.getDistrictId()); + record.put("districtName", order.getDistrictName()); + record.put("hopeRelocationDate", order.getHopeRelocationDate()); + record.put("trueRelocationDate", order.getTrueRelocationDate()); + record.put("relocationReason", order.getRelocationReason()); + record.put("customerContact", order.getCustomerContact()); + record.put("contactMobile", order.getContactMobile()); + record.put("projectAddress", order.getProjectAddress()); + record.put("projectMemo", order.getProjectMemo()); + record.put("applyUserId", order.getApplyUserId()); + record.put("applyUserName", order.getApplyUserName()); + record.put("auditUserName", order.getAuditUserName()); + record.put("dispatchUserName", order.getDispatchUserName()); + record.put("maintenanceUserName", order.getMaintenanceUserName()); + record.put("cooperateUserName", order.getCooperateUserName()); + record.put("scheduleUserName", order.getScheduleUserName()); + record.put("hopeRelocationCount", order.getHopeRelocationCount()); + record.put("trueRelocationCount", order.getTrueRelocationCount()); + record.put("applyTime", formatTime(order.getApplyTime())); + record.put("auditTime", formatTime(order.getAuditTime())); + record.put("dispatchTime", formatTime(order.getDispatchTime())); + record.put("finishTime", formatTime(order.getFinishTime())); + record.put("cancelTime", formatTime(order.getCancelTime())); + record.put("createTime", formatTime(order.getCreateTime())); + record.put("modifyTime", formatTime(order.getModifyTime())); + return record; + } + + private Map detailRecord(RelocationOrderDetail detail) { + Map record = new LinkedHashMap<>(); + record.put("id", detail.getId()); + record.put("deviceType", detail.getDeviceType()); + record.put("deviceTypeText", text(DEVICE_TYPE_TEXT, detail.getDeviceType())); + record.put("snCode", detail.getSnCode()); + record.put("sourcePositionType", detail.getSourcePositionType()); + record.put("sourcePositionTypeText", text(POSITION_TYPE_TEXT, detail.getSourcePositionType())); + record.put("sourcePositionId", detail.getSourcePositionId()); + record.put("sourcePosition", detail.getSourcePosition()); + record.put("sourceGender", detail.getSourceGender()); + record.put("sourceGenderText", text(GENDER_TEXT, detail.getSourceGender())); + record.put("targetSourceSnCode", detail.getTargetSourceSnCode()); + record.put("targetPositionType", detail.getTargetPositionType()); + record.put("targetPositionTypeText", text(POSITION_TYPE_TEXT, detail.getTargetPositionType())); + record.put("targetPositionId", detail.getTargetPositionId()); + record.put("targetPosition", detail.getTargetPosition()); + record.put("targetGender", detail.getTargetGender()); + record.put("targetGenderText", text(GENDER_TEXT, detail.getTargetGender())); + record.put("state", detail.getState()); + record.put("stateText", text(DETAIL_STATE_TEXT, detail.getState())); + record.put("memo", detail.getMemo()); + return record; + } + + private Map processRecord(OrderProcess process, List users) { + Map record = new LinkedHashMap<>(); + record.put("id", process.getId()); + record.put("userId", process.getUserId()); + record.put("userName", process.getUserName()); + record.put("operationTime", formatTime(process.getOperationTime())); + record.put("scheduleState", process.getScheduleState()); + record.put("scheduleStateText", text(SCHEDULE_STATE_TEXT, process.getScheduleState())); + record.put("auditState", process.getAuditState()); + record.put("auditStateText", text(AUDIT_STATE_TEXT, process.getAuditState())); + record.put("auditLevel", process.getAuditLevel()); + record.put("operationTheme", process.getOperationTheme()); + record.put("operationThemeText", text(OPERATION_THEME_TEXT, process.getOperationTheme())); + record.put("operationType", process.getOperationType()); + record.put("memo", process.getMemo()); + record.put("maintenanceUsers", personnel(users, 0)); + record.put("cooperateUsers", personnel(users, 1)); + return record; + } + + private List> personnel(List users, int userType) { + return users.stream().filter(user -> Integer.valueOf(userType).equals(user.getUserType())) + .map(user -> { + Map record = new LinkedHashMap<>(); + record.put("id", user.getId()); + record.put("userId", user.getUserId()); + record.put("userName", user.getUserName()); + record.put("userType", user.getUserType()); + return record; + }).collect(Collectors.toList()); + } + + private Map fileRecord(WarehouseInOutPic pic) { + Map record = new LinkedHashMap<>(); + record.put("id", pic.getId()); + record.put("processId", pic.getAggregateId()); + record.put("fileName", pic.getFileName()); + record.put("bucket", pic.getOssbucketname()); + record.put("objectKey", nullToEmpty(pic.getOssfolder()) + nullToEmpty(pic.getFileName())); + return record; + } + + private static User currentUser() { + return AuthContextHolder.get().getUser(); + } + + private static void like(LambdaQueryWrapper wrapper, + SFunction column, String value) { + if (StringUtils.hasText(value)) wrapper.like(column, value.trim()); + } + + private static void validateCode(Integer value, Set accepted, String name) { + if (value != null && !accepted.contains(value)) { + throw new IllegalArgumentException(name + " 取值不合法"); + } + } + + private static LocalDate parseDate(String value, String name) { + if (!StringUtils.hasText(value)) return null; + try { + return LocalDate.parse(value.trim(), DATE_FORMATTER); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd"); + } + } + + private static void validateRange(LocalDate from, LocalDate to, String fromName, String toName) { + if (from != null && to != null && from.isAfter(to)) { + throw new IllegalArgumentException(fromName + " 不能晚于 " + toName); + } + } + + 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 String text(Map values, Integer code) { + return code == null ? null : values.getOrDefault(code, "未知"); + } + + private static String formatTime(Date value) { + return value == null ? null : TIME_FORMATTER.format(value.toInstant()); + } + + private static String nullToEmpty(String value) { + return value == null ? "" : value; + } +} 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 a1c1193..857a7b3 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 @@ -13,6 +13,7 @@ import me.iiv.agent.entity.m2.ContractNew; import me.iiv.agent.entity.m2.DeliveryOrder; import me.iiv.agent.entity.m2.InstallOrder; import me.iiv.agent.entity.m2.PickupOrder; +import me.iiv.agent.entity.m2.RelocationOrder; import me.iiv.agent.entity.m2.WarehouseAgentAuth; import me.iiv.agent.entity.m2.WarehouseIn; import me.iiv.agent.entity.m2.WarehouseOut; @@ -275,6 +276,64 @@ public class PermissionService { }); } + /** + * 移机工单权限:总部看全部;代理用户按自己/授权代管代理、代管物业过滤, + * 销售非负责人按自己创建和代管代理物业过滤。流程参与者及 type=2 的移机/协同人员始终可见。 + */ + public void applyRelocationOrderRowFilter(LambdaQueryWrapper wrapper, User user) { + 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())); + String processParticipant = "EXISTS (SELECT 1 FROM order_process op " + + "WHERE op.order_no = relocation_order.order_no AND op.soft_delete = 0 " + + "AND op.order_type = 4 AND op.user_id = {0})"; + String workerParticipant = "EXISTS (SELECT 1 FROM install_order_user iou " + + "WHERE iou.install_order_no = relocation_order.order_no AND iou.soft_delete = 0 " + + "AND iou.type = 2 AND iou.user_id = {0})"; + + 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())); + } + List visiblePropertyIds = propertyIds.isEmpty() + ? List.of(0L) : new ArrayList<>(new LinkedHashSet<>(propertyIds)); + wrapper.and(w -> w.in(RelocationOrder::getPropertyId, visiblePropertyIds) + .or().apply(processParticipant, user.getId()) + .or().apply(workerParticipant, user.getId())); + return; + } + + List managedPropertyIds = managedPropertyIds(agentIds); + wrapper.and(w -> { + w.in(RelocationOrder::getApplyAgentId, agentIds) + .or().in(RelocationOrder::getAgentId, agentIds); + if (!managedPropertyIds.isEmpty()) { + w.or().in(RelocationOrder::getPropertyId, managedPropertyIds); + } + w.or().apply(processParticipant, user.getId()) + .or().apply(workerParticipant, user.getId()); + }); + } + /** * 装机工单权限,1:1 对齐老 getWorkOrderAuth(type=0) 的有效业务口径:总部看全部; * 销售非负责人只看自己创建物业及授权代管公司的物业;其他代理用户可看自己、 diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/RelocationOrderToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/RelocationOrderToolsTest.java new file mode 100644 index 0000000..653d452 --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/RelocationOrderToolsTest.java @@ -0,0 +1,189 @@ +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.InstallOrderUser; +import me.iiv.agent.entity.m2.OrderProcess; +import me.iiv.agent.entity.m2.RelocationOrder; +import me.iiv.agent.entity.m2.RelocationOrderDetail; +import me.iiv.agent.entity.m2.WarehouseInOutPic; +import me.iiv.agent.mapper.m2.InstallOrderUserMapper; +import me.iiv.agent.mapper.m2.OrderProcessMapper; +import me.iiv.agent.mapper.m2.RelocationOrderDetailMapper; +import me.iiv.agent.mapper.m2.RelocationOrderMapper; +import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper; +import me.iiv.agent.mcp.McpErrorCode; +import me.iiv.agent.mcp.ToolCallException; +import me.iiv.agent.permission.PermissionService; +import me.iiv.agent.security.AuthContext; +import me.iiv.agent.security.AuthContextHolder; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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 RelocationOrderToolsTest { + + @Mock RelocationOrderMapper orderMapper; + @Mock RelocationOrderDetailMapper detailMapper; + @Mock OrderProcessMapper processMapper; + @Mock InstallOrderUserMapper userMapper; + @Mock WarehouseInOutPicMapper picMapper; + @Mock PermissionService permissionService; + + RelocationOrderTools tools; + + @BeforeEach + void setUp() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, RelocationOrder.class); + TableInfoHelper.initTableInfo(assistant, RelocationOrderDetail.class); + TableInfoHelper.initTableInfo(assistant, OrderProcess.class); + TableInfoHelper.initTableInfo(assistant, InstallOrderUser.class); + TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class); + tools = new RelocationOrderTools(orderMapper, detailMapper, processMapper, userMapper, + picMapper, permissionService); + User user = new User(); + user.setId(7L); + AuthContextHolder.set(new AuthContext(null, user)); + } + + @AfterEach + void tearDown() { + AuthContextHolder.clear(); + } + + @Test + void listReturnsStateTextAndClampsRows() { + when(orderMapper.selectCount(any())).thenReturn(1L); + when(orderMapper.selectList(any())).thenReturn(List.of(order())); + + Map result = tools.relocationOrderListQuery("YJ", 10L, "项目", "申请人", + "移机员", "协同员", 5, "2026-09-01", "2026-09-30", + null, null, null, null, null, null, 0, 101); + + assertEquals(1L, result.get("total")); + assertEquals(1, result.get("page")); + assertEquals(100, result.get("rows")); + @SuppressWarnings("unchecked") + Map record = ((List>) result.get("records")).get(0); + assertEquals("已完成", record.get("orderStateText")); + verify(permissionService).applyRelocationOrderRowFilter(any(), any()); + } + + @Test + void listRejectsInvalidStateAndDateRangeBeforeQuery() { + assertThrows(IllegalArgumentException.class, () -> tools.relocationOrderListQuery(null, null, + null, null, null, null, 9, null, null, null, null, null, null, null, null, null, null)); + assertThrows(IllegalArgumentException.class, () -> tools.relocationOrderListQuery(null, null, + null, null, null, null, null, "2026-09-30", "2026-09-01", null, null, + null, null, null, null, null, null)); + verifyNoInteractions(orderMapper, permissionService); + } + + @Test + void detailUsesSamePermissionAndHidesExistence() { + when(orderMapper.selectList(any())).thenReturn(List.of()); + + ToolCallException error = assertThrows(ToolCallException.class, + () -> tools.relocationOrderDetailQuery(20L, null)); + + assertEquals(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, error.getCode()); + verify(permissionService).applyRelocationOrderRowFilter(any(), any()); + verifyNoInteractions(detailMapper, processMapper, userMapper, picMapper); + } + + @Test + void detailReturnsDetailsPersonnelProcessesAndCertificates() { + when(orderMapper.selectList(any())).thenReturn(List.of(order())); + RelocationOrderDetail detail = new RelocationOrderDetail(); + detail.setId(3L); + detail.setDeviceType(0); + detail.setSourceGender(0); + detail.setTargetGender(1); + detail.setState(1); + when(detailMapper.selectList(any())).thenReturn(List.of(detail)); + + InstallOrderUser worker = new InstallOrderUser(); + worker.setId(4L); + worker.setUserId(40L); + worker.setUserName("移机员"); + worker.setUserType(0); + worker.setType(2); + worker.setInstallOrderProcessId(5L); + InstallOrderUser collaborator = new InstallOrderUser(); + collaborator.setId(6L); + collaborator.setUserName("协同员"); + collaborator.setUserType(1); + collaborator.setType(2); + collaborator.setInstallOrderProcessId(5L); + when(userMapper.selectList(any())).thenReturn(List.of(worker, collaborator), + List.of(worker, collaborator)); + + OrderProcess process = new OrderProcess(); + process.setId(5L); + process.setOperationTheme(6); + process.setScheduleState(2); + process.setOrderType(4); + when(processMapper.selectList(any())).thenReturn(List.of(process)); + WarehouseInOutPic pic = new WarehouseInOutPic(); + pic.setId(7L); + pic.setAggregateId(5L); + pic.setFileName("finish.jpg"); + pic.setOssbucketname("bucket-a"); + pic.setOssfolder("relocation/"); + pic.setInOutAllocationType(15); + when(picMapper.selectList(any())).thenReturn(List.of(pic)); + + Map result = tools.relocationOrderDetailQuery(20L, "YJ-001"); + + assertEquals(1, ((List) result.get("relocationDetails")).size()); + assertEquals(1, ((List) result.get("maintenanceUsers")).size()); + assertEquals(1, ((List) result.get("cooperateUsers")).size()); + @SuppressWarnings("unchecked") + Map processResult = ((List>) result.get("processes")).get(0); + assertEquals("结单", processResult.get("operationThemeText")); + @SuppressWarnings("unchecked") + Map certificate = ((List>) result.get("completionCertificates")).get(0); + assertEquals("relocation/finish.jpg", certificate.get("objectKey")); + } + + @Test + void springAiPublishesBothSchemas() { + var callbacks = org.springframework.ai.tool.method.MethodToolCallbackProvider.builder() + .toolObjects(tools).build().getToolCallbacks(); + Map 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("relocation.order.list.query").contains("finishDateTo")); + assertTrue(schemas.get("relocation.order.detail.query").contains("relocationOrderNo")); + } + + private static RelocationOrder order() { + RelocationOrder order = new RelocationOrder(); + order.setId(20L); + order.setOrderNo("YJ-001"); + order.setOrderState(5); + order.setTrueRelocationCount(1); + return order; + } +} diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceRelocationOrderRowFilterTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceRelocationOrderRowFilterTest.java new file mode 100644 index 0000000..5f8db40 --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceRelocationOrderRowFilterTest.java @@ -0,0 +1,126 @@ +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.AgentManageDistrictAuth; +import me.iiv.agent.entity.m0.Authority; +import me.iiv.agent.entity.m0.Properyt; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.entity.m2.RelocationOrder; +import me.iiv.agent.entity.m2.WarehouseAgentAuth; +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.HashOperations; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PermissionServiceRelocationOrderRowFilterTest { + @Mock AuthorityMapper authorityMapper; + @Mock AgentMapper agentMapper; + @Mock WarehouseAgentAuthMapper warehouseAgentAuthMapper; + @Mock AgentManageDistrictAuthMapper agentManageDistrictAuthMapper; + @Mock ProperytMapper properytMapper; + @Mock StringRedisTemplate redisTemplate; + @Mock HashOperations hashOperations; + PermissionService service; + + @BeforeEach + void setUp() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, RelocationOrder.class); + TableInfoHelper.initTableInfo(assistant, Properyt.class); + service = new PermissionService(authorityMapper, agentMapper, warehouseAgentAuthMapper, + agentManageDistrictAuthMapper, properytMapper, redisTemplate); + } + + @Test + void headquartersAddsNoFilter() { + when(authorityMapper.selectById(9L)).thenReturn(authority(0)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyRelocationOrderRowFilter(wrapper, user(null)); + + assertTrue(wrapper.getSqlSegment().isBlank()); + } + + @Test + void agentCombinesAgentsManagedPropertyProcessAndWorker() { + when(authorityMapper.selectById(9L)).thenReturn(authority(1)); + WarehouseAgentAuth managed = new WarehouseAgentAuth(); + managed.setManagerAgentId(12L); + when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managed)); + AgentManageDistrictAuth property = new AgentManageDistrictAuth(); + property.setPropertyId(30L); + when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of(property)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyRelocationOrderRowFilter(wrapper, user(10L)); + + String sql = wrapper.getSqlSegment(); + Collection values = wrapper.getParamNameValuePairs().values(); + assertTrue(sql.contains("apply_agent_id IN")); + assertTrue(sql.contains("agent_id IN")); + assertTrue(sql.contains("property_id IN")); + assertTrue(sql.contains("order_process")); + assertTrue(sql.contains("order_type = 4")); + assertTrue(sql.contains("install_order_user")); + assertTrue(sql.contains("type = 2")); + assertTrue(values.containsAll(List.of(7L, 10L, 12L, 30L))); + } + + @Test + void salesNonLeaderStillAllowsParticipants() { + User user = user(10L); + user.setDeptId(6L); + user.setPostId(8L); + when(authorityMapper.selectById(9L)).thenReturn(authority(1)); + when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of()); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",6,"); + when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn(",99,"); + Properyt own = new Properyt(); + own.setId(21L); + when(properytMapper.selectList(any())).thenReturn(List.of(own)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyRelocationOrderRowFilter(wrapper, user); + + String sql = wrapper.getSqlSegment(); + assertTrue(sql.contains("property_id IN")); + assertTrue(sql.contains("order_process")); + assertTrue(sql.contains("install_order_user")); + } + + private static User user(Long agentId) { + User user = new User(); + user.setId(7L); + user.setAuthorityId(9L); + user.setAgentId(agentId); + return user; + } + + private static Authority authority(int isAgent) { + Authority authority = new Authority(); + authority.setId(9L); + authority.setIsagent(isAgent); + authority.setSoftDelete(0); + return authority; + } +} -- 2.54.0