[REQ-20260917-0010] 移机工单 MCP 查询工具(列表+详情) (#20)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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` 的工单可见,覆盖调度后尚未生成流程记录的作业人员。
|
||||
|
||||
代理身份未绑定代理商时失败关闭。
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<OrderProcess> {
|
||||
}
|
||||
+7
@@ -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<RelocationOrderDetail> {
|
||||
}
|
||||
@@ -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<RelocationOrder> {
|
||||
}
|
||||
@@ -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<Integer, String> ORDER_STATE_TEXT = Map.of(
|
||||
0, "待提交", 1, "待审核", 2, "被驳回", 3, "调度中",
|
||||
4, "作业中", 5, "已完成", 6, "已取消");
|
||||
private static final Map<Integer, String> DEVICE_TYPE_TEXT = Map.of(
|
||||
0, "出纸机", 1, "空感", 2, "泡沫机");
|
||||
private static final Map<Integer, String> POSITION_TYPE_TEXT = Map.of(0, "厕所", 1, "厕位");
|
||||
private static final Map<Integer, String> GENDER_TEXT = Map.of(
|
||||
0, "男厕", 1, "女厕", 2, "母婴室", 3, "残障卫生间",
|
||||
4, "第三卫生间", 5, "其他");
|
||||
private static final Map<Integer, String> DETAIL_STATE_TEXT = Map.of(
|
||||
0, "未执行", 1, "已执行", 2, "执行失败");
|
||||
private static final Map<Integer, String> SCHEDULE_STATE_TEXT = Map.of(
|
||||
0, "无需待办", 1, "待办", 2, "已办");
|
||||
private static final Map<Integer, String> AUDIT_STATE_TEXT = Map.of(0, "审核驳回", 1, "审核通过");
|
||||
private static final Map<Integer, String> OPERATION_THEME_TEXT = Map.of(
|
||||
0, "移机申请", 1, "部门内审核", 2, "运营调度",
|
||||
3, "移机协同", 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<String, Object> 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<RelocationOrder> wrapper = new LambdaQueryWrapper<RelocationOrder>()
|
||||
.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<RelocationOrder> orders = orderMapper.selectList(wrapper.orderByDesc(RelocationOrder::getId)
|
||||
.last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize));
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("total", total);
|
||||
result.put("page", pageNo);
|
||||
result.put("rows", pageSize);
|
||||
if (truncated) {
|
||||
result.put("notice", "rows 超过上限 100,已截断为 100");
|
||||
}
|
||||
result.put("records", 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<String, Object> 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<RelocationOrder> wrapper = new LambdaQueryWrapper<RelocationOrder>()
|
||||
.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<RelocationOrder> 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<RelocationOrderDetail> details = detailMapper.selectList(
|
||||
new LambdaQueryWrapper<RelocationOrderDetail>()
|
||||
.eq(RelocationOrderDetail::getSoftDelete, 0)
|
||||
.eq(RelocationOrderDetail::getOrderNo, orderNo)
|
||||
.orderByAsc(RelocationOrderDetail::getId));
|
||||
List<InstallOrderUser> users = userMapper.selectList(new LambdaQueryWrapper<InstallOrderUser>()
|
||||
.eq(InstallOrderUser::getSoftDelete, 0)
|
||||
.eq(InstallOrderUser::getInstallOrderNo, orderNo)
|
||||
.eq(InstallOrderUser::getType, RELOCATION_USER_TYPE)
|
||||
.orderByAsc(InstallOrderUser::getId));
|
||||
List<OrderProcess> processes = processMapper.selectList(new LambdaQueryWrapper<OrderProcess>()
|
||||
.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<Long, List<InstallOrderUser>> processUsers = processUsers(processes);
|
||||
List<WarehouseInOutPic> certificates = completionCertificates(processes);
|
||||
|
||||
Map<String, Object> 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<Long, List<InstallOrderUser>> processUsers(List<OrderProcess> processes) {
|
||||
if (processes.isEmpty()) return Map.of();
|
||||
List<Long> ids = processes.stream().map(OrderProcess::getId).collect(Collectors.toList());
|
||||
return userMapper.selectList(new LambdaQueryWrapper<InstallOrderUser>()
|
||||
.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<WarehouseInOutPic> completionCertificates(List<OrderProcess> processes) {
|
||||
List<Long> 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<WarehouseInOutPic>()
|
||||
.eq(WarehouseInOutPic::getSoftDelete, 0)
|
||||
.in(WarehouseInOutPic::getAggregateId, completionIds)
|
||||
.eq(WarehouseInOutPic::getInOutAllocationType, COMPLETION_AGGREGATE_TYPE)
|
||||
.orderByAsc(WarehouseInOutPic::getId));
|
||||
}
|
||||
|
||||
private Map<String, Object> orderRecord(RelocationOrder order) {
|
||||
Map<String, Object> 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<String, Object> detailRecord(RelocationOrderDetail detail) {
|
||||
Map<String, Object> 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<String, Object> processRecord(OrderProcess process, List<InstallOrderUser> users) {
|
||||
Map<String, Object> 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<Map<String, Object>> personnel(List<InstallOrderUser> users, int userType) {
|
||||
return users.stream().filter(user -> Integer.valueOf(userType).equals(user.getUserType()))
|
||||
.map(user -> {
|
||||
Map<String, Object> 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<String, Object> fileRecord(WarehouseInOutPic pic) {
|
||||
Map<String, Object> 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<RelocationOrder> wrapper,
|
||||
SFunction<RelocationOrder, ?> column, String value) {
|
||||
if (StringUtils.hasText(value)) wrapper.like(column, value.trim());
|
||||
}
|
||||
|
||||
private static void validateCode(Integer value, Set<Integer> 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<Integer, String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<RelocationOrder> wrapper, User user) {
|
||||
if (!isAgentAuthority(user.getAuthorityId())) {
|
||||
return;
|
||||
}
|
||||
if (user.getAgentId() == null) {
|
||||
throw new IllegalStateException("代理商用户未关联代理商,拒绝访问业务数据");
|
||||
}
|
||||
List<WarehouseAgentAuth> auths = managedAuths(user.getId());
|
||||
List<Long> 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<Long> propertyIds = properytMapper.selectList(new LambdaQueryWrapper<Properyt>()
|
||||
.eq(Properyt::getSoftDelete, 0)
|
||||
.eq(Properyt::getCreateId, user.getId())
|
||||
.select(Properyt::getId))
|
||||
.stream().map(Properyt::getId).collect(Collectors.toList());
|
||||
List<Long> managedAgentIds = auths.stream().map(WarehouseAgentAuth::getManagerAgentId)
|
||||
.collect(Collectors.toList());
|
||||
if (!managedAgentIds.isEmpty()) {
|
||||
propertyIds.addAll(properytMapper.selectList(new LambdaQueryWrapper<Properyt>()
|
||||
.eq(Properyt::getSoftDelete, 0)
|
||||
.in(Properyt::getAgentId, managedAgentIds)
|
||||
.select(Properyt::getId))
|
||||
.stream().map(Properyt::getId).collect(Collectors.toList()));
|
||||
}
|
||||
List<Long> 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<Long> 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) 的有效业务口径:总部看全部;
|
||||
* 销售非负责人只看自己创建物业及授权代管公司的物业;其他代理用户可看自己、
|
||||
|
||||
+189
@@ -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<String, Object> 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<String, Object> record = ((List<Map<String, Object>>) 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<String, Object> 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<String, Object> processResult = ((List<Map<String, Object>>) result.get("processes")).get(0);
|
||||
assertEquals("结单", processResult.get("operationThemeText"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> certificate = ((List<Map<String, Object>>) 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<String, String> schemas = Arrays.stream(callbacks).collect(java.util.stream.Collectors.toMap(
|
||||
callback -> callback.getToolDefinition().name(),
|
||||
callback -> callback.getToolDefinition().inputSchema()));
|
||||
|
||||
assertEquals(2, schemas.size());
|
||||
assertTrue(schemas.get("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;
|
||||
}
|
||||
}
|
||||
+126
@@ -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<String, Object, Object> 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<RelocationOrder> 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<RelocationOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyRelocationOrderRowFilter(wrapper, user(10L));
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
Collection<Object> 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<RelocationOrder> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user