[REQ-20260918-0004] 撤机工单 MCP 查询工具(列表+详情)
- withdraw.order.list.query:对齐 /withdrawOrder/pageList,21 个可选参数(类型/状态白名单、 撤机单号/取货单号、代理/项目/三人员、申请/审核/调度/取货/完成 5 组日期范围、分页截断 notice) - withdraw.order.detail.query:主单+取回明细(in_count>0)+运维/协同人员+已办流程(order_type=1 排除抄送)+凭证附件(bucket/objectKey 三元组),不存在/无权统一 NOT_FOUND_OR_FORBIDDEN - 行级权限 applyWithdrawOrderRowFilter 1:1 复刻 getWorkOrderAuth(type=3), 补齐老系统 lookDetails 缺失的详情权限校验 - 修复老系统 finishDate 过滤误生成 finish_date 列的 bug,显式映射 finish_time - 新增 WithdrawOrder/WithdrawOrderDetails 实体与空 mapper,测试 10 个新用例 - 文档:agent-mcp-tools-reference §28/29 + withdraw-order-mcp-tools-design.md 单测全量 281/281 通过
This commit is contained in:
@@ -707,6 +707,34 @@
|
||||
|
||||
详细设计见 `relocation-order-mcp-tools-design.md`。
|
||||
|
||||
## 28. withdraw.order.list.query
|
||||
|
||||
分页查询撤机工单,对应旧系统 `POST /withdrawOrder/pageList`。
|
||||
|
||||
- 分类:workorder | 类型:读
|
||||
- 数据来源:m2 `withdraw_order`。
|
||||
- 入参:`type`、`orderState`、`withdrawOrderNo`、`pickupOrderNo`、`agentId`、`districtName`、
|
||||
`applyUserName`、`maintenanceUserName`、`cooperateUserName`,申请/审核/调度/取货/完成日期范围及 `page/rows`。
|
||||
- 类型:0=部分撤机、1=全部撤机;状态:0=待审核、1=被驳回、2=调度中、3=作业中、4=待取回、5=已完成、6=已取消。
|
||||
- 日期格式为 `yyyy-MM-dd`,结束日期含当天;分页默认 1/20、上限 100,固定按 ID 倒序。
|
||||
老系统 `finishDate` 过滤误生成不存在的 `finish_date` 列(查询报错),MCP 侧已修正映射到 `finish_time`。
|
||||
- 行级权限:总部看全部;代理身份按申请代理/所属代理(本代理+授权代管)、授权代管物业过滤,
|
||||
且本人出现在 `order_process(order_type=1)` 的工单可见;销售非负责人按自己创建及代管代理物业过滤。
|
||||
|
||||
## 29. withdraw.order.detail.query
|
||||
|
||||
按 `id` 或 `withdrawOrderNo` 查询详情,两者至少传一个;同时传入时必须匹配同一条可见记录。
|
||||
|
||||
- 行级权限与列表完全一致(老系统 lookDetails 无权限校验,MCP 侧已补齐);不存在和无权统一返回 `NOT_FOUND_OR_FORBIDDEN`。
|
||||
- `record`:撤机主单(期望/实际撤机日期、撤机理由、联系人、项目地址等)及状态/类型文案。
|
||||
- `withdrawDetails`:`withdraw_order_details` 中 `in_count>0` 的实际取回物品明细。
|
||||
- `maintenanceUsers/cooperateUsers`:`install_order_user type=1` 的运维/协同人员。
|
||||
- `processes`:`order_type=1`、`schedule_state=2` 且非抄送(theme=12)的流程,按操作时间、ID 正序,主题/类型给 code+文案。
|
||||
- `certificates`:流程凭证附件,限定 `in_out_allocation_type=12`,仅返回 `id/fileName/bucket/objectKey`。
|
||||
- 撤机结单后自动生成取货工单(`pickup_order.type=2`),可用 `pickup.order.list.query` 的 `withdrawOrderNo` 参数串联查取货/入库进度。
|
||||
|
||||
详细设计见 `withdraw-order-mcp-tools-design.md`。
|
||||
|
||||
---
|
||||
|
||||
## 附录:在线查看工具清单
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# 撤机工单 MCP 工具设计(withdraw.order.list.query / withdraw.order.detail.query)
|
||||
|
||||
- 需求:撤机工单查询工具(对齐老系统 `/withdrawOrder/pageList`、`/withdrawOrder/lookDetails`)
|
||||
- 日期:2026-09-18
|
||||
- 状态:已实现,单测 281/281 通过
|
||||
- 分类:workorder | 类型:读
|
||||
|
||||
## 1. 老系统语义来源
|
||||
|
||||
老系统撤机模块(`WithdrawOrderController`,2025-09 新增):
|
||||
|
||||
- 主表 `withdraw_order`(m2,单号 CJGD 前缀)+ `withdraw_order_details`
|
||||
- 流程走**共享** `order_process`(`order_type=1` 区分撤机)
|
||||
- 作业人员走共享 `install_order_user`(`type=1`)
|
||||
- 状态机 `orderState`:0待审核 / 1被驳回 / 2调度中 / 3作业中 / 4待取回 / 5已完成 / 6已取消
|
||||
- `type`:0部分撤机 / 1全部撤机
|
||||
- 业务链路:申请 → 调度(通过→作业中)→ 结单 → 自动生成取货工单(`pickup_order.type=2`,
|
||||
`withdraw_order_no` 回填)→ 取货入库自动生成入库单
|
||||
|
||||
### 已知老系统缺陷(MCP 侧修正)
|
||||
|
||||
- `pageList` 按 `finishDateStart/End` 过滤时经 `beanToWrapper` 生成不存在的列 `finish_date`,
|
||||
实体列实为 `finish_time`——该过滤在老系统会直接 SQL 报错。MCP 侧显式映射到 `finish_time`。
|
||||
- `lookDetails` 详情接口**没有**权限校验(只按单号查)——MCP 侧补齐行级过滤(见 §4)。
|
||||
- 老系统列表对人名空值补 `-`,MCP 侧不补(保持 null,交由 agent 判断)。
|
||||
|
||||
## 2. 工具设计
|
||||
|
||||
### 2.1 withdraw.order.list.query
|
||||
|
||||
入参全部可选,风格对齐 pickup/relocation:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|---|---|
|
||||
| type | 0部分撤机 / 1全部撤机(白名单校验) |
|
||||
| orderState | 7 值白名单,非法拒绝 |
|
||||
| withdrawOrderNo / pickupOrderNo | 撤机单号 / 关联取货单号,模糊 |
|
||||
| agentId / districtName | 所属公司 id 精确 / 项目名称模糊 |
|
||||
| applyUserName / maintenanceUserName / cooperateUserName | 申请人/运维/协同,模糊 |
|
||||
| applyDateFrom/To ~ finishDateFrom/To | 5 组日期范围(申请/审核/调度/取货/完成),yyyy-MM-dd,结束含当天 |
|
||||
| page / rows | 默认 1/20,rows>100 截断为 100 并返回 notice |
|
||||
|
||||
返回:`total/page/rows/[notice]/records`,records 同时给 `orderState+orderStateText`、`type+typeText`;
|
||||
排序固定 `id DESC`。
|
||||
|
||||
### 2.2 withdraw.order.detail.query
|
||||
|
||||
入参 `(id, withdrawOrderNo)` 二选一;返回信息组:
|
||||
|
||||
- `record`:主单全字段(期望/实际撤机日期、撤机理由 memo、联系人、项目地址等)
|
||||
- `withdrawDetails`:`withdraw_order_details` 中 `in_count>0` 的行(实际取回明细)
|
||||
- `maintenanceUsers / cooperateUsers`:来自 `install_order_user`(type=1,user_type 0/1)
|
||||
- `processes`:`order_process`(order_type=1,schedule_state=2 已办、排除抄送 theme=12,
|
||||
按 operationTime+id 升序),主题/类型同时给 code+文案
|
||||
- `certificates`:凭证附件 `warehouse_in_out_pic`(in_out_allocation_type=12),
|
||||
输出 `{id, fileName, bucket, objectKey}` 三元组(不拼 URL,与现有 4 组工单一致),按流程 id 批量 IN 查询防 N+1
|
||||
|
||||
不存在或无权统一抛 `NOT_FOUND_OR_FORBIDDEN`(不泄露存在性)。
|
||||
|
||||
### 凭证口径说明(近似)
|
||||
|
||||
老系统取货/入库凭证(theme 6/7)挂在 `pickup_order_process` 上(经取货单号二次查询),
|
||||
MCP 侧简化为按本工单 `order_process` 流程 id 批量查 `warehouse_in_out_pic(in_out_allocation_type=12)`。
|
||||
两者 aggregate_id 语义不完全等价,若线上凭证挂接方式不同需再校准(可与
|
||||
`pickup.order.detail.query` 的 `withdrawOrderNo` 串联作为兜底)。
|
||||
|
||||
## 3. 与老系统字段映射
|
||||
|
||||
| 过滤维度 | 老系统参数 | MCP 实体列 |
|
||||
|---|---|---|
|
||||
| 申请日期 | applyDateStart/End | apply_time |
|
||||
| 审核日期 | auditDateStart/End | audit_time |
|
||||
| 调度日期 | dispatchDateStart/End | dispatch_time |
|
||||
| 取货日期 | pickupDateStart/End | pickup_time |
|
||||
| 完成日期 | finishDateStart/End | finish_time(老系统误生成 finish_date) |
|
||||
|
||||
## 4. 行级权限
|
||||
|
||||
`PermissionService.applyWithdrawOrderRowFilter`,1:1 复刻老系统 `getWorkOrderAuth(type=3)`:
|
||||
|
||||
- 总部(isagent=0)放行
|
||||
- 代理商:`apply_agent_id`/`agent_id` ∈ {本代理+授权代管} OR `property_id` ∈ 代管物业
|
||||
OR 本人出现在 `order_process`(order_type=1)
|
||||
- 销售非负责人:只看自己创建物业 + 代管代理名下物业(property_id 过滤),空集塞 0
|
||||
- 列表与详情复用同一方法,防详情绕过
|
||||
|
||||
## 5. 实现清单
|
||||
|
||||
- 新增:`entity/m2/WithdrawOrder.java`、`entity/m2/WithdrawOrderDetails.java`、
|
||||
`mapper/m2/WithdrawOrderMapper.java`、`mapper/m2/WithdrawOrderDetailsMapper.java`、
|
||||
`mcp/tool/WithdrawOrderTools.java`
|
||||
- 修改:`permission/PermissionService.java`(applyWithdrawOrderRowFilter)
|
||||
- 测试:`WithdrawOrderToolsTest`(6 用例:参数组合/截断 notice/人名不补/非法参数查库前拒绝/
|
||||
详情权限与存在性隐藏/详情组装/Spring AI schema 发布)、
|
||||
`PermissionServiceWithdrawOrderRowFilterTest`(4 身份)
|
||||
- 文档:doc/agent-mcp-tools-reference.md §28/29、本文件(README 不逐工具列举,无需改)
|
||||
|
||||
## 6. 上线检查单
|
||||
|
||||
- [x] 单测全量通过(281/281)
|
||||
- [ ] PR 合入 main
|
||||
- [ ] 远程部署(./deploy/deploy.sh remote-deploy)
|
||||
- [ ] prod scope 1(code='test')幂等绑定 tool id(ToolSyncService 启动自动同步 tool 表后
|
||||
INSERT scope_tool,防 tools/list 不可见——prod 作用域是 code='test' 不是 operation)
|
||||
- [ ] 线上 tools/list 冒烟(真实 AK,由用户确认)
|
||||
@@ -0,0 +1,59 @@
|
||||
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("withdraw_order")
|
||||
public class WithdrawOrder 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 pickupOrderNo;
|
||||
private String withdrawOrderNo;
|
||||
private Integer type;
|
||||
private Long agentId;
|
||||
private Long propertyId;
|
||||
private Long districtId;
|
||||
private String agentName;
|
||||
private String propertyName;
|
||||
private String districtName;
|
||||
private Integer orderState;
|
||||
private String preWithdrawDate;
|
||||
private String trueWithdrawDate;
|
||||
private String customerContact;
|
||||
private String contactMobile;
|
||||
private String projectAddress;
|
||||
private Integer withdrawGoodsCount;
|
||||
private String projectMemo;
|
||||
private String memo;
|
||||
private Long applyUserId;
|
||||
private String applyUserName;
|
||||
private Date applyTime;
|
||||
private Date auditTime;
|
||||
private Date dispatchTime;
|
||||
private Date pickupTime;
|
||||
private Date warehousingTime;
|
||||
private Date finishTime;
|
||||
private String auditUserName;
|
||||
private String dispatchUserName;
|
||||
private String maintenanceUserName;
|
||||
private String cooperateUserName;
|
||||
private String scheduleUserName;
|
||||
}
|
||||
@@ -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("withdraw_order_details")
|
||||
public class WithdrawOrderDetails 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 withdrawOrderNo;
|
||||
private Long materielDefinitionId;
|
||||
private String materialNo;
|
||||
private String materielFullName;
|
||||
private String materielSort;
|
||||
private Long materielIndexId;
|
||||
private String specifications;
|
||||
private String snCode;
|
||||
private Integer inCount;
|
||||
private Long inWarehouseId;
|
||||
private String inWarehouseName;
|
||||
private Integer type;
|
||||
private String productSort;
|
||||
private String address;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WithdrawOrderDetails;
|
||||
|
||||
public interface WithdrawOrderDetailsMapper extends BaseMapper<WithdrawOrderDetails> {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.iiv.agent.mapper.m2;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import me.iiv.agent.entity.m2.WithdrawOrder;
|
||||
|
||||
public interface WithdrawOrderMapper extends BaseMapper<WithdrawOrder> {
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
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.WarehouseInOutPic;
|
||||
import me.iiv.agent.entity.m2.WithdrawOrder;
|
||||
import me.iiv.agent.entity.m2.WithdrawOrderDetails;
|
||||
import me.iiv.agent.mapper.m2.InstallOrderUserMapper;
|
||||
import me.iiv.agent.mapper.m2.OrderProcessMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper;
|
||||
import me.iiv.agent.mapper.m2.WithdrawOrderDetailsMapper;
|
||||
import me.iiv.agent.mapper.m2.WithdrawOrderMapper;
|
||||
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;
|
||||
|
||||
/** 撤机工单查询工具,对齐老系统 /withdrawOrder/pageList 与 /lookDetails。 */
|
||||
@Component
|
||||
public class WithdrawOrderTools implements ToolObject {
|
||||
|
||||
private static final int DEFAULT_ROWS = 20;
|
||||
private static final int MAX_ROWS = 100;
|
||||
private static final int WITHDRAW_ORDER_TYPE = 1;
|
||||
private static final int WITHDRAW_USER_TYPE = 1;
|
||||
private static final int PICKUP_AGGREGATE_TYPE = 12;
|
||||
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> TYPE_TEXT = Map.of(0, "部分撤机", 1, "全部撤机");
|
||||
private static final Map<Integer, String> SCHEDULE_STATE_TEXT = Map.of(
|
||||
0, "无需待办", 1, "待办", 2, "已办");
|
||||
private static final Map<Integer, String> AUDIT_STATE_TEXT = Map.of(0, "审核驳回", 1, "审核通过");
|
||||
private static final Map<Integer, String> OPERATION_THEME_TEXT = Map.ofEntries(
|
||||
Map.entry(0, "撤机申请"), Map.entry(1, "审核通过"), Map.entry(2, "运营调度"),
|
||||
Map.entry(3, "撤机协同"), Map.entry(4, "送货"), Map.entry(5, "收货确认"),
|
||||
Map.entry(6, "取货"), Map.entry(7, "入库"), Map.entry(8, "结单"),
|
||||
Map.entry(9, "驳回修改"), Map.entry(10, "审核驳回"), Map.entry(11, "已取消"),
|
||||
Map.entry(12, "抄送"), Map.entry(13, "装机协同"), Map.entry(14, "取货结单"));
|
||||
private static final Map<Integer, String> OPERATION_TYPE_TEXT = Map.of(
|
||||
0, "审批流操作", 1, "运营调度", 2, "本人修改", 3, "驳回或修改操作",
|
||||
4, "配发协同", 5, "送货", 6, "收货确认", 7, "结单");
|
||||
|
||||
final WithdrawOrderMapper orderMapper;
|
||||
final WithdrawOrderDetailsMapper detailsMapper;
|
||||
final OrderProcessMapper processMapper;
|
||||
final InstallOrderUserMapper userMapper;
|
||||
final WarehouseInOutPicMapper picMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public WithdrawOrderTools(WithdrawOrderMapper orderMapper,
|
||||
WithdrawOrderDetailsMapper detailsMapper,
|
||||
OrderProcessMapper processMapper,
|
||||
InstallOrderUserMapper userMapper,
|
||||
WarehouseInOutPicMapper picMapper,
|
||||
PermissionService permissionService) {
|
||||
this.orderMapper = orderMapper;
|
||||
this.detailsMapper = detailsMapper;
|
||||
this.processMapper = processMapper;
|
||||
this.userMapper = userMapper;
|
||||
this.picMapper = picMapper;
|
||||
this.permissionService = permissionService;
|
||||
}
|
||||
|
||||
@XqTool(category = "workorder", kind = "read")
|
||||
@Tool(name = "withdraw.order.list.query", description = "分页查询当前用户可见的撤机工单,对齐老系统 /withdrawOrder/pageList。"
|
||||
+ "支持撤机类型、状态、撤机单号、取货单号、代理、项目、申请/运维/协同人员及申请/审核/调度/取货/完成日期范围。"
|
||||
+ "日期格式 yyyy-MM-dd,结束日期含当天;page 默认1,rows 默认20、上限100;固定按 id 倒序。"
|
||||
+ "人名字段为空时保持为空,不补占位符。")
|
||||
public Map<String, Object> withdrawOrderListQuery(
|
||||
@ToolParam(required = false, description = "撤机类型:0部分撤机 1全部撤机") Integer type,
|
||||
@ToolParam(required = false, description = "状态:0待审核 1被驳回 2调度中 3作业中 4待取回 5已完成 6已取消") Integer orderState,
|
||||
@ToolParam(required = false, description = "撤机工单号,模糊匹配") String withdrawOrderNo,
|
||||
@ToolParam(required = false, description = "取货工单号,模糊匹配") String pickupOrderNo,
|
||||
@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 = "申请日期起,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 pickupDateFrom,
|
||||
@ToolParam(required = false, description = "取货日期止,yyyy-MM-dd,含当天") String pickupDateTo,
|
||||
@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(type, Set.of(0, 1), "type");
|
||||
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 pickupFrom = parseDate(pickupDateFrom, "pickupDateFrom");
|
||||
LocalDate pickupTo = parseDate(pickupDateTo, "pickupDateTo");
|
||||
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(pickupFrom, pickupTo, "pickupDateFrom", "pickupDateTo");
|
||||
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<WithdrawOrder> wrapper = new LambdaQueryWrapper<WithdrawOrder>()
|
||||
.eq(WithdrawOrder::getSoftDelete, 0)
|
||||
.eq(type != null, WithdrawOrder::getType, type)
|
||||
.eq(orderState != null, WithdrawOrder::getOrderState, orderState)
|
||||
.eq(agentId != null, WithdrawOrder::getAgentId, agentId)
|
||||
// 老系统 finishDate 过滤误生成 finish_date 列(SQL 报错),MCP 侧映射到正确的 finish_time
|
||||
.ge(applyFrom != null, WithdrawOrder::getApplyTime, startOfDay(applyFrom))
|
||||
.lt(applyTo != null, WithdrawOrder::getApplyTime, nextDayStart(applyTo))
|
||||
.ge(auditFrom != null, WithdrawOrder::getAuditTime, startOfDay(auditFrom))
|
||||
.lt(auditTo != null, WithdrawOrder::getAuditTime, nextDayStart(auditTo))
|
||||
.ge(dispatchFrom != null, WithdrawOrder::getDispatchTime, startOfDay(dispatchFrom))
|
||||
.lt(dispatchTo != null, WithdrawOrder::getDispatchTime, nextDayStart(dispatchTo))
|
||||
.ge(pickupFrom != null, WithdrawOrder::getPickupTime, startOfDay(pickupFrom))
|
||||
.lt(pickupTo != null, WithdrawOrder::getPickupTime, nextDayStart(pickupTo))
|
||||
.ge(finishFrom != null, WithdrawOrder::getFinishTime, startOfDay(finishFrom))
|
||||
.lt(finishTo != null, WithdrawOrder::getFinishTime, nextDayStart(finishTo));
|
||||
like(wrapper, WithdrawOrder::getWithdrawOrderNo, withdrawOrderNo);
|
||||
like(wrapper, WithdrawOrder::getPickupOrderNo, pickupOrderNo);
|
||||
like(wrapper, WithdrawOrder::getDistrictName, districtName);
|
||||
like(wrapper, WithdrawOrder::getApplyUserName, applyUserName);
|
||||
like(wrapper, WithdrawOrder::getMaintenanceUserName, maintenanceUserName);
|
||||
like(wrapper, WithdrawOrder::getCooperateUserName, cooperateUserName);
|
||||
permissionService.applyWithdrawOrderRowFilter(wrapper, currentUser());
|
||||
|
||||
long total = orderMapper.selectCount(wrapper);
|
||||
List<WithdrawOrder> orders = orderMapper.selectList(wrapper.orderByDesc(WithdrawOrder::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 = "withdraw.order.detail.query", description = "按 id 或撤机工单号查询详情。"
|
||||
+ "两者至少传一个,同时传入时必须匹配同一条记录;行级权限与列表一致。"
|
||||
+ "不存在或无权查看统一返回 NOT_FOUND_OR_FORBIDDEN。返回主单、撤机物品明细(in_count>0)、"
|
||||
+ "运维/协同人员、已办流程(共享 order_process,order_type=1,排除抄送)与凭证附件;附件仅返回 bucket/objectKey/fileName。")
|
||||
public Map<String, Object> withdrawOrderDetailQuery(
|
||||
@ToolParam(required = false, description = "撤机工单主键 id,与 withdrawOrderNo 至少传一个") Long id,
|
||||
@ToolParam(required = false, description = "撤机工单号,与 id 至少传一个") String withdrawOrderNo) {
|
||||
if ((id == null || id <= 0) && !StringUtils.hasText(withdrawOrderNo)) {
|
||||
throw new IllegalArgumentException("id、withdrawOrderNo 至少传一个");
|
||||
}
|
||||
if (id != null && id <= 0) {
|
||||
throw new IllegalArgumentException("id 必须为正整数");
|
||||
}
|
||||
LambdaQueryWrapper<WithdrawOrder> wrapper = new LambdaQueryWrapper<WithdrawOrder>()
|
||||
.eq(WithdrawOrder::getSoftDelete, 0)
|
||||
.eq(id != null, WithdrawOrder::getId, id)
|
||||
.eq(StringUtils.hasText(withdrawOrderNo), WithdrawOrder::getWithdrawOrderNo,
|
||||
StringUtils.hasText(withdrawOrderNo) ? withdrawOrderNo.trim() : withdrawOrderNo);
|
||||
permissionService.applyWithdrawOrderRowFilter(wrapper, currentUser());
|
||||
List<WithdrawOrder> visible = orderMapper.selectList(
|
||||
wrapper.orderByDesc(WithdrawOrder::getId).last("LIMIT 1"));
|
||||
if (visible.isEmpty()) {
|
||||
throw new ToolCallException(McpErrorCode.NOT_FOUND_OR_FORBIDDEN,
|
||||
"撤机工单不存在或当前身份无权查看");
|
||||
}
|
||||
|
||||
WithdrawOrder order = visible.get(0);
|
||||
String orderNo = order.getWithdrawOrderNo();
|
||||
List<WithdrawOrderDetails> details = detailsMapper.selectList(
|
||||
new LambdaQueryWrapper<WithdrawOrderDetails>()
|
||||
.eq(WithdrawOrderDetails::getSoftDelete, 0)
|
||||
.eq(WithdrawOrderDetails::getWithdrawOrderNo, orderNo)
|
||||
.gt(WithdrawOrderDetails::getInCount, 0)
|
||||
.orderByAsc(WithdrawOrderDetails::getId));
|
||||
List<InstallOrderUser> users = userMapper.selectList(new LambdaQueryWrapper<InstallOrderUser>()
|
||||
.eq(InstallOrderUser::getSoftDelete, 0)
|
||||
.eq(InstallOrderUser::getInstallOrderNo, orderNo)
|
||||
.eq(InstallOrderUser::getType, WITHDRAW_USER_TYPE)
|
||||
.orderByAsc(InstallOrderUser::getId));
|
||||
List<OrderProcess> processes = processMapper.selectList(new LambdaQueryWrapper<OrderProcess>()
|
||||
.eq(OrderProcess::getSoftDelete, 0)
|
||||
.eq(OrderProcess::getOrderNo, orderNo)
|
||||
.eq(OrderProcess::getOrderType, WITHDRAW_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 = certificates(processes);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("record", orderRecord(order));
|
||||
result.put("withdrawDetails", 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("certificates", 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, WITHDRAW_USER_TYPE)
|
||||
.in(InstallOrderUser::getInstallOrderProcessId, ids)
|
||||
.orderByAsc(InstallOrderUser::getId)).stream()
|
||||
.collect(Collectors.groupingBy(InstallOrderUser::getInstallOrderProcessId,
|
||||
LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
private List<WarehouseInOutPic> certificates(List<OrderProcess> processes) {
|
||||
if (processes.isEmpty()) return List.of();
|
||||
List<Long> ids = processes.stream().map(OrderProcess::getId).collect(Collectors.toList());
|
||||
return picMapper.selectList(new LambdaQueryWrapper<WarehouseInOutPic>()
|
||||
.eq(WarehouseInOutPic::getSoftDelete, 0)
|
||||
.in(WarehouseInOutPic::getAggregateId, ids)
|
||||
.eq(WarehouseInOutPic::getInOutAllocationType, PICKUP_AGGREGATE_TYPE)
|
||||
.orderByAsc(WarehouseInOutPic::getId));
|
||||
}
|
||||
|
||||
private Map<String, Object> orderRecord(WithdrawOrder order) {
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("id", order.getId());
|
||||
record.put("withdrawOrderNo", order.getWithdrawOrderNo());
|
||||
record.put("pickupOrderNo", order.getPickupOrderNo());
|
||||
record.put("type", order.getType());
|
||||
record.put("typeText", text(TYPE_TEXT, order.getType()));
|
||||
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("preWithdrawDate", order.getPreWithdrawDate());
|
||||
record.put("trueWithdrawDate", order.getTrueWithdrawDate());
|
||||
record.put("customerContact", order.getCustomerContact());
|
||||
record.put("contactMobile", order.getContactMobile());
|
||||
record.put("projectAddress", order.getProjectAddress());
|
||||
record.put("withdrawGoodsCount", order.getWithdrawGoodsCount());
|
||||
record.put("projectMemo", order.getProjectMemo());
|
||||
record.put("memo", order.getMemo());
|
||||
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("applyTime", formatTime(order.getApplyTime()));
|
||||
record.put("auditTime", formatTime(order.getAuditTime()));
|
||||
record.put("dispatchTime", formatTime(order.getDispatchTime()));
|
||||
record.put("pickupTime", formatTime(order.getPickupTime()));
|
||||
record.put("warehousingTime", formatTime(order.getWarehousingTime()));
|
||||
record.put("finishTime", formatTime(order.getFinishTime()));
|
||||
record.put("createTime", formatTime(order.getCreateTime()));
|
||||
record.put("modifyTime", formatTime(order.getModifyTime()));
|
||||
return record;
|
||||
}
|
||||
|
||||
private Map<String, Object> detailRecord(WithdrawOrderDetails detail) {
|
||||
Map<String, Object> record = new LinkedHashMap<>();
|
||||
record.put("id", detail.getId());
|
||||
record.put("materielDefinitionId", detail.getMaterielDefinitionId());
|
||||
record.put("materialNo", detail.getMaterialNo());
|
||||
record.put("materielFullName", detail.getMaterielFullName());
|
||||
record.put("materielSort", detail.getMaterielSort());
|
||||
record.put("materielIndexId", detail.getMaterielIndexId());
|
||||
record.put("specifications", detail.getSpecifications());
|
||||
record.put("snCode", detail.getSnCode());
|
||||
record.put("inCount", detail.getInCount());
|
||||
record.put("inWarehouseId", detail.getInWarehouseId());
|
||||
record.put("inWarehouseName", detail.getInWarehouseName());
|
||||
record.put("type", detail.getType());
|
||||
record.put("productSort", detail.getProductSort());
|
||||
record.put("address", detail.getAddress());
|
||||
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("operationTypeText", text(OPERATION_TYPE_TEXT, 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("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<WithdrawOrder> wrapper,
|
||||
SFunction<WithdrawOrder, ?> 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;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ 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.WithdrawOrder;
|
||||
import me.iiv.agent.entity.m2.WarehouseIn;
|
||||
import me.iiv.agent.entity.m2.WarehouseOut;
|
||||
import me.iiv.agent.entity.m2.WarehouseStockManage;
|
||||
@@ -334,6 +335,59 @@ public class PermissionService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤机工单权限:1:1 复刻老系统 getWorkOrderAuth(type=3)。总部看全部;其余代理身份
|
||||
* 可看申请代理、所属代理(本代理+授权代管)、授权代管物业范围,以及本人出现在
|
||||
* order_process(order_type=1) 的工单;销售非负责人只看自己创建及代管代理名下物业,
|
||||
* 空集塞 0。列表和详情必须复用本方法,老系统 lookDetails 无权限校验,MCP 侧必须补齐。
|
||||
*/
|
||||
public void applyWithdrawOrderRowFilter(LambdaQueryWrapper<WithdrawOrder> 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()));
|
||||
|
||||
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()));
|
||||
}
|
||||
wrapper.in(WithdrawOrder::getPropertyId,
|
||||
propertyIds.isEmpty() ? List.of(0L) : new LinkedHashSet<>(propertyIds));
|
||||
return;
|
||||
}
|
||||
|
||||
String processParticipant = "EXISTS (SELECT 1 FROM order_process op "
|
||||
+ "WHERE op.order_no = withdraw_order.withdraw_order_no AND op.soft_delete = 0 "
|
||||
+ "AND op.order_type = 1 AND op.user_id = {0})";
|
||||
List<Long> managedPropertyIds = managedPropertyIds(agentIds);
|
||||
wrapper.and(w -> {
|
||||
w.in(WithdrawOrder::getApplyAgentId, agentIds)
|
||||
.or().in(WithdrawOrder::getAgentId, agentIds);
|
||||
if (!managedPropertyIds.isEmpty()) {
|
||||
w.or().in(WithdrawOrder::getPropertyId, managedPropertyIds);
|
||||
}
|
||||
w.or().apply(processParticipant, user.getId());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 装机工单权限,1:1 对齐老 getWorkOrderAuth(type=0) 的有效业务口径:总部看全部;
|
||||
* 销售非负责人只看自己创建物业及授权代管公司的物业;其他代理用户可看自己、
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
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.WarehouseInOutPic;
|
||||
import me.iiv.agent.entity.m2.WithdrawOrder;
|
||||
import me.iiv.agent.entity.m2.WithdrawOrderDetails;
|
||||
import me.iiv.agent.mapper.m2.InstallOrderUserMapper;
|
||||
import me.iiv.agent.mapper.m2.OrderProcessMapper;
|
||||
import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper;
|
||||
import me.iiv.agent.mapper.m2.WithdrawOrderDetailsMapper;
|
||||
import me.iiv.agent.mapper.m2.WithdrawOrderMapper;
|
||||
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.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class WithdrawOrderToolsTest {
|
||||
|
||||
@Mock WithdrawOrderMapper orderMapper;
|
||||
@Mock WithdrawOrderDetailsMapper detailsMapper;
|
||||
@Mock OrderProcessMapper processMapper;
|
||||
@Mock InstallOrderUserMapper userMapper;
|
||||
@Mock WarehouseInOutPicMapper picMapper;
|
||||
@Mock PermissionService permissionService;
|
||||
|
||||
WithdrawOrderTools tools;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, WithdrawOrder.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WithdrawOrderDetails.class);
|
||||
TableInfoHelper.initTableInfo(assistant, OrderProcess.class);
|
||||
TableInfoHelper.initTableInfo(assistant, InstallOrderUser.class);
|
||||
TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class);
|
||||
tools = new WithdrawOrderTools(orderMapper, detailsMapper, processMapper, userMapper,
|
||||
picMapper, permissionService);
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
AuthContextHolder.set(new AuthContext(null, user));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
AuthContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listReturnsStateAndTypeTextAndClampsRows() {
|
||||
when(orderMapper.selectCount(any())).thenReturn(1L);
|
||||
when(orderMapper.selectList(any())).thenReturn(List.of(order()));
|
||||
|
||||
Map<String, Object> result = tools.withdrawOrderListQuery(1, 5, "CJGD", "QH", 10L, "项目",
|
||||
"申请人", "运维员", "协同员", "2026-09-01", "2026-09-30",
|
||||
null, null, null, null, null, null, null, null, 0, 101);
|
||||
|
||||
assertEquals(1L, result.get("total"));
|
||||
assertEquals(1, result.get("page"));
|
||||
assertEquals(100, result.get("rows"));
|
||||
assertEquals("rows 超过上限 100,已截断为 100", result.get("notice"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> record = ((List<Map<String, Object>>) result.get("records")).get(0);
|
||||
assertEquals("全部撤机", record.get("typeText"));
|
||||
assertEquals("已完成", record.get("orderStateText"));
|
||||
// 老系统列表为空人名补 "-",MCP 侧保持为空
|
||||
assertNull(record.get("cooperateUserName"));
|
||||
verify(permissionService).applyWithdrawOrderRowFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listRejectsInvalidCodeAndDateRangeBeforeQuery() {
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.withdrawOrderListQuery(null,
|
||||
9, null, null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.withdrawOrderListQuery(2,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.withdrawOrderListQuery(null,
|
||||
null, null, null, null, null, null, null, null, "2026-09-30", "2026-09-01",
|
||||
null, null, null, null, null, null, null, null, null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.withdrawOrderListQuery(null,
|
||||
null, null, null, null, null, null, null, null, null, "2026-09-01-1",
|
||||
null, null, 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.withdrawOrderDetailQuery(20L, null));
|
||||
|
||||
assertEquals(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, error.getCode());
|
||||
verify(permissionService).applyWithdrawOrderRowFilter(any(), any());
|
||||
verifyNoInteractions(detailsMapper, processMapper, userMapper, picMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailRejectsMissingAndNonPositiveId() {
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.withdrawOrderDetailQuery(null, null));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.withdrawOrderDetailQuery(0L, "CJGD-001"));
|
||||
assertThrows(IllegalArgumentException.class, () -> tools.withdrawOrderDetailQuery(-1L, null));
|
||||
verifyNoInteractions(orderMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsDetailsPersonnelProcessesAndCertificates() {
|
||||
when(orderMapper.selectList(any())).thenReturn(List.of(order()));
|
||||
WithdrawOrderDetails detail = new WithdrawOrderDetails();
|
||||
detail.setId(3L);
|
||||
detail.setMaterialNo("M-001");
|
||||
detail.setInCount(2);
|
||||
when(detailsMapper.selectList(any())).thenReturn(List.of(detail));
|
||||
|
||||
InstallOrderUser worker = new InstallOrderUser();
|
||||
worker.setId(4L);
|
||||
worker.setUserId(40L);
|
||||
worker.setUserName("运维员");
|
||||
worker.setUserType(0);
|
||||
worker.setType(1);
|
||||
worker.setInstallOrderProcessId(5L);
|
||||
InstallOrderUser collaborator = new InstallOrderUser();
|
||||
collaborator.setId(6L);
|
||||
collaborator.setUserName("协同员");
|
||||
collaborator.setUserType(1);
|
||||
collaborator.setType(1);
|
||||
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(8);
|
||||
process.setOperationType(7);
|
||||
process.setScheduleState(2);
|
||||
process.setOrderType(1);
|
||||
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("withdraw/");
|
||||
pic.setInOutAllocationType(12);
|
||||
when(picMapper.selectList(any())).thenReturn(List.of(pic));
|
||||
|
||||
Map<String, Object> result = tools.withdrawOrderDetailQuery(20L, "CJGD-001");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> record = (Map<String, Object>) result.get("record");
|
||||
assertEquals("CJGD-001", record.get("withdrawOrderNo"));
|
||||
assertEquals("已完成", record.get("orderStateText"));
|
||||
assertEquals(1, ((List<?>) result.get("withdrawDetails")).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"));
|
||||
assertEquals("结单", processResult.get("operationTypeText"));
|
||||
assertEquals(1, ((List<?>) processResult.get("maintenanceUsers")).size());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> certificate = ((List<Map<String, Object>>) result.get("certificates")).get(0);
|
||||
assertEquals("withdraw/finish.jpg", certificate.get("objectKey"));
|
||||
assertEquals("bucket-a", certificate.get("bucket"));
|
||||
}
|
||||
|
||||
@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("withdraw.order.list.query").contains("finishDateTo"));
|
||||
assertTrue(schemas.get("withdraw.order.list.query").contains("pickupDateFrom"));
|
||||
assertTrue(schemas.get("withdraw.order.detail.query").contains("withdrawOrderNo"));
|
||||
}
|
||||
|
||||
private static WithdrawOrder order() {
|
||||
WithdrawOrder order = new WithdrawOrder();
|
||||
order.setId(20L);
|
||||
order.setWithdrawOrderNo("CJGD-001");
|
||||
order.setOrderState(5);
|
||||
order.setType(1);
|
||||
order.setWithdrawGoodsCount(2);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
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.WithdrawOrder;
|
||||
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 me.iiv.agent.entity.m2.WarehouseAgentAuth;
|
||||
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.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PermissionServiceWithdrawOrderRowFilterTest {
|
||||
@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, WithdrawOrder.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<WithdrawOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyWithdrawOrderRowFilter(wrapper, user(null));
|
||||
|
||||
assertTrue(wrapper.getSqlSegment().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentCombinesAgentsManagedPropertyAndProcess() {
|
||||
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<WithdrawOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyWithdrawOrderRowFilter(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 = 1"));
|
||||
// 撤机权限不含 install_order_user 作业人员分支(老 getWorkOrderAuth(type=3) 口径)
|
||||
assertFalse(sql.contains("install_order_user"));
|
||||
assertTrue(values.containsAll(List.of(7L, 10L, 12L, 30L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void salesNonLeaderLimitsToOwnAndManagedProperties() {
|
||||
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<WithdrawOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyWithdrawOrderRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
assertTrue(sql.contains("property_id IN"));
|
||||
assertFalse(sql.contains("order_process"));
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(21L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void salesNonLeaderWithoutPropertiesSeesNothing() {
|
||||
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,");
|
||||
when(properytMapper.selectList(any())).thenReturn(List.of());
|
||||
LambdaQueryWrapper<WithdrawOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
service.applyWithdrawOrderRowFilter(wrapper, user);
|
||||
|
||||
String sql = wrapper.getSqlSegment();
|
||||
assertTrue(sql.contains("property_id IN"));
|
||||
// 空集塞 0,保证 SQL 恒假而非全表放行
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().contains(0L));
|
||||
}
|
||||
|
||||
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