[REQ-20260916-0020] 送货工单 MCP 查询工具(列表+详情) #18

Merged
wangheng merged 1 commits from feat/REQ-20260916-0020-delivery-order-tools into main 2026-09-16 16:41:08 +08:00
17 changed files with 1074 additions and 0 deletions
+2
View File
@@ -85,6 +85,8 @@ xiaoqu-agent-service/
| `warehouse.in.list.query` | warehouse | read | 入库单分页查询;入库/申请日期独立过滤,主单代理、涉及仓库、流程参与人三类权限取并集 |
| `warehouse.in.detail.query` | warehouse | read | 入库单详情;支持调拨单,物料汇总、可选 SN、流程、附件和关联出库索引 |
| `warehouse.todo.list.query` | warehouse | read | 当前用户仓库待办统一查询(REQ-20260915-0008),合并老系统出入库/调拨三个待办接口;流程视图 user_id 过滤,不按代理商过滤;调拨派生出入库单带 derivedFrom 标记 |
| `delivery.order.list.query` | workorder | read | 送货工单分页查询;申请/完成日期独立过滤,代理、代管物业及流程参与人权限取并集 |
| `delivery.order.detail.query` | workorder | read | 送货工单详情;返回物流、配发物品、发货/签收附件、流程及关联单据 |
分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 `notice` 字段提示。
@@ -583,6 +583,38 @@
---
## 20. delivery.order.list.query
分页查询送货工单,对应旧系统 `POST /delivery/deliveryListByPage`。
- 分类:workorder | 类型:读
- 数据来源:m2 `delivery_order`。
- 入参:`deliveryOrderNo`、`sourceOrderNo`、`agentId`、`districtName`、`orderType`、
`orderState`、`sendMode`、`applyUserName`、申请/完成日期范围及 `page/rows`。
- `orderType`:0=装机单送货,1=出库单送货;`orderState`:0=派送中,1=已完成,2=已取消;
`sendMode`:0=快递公司,1=自行派送。
- 申请日期按业务字段 `apply_time` 查询,不沿用旧接口的 `create_time`;申请与完成日期可同时过滤。
- 行级权限:总部看全部;代理身份可看申请代理或所属代理在自己、直属下级、授权代管范围内的工单,
以及授权代管物业和本人参与流程的工单;销售非负责人按其可见物业范围过滤。
- 返回 `total/page/rows/records`,固定按 ID 倒序;记录同时返回状态、类型、配送方式的码值与文案。
## 21. delivery.order.detail.query
按 `id` 或 `deliveryOrderNo` 查询详情,两者至少传一个;同时传入时必须匹配同一条可见记录。
- 行级权限与列表完全一致;不存在和无权统一返回 `NOT_FOUND_OR_FORBIDDEN`。
- `record`:送货主单和文案字段。
- `logistics`:物流公司/单号或派送员信息、物流状态;无物流记录时为 null。
- `goods`:m2 `install_distribute_details` 配发物品,含物料、SN、数量、出入仓库和商城订单号。
- `shippingFiles`:m1 `mall_logistics_pic` 发货图片。
- `processes`:`schedule_state=2` 的流程记录及 m2 `warehouse_in_out_pic` 签收附件,按操作时间、ID 正序。
- `relatedDocuments`:m2 `install_to_out_warehouse` 关联的出库、入库、调拨或划转单。
- 所有附件仅返回 `id/fileName/bucket/objectKey`,不拼接公开 URL。
详细设计见 `delivery-order-mcp-tools-design.md`。
---
## 附录:在线查看工具清单
```bash
@@ -0,0 +1,43 @@
# 送货工单 MCP 工具设计
## 范围
基于旧系统 `DeliveryOrderController` 的 `/delivery/deliveryListByPage` 与 `/delivery/deliveryScan`,
提供两个只读 MCP 工具:
- `delivery.order.list.query`
- `delivery.order.detail.query`
不包含送货签收、取消、导出等写操作。
## 数据关系
| 数据 | 数据库/表 | 关联键 |
|---|---|---|
| 主单 | m2 `delivery_order` | `delivery_order_no` |
| 配发物品 | m2 `install_distribute_details` | `delivery_order_no` |
| 流程 | m2 `delivery_order_process` | `delivery_order_no` |
| 签收附件 | m2 `warehouse_in_out_pic` | `aggregate_id=process.id`, `file_type=3`, `in_out_allocation_type=11` |
| 关联单据 | m2 `install_to_out_warehouse` | `delivery_order_no` |
| 物流 | m1 `mall_order_logistics` | `order_no=delivery_order_no` |
| 发货图片 | m1 `mall_logistics_pic` | `order_no=delivery_order_no` |
跨库数据分开查询,不使用跨库 JOIN。详情先在带行权限的主单查询中确定可见记录,再读取关联信息。
## 权限
列表和详情统一调用 `PermissionService.applyDeliveryOrderRowFilter`:
1. 总部身份不增加过滤。
2. 销售非负责人按自己创建及代管代理名下的物业范围过滤,同时保留本人流程参与记录。
3. 其他代理身份可见申请代理或所属代理在自己、直属下级和授权代管范围内的工单;另包含代管物业、
本人参与 `delivery_order_process` 的工单。
4. 详情不存在与无权查看统一返回 `NOT_FOUND_OR_FORBIDDEN`。
## 查询与输出约束
- 日期严格使用 `yyyy-MM-dd`,结束日期通过次日零点的排他上界实现“含当天”。
- 列表分页默认 1/20,rows 最大 100,固定 `id DESC`。
- 详情同时传入 ID 和单号时按 AND 匹配,避免参数指向不同工单。
- 流程只返回 `schedule_state=2` 的已处理记录,兼容历史 `operation_type=0/5/6`。
- 附件不返回拼接的 OSS HTTP 地址,只返回 bucket 和 objectKey,由有权限的上层服务决定访问方式。
@@ -0,0 +1,26 @@
package me.iiv.agent.entity.m1;
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;
/** 物流发货图片(m1,只读)。 */
@Getter
@Setter
@ToString
@TableName("mall_logistics_pic")
public class MallLogisticsPic implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Integer softDelete;
private Integer state;
private String fileName;
private String orderNo;
private String ossbucketname;
private String ossfolder;
}
@@ -0,0 +1,45 @@
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("delivery_order")
public class DeliveryOrder 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 deliveryOrderNo;
private String installOrderNo;
private Long agentId;
private Long propertyId;
private Long districtId;
private String agentName;
private String propertyName;
private String districtName;
private Integer orderState;
private Long applyUserId;
private String applyUserName;
private Date applyTime;
private Date finishDate;
private String scheduleUserName;
private String pickupOrderNo;
private Integer sendMode;
private Integer orderType;
}
@@ -0,0 +1,35 @@
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("delivery_order_process")
public class DeliveryOrderProcess 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 deliveryOrderNo;
private Long userId;
private String userName;
private Date operationTime;
private Integer scheduleState;
private Integer auditState;
private Integer operationTheme;
private Integer operationType;
private String memo;
}
@@ -0,0 +1,41 @@
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;
/** 装机/送货配发物品(m2,只读)。 */
@Getter
@Setter
@ToString
@TableName("install_distribute_details")
public class InstallDistributeDetail implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Integer softDelete;
private String installOrderNo;
private String deliveryOrderNo;
private Long materielDefinitionId;
private String materialNo;
private String materielFullName;
private String materielSort;
private Long materielIndexId;
private String specifications;
private String snCode;
private Integer outCount;
private Long outWarehouseId;
private String outWarehouseName;
private Long inWarehouseId;
private String inWarehouseName;
private String outInOrderNo;
private Integer actionType;
private String productSort;
private String mallOrderNo;
private Integer freeFlag;
private Integer payFlag;
}
@@ -0,0 +1,27 @@
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;
/** 工单与出入库/调拨/划转单关联(m2,只读)。 */
@Getter
@Setter
@ToString
@TableName("install_to_out_warehouse")
public class InstallToOutWarehouse implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private Integer softDelete;
private String installOrderNo;
private String deliveryOrderNo;
private String pickupOrderNo;
private Integer orderType;
private Integer outWarehouseType;
private String orderNo;
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m1;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m1.MallLogisticsPic;
public interface MallLogisticsPicMapper extends BaseMapper<MallLogisticsPic> {
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m2;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m2.DeliveryOrder;
public interface DeliveryOrderMapper extends BaseMapper<DeliveryOrder> {
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m2;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m2.DeliveryOrderProcess;
public interface DeliveryOrderProcessMapper extends BaseMapper<DeliveryOrderProcess> {
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m2;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m2.InstallDistributeDetail;
public interface InstallDistributeDetailMapper extends BaseMapper<InstallDistributeDetail> {
}
@@ -0,0 +1,7 @@
package me.iiv.agent.mapper.m2;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m2.InstallToOutWarehouse;
public interface InstallToOutWarehouseMapper extends BaseMapper<InstallToOutWarehouse> {
}
@@ -0,0 +1,421 @@
package me.iiv.agent.mcp.tool;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallLogisticsPic;
import me.iiv.agent.entity.m1.MallOrderLogistics;
import me.iiv.agent.entity.m2.DeliveryOrder;
import me.iiv.agent.entity.m2.DeliveryOrderProcess;
import me.iiv.agent.entity.m2.InstallDistributeDetail;
import me.iiv.agent.entity.m2.InstallToOutWarehouse;
import me.iiv.agent.entity.m2.WarehouseInOutPic;
import me.iiv.agent.mapper.m1.MallLogisticsPicMapper;
import me.iiv.agent.mapper.m1.MallOrderLogisticsMapper;
import me.iiv.agent.mapper.m2.DeliveryOrderMapper;
import me.iiv.agent.mapper.m2.DeliveryOrderProcessMapper;
import me.iiv.agent.mapper.m2.InstallDistributeDetailMapper;
import me.iiv.agent.mapper.m2.InstallToOutWarehouseMapper;
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;
/** 送货工单只读查询,参考老系统 /delivery/deliveryListByPage 与 /delivery/deliveryScan。 */
@Component
public class DeliveryOrderTools implements ToolObject {
private static final int DEFAULT_ROWS = 20;
private static final int MAX_ROWS = 100;
private static final int RECEIPT_FILE_TYPE = 3;
private static final int DELIVERY_AGGREGATE_TYPE = 11;
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, "已取消");
private static final Map<Integer, String> ORDER_TYPE_TEXT = Map.of(
0, "装机单送货", 1, "出库单送货");
private static final Map<Integer, String> SEND_MODE_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> OPERATION_THEME_TEXT = Map.of(
0, "提交送货工单", 1, "签收");
private static final Map<Integer, String> OPERATION_TYPE_TEXT = Map.of(
0, "通过", 5, "送货", 6, "收货确认");
private static final Map<Integer, String> LOGISTICS_STATE_TEXT = Map.of(
0, "已出单", 1, "已出库", 2, "已发货", 3, "已揽件",
4, "运输中", 5, "待取件", 6, "派送中", 7, "已签收");
private static final Map<Integer, String> DOCUMENT_TYPE_TEXT = Map.of(
1, "出库单", 2, "入库单", 3, "调拨单", 4, "划转单");
final DeliveryOrderMapper deliveryOrderMapper;
final DeliveryOrderProcessMapper processMapper;
final InstallDistributeDetailMapper detailMapper;
final InstallToOutWarehouseMapper relationMapper;
final WarehouseInOutPicMapper receiptPicMapper;
final MallOrderLogisticsMapper logisticsMapper;
final MallLogisticsPicMapper shippingPicMapper;
final PermissionService permissionService;
public DeliveryOrderTools(DeliveryOrderMapper deliveryOrderMapper,
DeliveryOrderProcessMapper processMapper,
InstallDistributeDetailMapper detailMapper,
InstallToOutWarehouseMapper relationMapper,
WarehouseInOutPicMapper receiptPicMapper,
MallOrderLogisticsMapper logisticsMapper,
MallLogisticsPicMapper shippingPicMapper,
PermissionService permissionService) {
this.deliveryOrderMapper = deliveryOrderMapper;
this.processMapper = processMapper;
this.detailMapper = detailMapper;
this.relationMapper = relationMapper;
this.receiptPicMapper = receiptPicMapper;
this.logisticsMapper = logisticsMapper;
this.shippingPicMapper = shippingPicMapper;
this.permissionService = permissionService;
}
@XqTool(category = "workorder", kind = "read")
@Tool(name = "delivery.order.list.query", description = "分页查询送货工单,参考老系统 /delivery/deliveryListByPage。"
+ "总部可看全部;代理身份可看自己、直属下级、代管代理/物业及本人参与流程的工单;销售非负责人按其物业范围过滤。"
+ "申请日期使用 apply_time,申请与完成日期可同时过滤,结束日期含当天。"
+ "orderType:0装机单送货、1出库单送货;orderState:0派送中、1已完成、2已取消;sendMode:0快递公司、1自行派送。")
public Map<String, Object> deliveryOrderListQuery(
@ToolParam(required = false, description = "送货工单号,模糊匹配") String deliveryOrderNo,
@ToolParam(required = false, description = "来源单号(装机单号或出库单号),模糊匹配") String sourceOrderNo,
@ToolParam(required = false, description = "所属代理商 id") Long agentId,
@ToolParam(required = false, description = "站点名称,模糊匹配") String districtName,
@ToolParam(required = false, description = "工单类型:0装机单送货 1出库单送货") Integer orderType,
@ToolParam(required = false, description = "状态:0派送中 1已完成 2已取消") Integer orderState,
@ToolParam(required = false, description = "配送方式:0快递公司 1自行派送") Integer sendMode,
@ToolParam(required = false, description = "申请人,模糊匹配") String applyUserName,
@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 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(orderType, Set.of(0, 1), "orderType");
validateCode(orderState, Set.of(0, 1, 2), "orderState");
validateCode(sendMode, Set.of(0, 1), "sendMode");
LocalDate applyFrom = parseDate(applyDateFrom, "applyDateFrom");
LocalDate applyTo = parseDate(applyDateTo, "applyDateTo");
LocalDate finishFrom = parseDate(finishDateFrom, "finishDateFrom");
LocalDate finishTo = parseDate(finishDateTo, "finishDateTo");
validateRange(applyFrom, applyTo, "applyDateFrom", "applyDateTo");
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<DeliveryOrder> wrapper = new LambdaQueryWrapper<DeliveryOrder>()
.eq(DeliveryOrder::getSoftDelete, 0)
.eq(agentId != null, DeliveryOrder::getAgentId, agentId)
.eq(orderType != null, DeliveryOrder::getOrderType, orderType)
.eq(orderState != null, DeliveryOrder::getOrderState, orderState)
.eq(sendMode != null, DeliveryOrder::getSendMode, sendMode)
.ge(applyFrom != null, DeliveryOrder::getApplyTime, startOfDay(applyFrom))
.lt(applyTo != null, DeliveryOrder::getApplyTime, nextDayStart(applyTo))
.ge(finishFrom != null, DeliveryOrder::getFinishDate, startOfDay(finishFrom))
.lt(finishTo != null, DeliveryOrder::getFinishDate, nextDayStart(finishTo));
like(wrapper, DeliveryOrder::getDeliveryOrderNo, deliveryOrderNo);
like(wrapper, DeliveryOrder::getInstallOrderNo, sourceOrderNo);
like(wrapper, DeliveryOrder::getDistrictName, districtName);
like(wrapper, DeliveryOrder::getApplyUserName, applyUserName);
permissionService.applyDeliveryOrderRowFilter(wrapper, currentUser());
long total = deliveryOrderMapper.selectCount(wrapper);
List<DeliveryOrder> orders = deliveryOrderMapper.selectList(wrapper
.orderByDesc(DeliveryOrder::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 = "delivery.order.detail.query", description = "按 id 或送货工单号查询送货工单详情。"
+ "行级权限与 delivery.order.list.query 完全一致;不存在与无权查看统一返回 NOT_FOUND_OR_FORBIDDEN。"
+ "返回主单、物流、配发物品、发货附件、已处理流程及签收附件、关联出入库/调拨/划转单。附件仅返回 bucket 与 objectKey。")
public Map<String, Object> deliveryOrderDetailQuery(
@ToolParam(required = false, description = "送货工单主键 id,与 deliveryOrderNo 至少传一个") Long id,
@ToolParam(required = false, description = "送货工单号,与 id 至少传一个") String deliveryOrderNo) {
if ((id == null || id <= 0) && !StringUtils.hasText(deliveryOrderNo)) {
throw new IllegalArgumentException("id、deliveryOrderNo 至少传一个");
}
if (id != null && id <= 0) {
throw new IllegalArgumentException("id 必须为正整数");
}
LambdaQueryWrapper<DeliveryOrder> wrapper = new LambdaQueryWrapper<DeliveryOrder>()
.eq(DeliveryOrder::getSoftDelete, 0)
.eq(id != null, DeliveryOrder::getId, id)
.eq(StringUtils.hasText(deliveryOrderNo), DeliveryOrder::getDeliveryOrderNo,
StringUtils.hasText(deliveryOrderNo) ? deliveryOrderNo.trim() : deliveryOrderNo);
permissionService.applyDeliveryOrderRowFilter(wrapper, currentUser());
List<DeliveryOrder> visible = deliveryOrderMapper.selectList(
wrapper.orderByDesc(DeliveryOrder::getId).last("LIMIT 1"));
if (visible.isEmpty()) {
throw new ToolCallException(McpErrorCode.NOT_FOUND_OR_FORBIDDEN,
"送货工单不存在或当前身份无权查看");
}
DeliveryOrder order = visible.get(0);
String orderNo = order.getDeliveryOrderNo();
List<InstallDistributeDetail> goods = detailMapper.selectList(
new LambdaQueryWrapper<InstallDistributeDetail>()
.eq(InstallDistributeDetail::getSoftDelete, 0)
.eq(InstallDistributeDetail::getDeliveryOrderNo, orderNo)
.orderByAsc(InstallDistributeDetail::getId));
List<DeliveryOrderProcess> processes = processMapper.selectList(
new LambdaQueryWrapper<DeliveryOrderProcess>()
.eq(DeliveryOrderProcess::getSoftDelete, 0)
.eq(DeliveryOrderProcess::getDeliveryOrderNo, orderNo)
.eq(DeliveryOrderProcess::getScheduleState, 2)
.orderByAsc(DeliveryOrderProcess::getOperationTime)
.orderByAsc(DeliveryOrderProcess::getId));
Map<Long, List<WarehouseInOutPic>> receiptFiles = receiptFiles(processes);
List<InstallToOutWarehouse> relations = relationMapper.selectList(
new LambdaQueryWrapper<InstallToOutWarehouse>()
.eq(InstallToOutWarehouse::getSoftDelete, 0)
.eq(InstallToOutWarehouse::getDeliveryOrderNo, orderNo)
.orderByAsc(InstallToOutWarehouse::getId));
Map<String, Object> result = new LinkedHashMap<>();
result.put("record", orderRecord(order));
result.put("logistics", logisticsRecord(order));
result.put("goods", goods.stream().map(this::goodsRecord).collect(Collectors.toList()));
result.put("shippingFiles", shippingFiles(orderNo));
result.put("processes", processes.stream()
.map(process -> processRecord(process, receiptFiles.getOrDefault(process.getId(), List.of())))
.collect(Collectors.toList()));
result.put("relatedDocuments", relations.stream().map(this::relationRecord).collect(Collectors.toList()));
result.put("relatedInstallOrderNo", order.getInstallOrderNo());
return result;
}
private Map<String, Object> orderRecord(DeliveryOrder order) {
Map<String, Object> record = new LinkedHashMap<>();
record.put("id", order.getId());
record.put("deliveryOrderNo", order.getDeliveryOrderNo());
record.put("sourceOrderNo", order.getInstallOrderNo());
record.put("orderType", order.getOrderType());
record.put("orderTypeText", text(ORDER_TYPE_TEXT, order.getOrderType()));
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("orderState", order.getOrderState());
record.put("orderStateText", text(ORDER_STATE_TEXT, order.getOrderState()));
record.put("sendMode", order.getSendMode());
record.put("sendModeText", text(SEND_MODE_TEXT, order.getSendMode()));
record.put("applyUserId", order.getApplyUserId());
record.put("applyUserName", order.getApplyUserName());
record.put("applyTime", formatTime(order.getApplyTime()));
record.put("finishDate", formatTime(order.getFinishDate()));
record.put("scheduleUserName", order.getScheduleUserName());
record.put("pickupOrderNo", order.getPickupOrderNo());
record.put("createTime", formatTime(order.getCreateTime()));
record.put("modifyTime", formatTime(order.getModifyTime()));
return record;
}
private Map<String, Object> logisticsRecord(DeliveryOrder order) {
List<MallOrderLogistics> rows = logisticsMapper.selectList(
new LambdaQueryWrapper<MallOrderLogistics>()
.eq(MallOrderLogistics::getSoftDelete, 0)
.eq(MallOrderLogistics::getOrderNo, order.getDeliveryOrderNo())
.orderByDesc(MallOrderLogistics::getId).last("LIMIT 1"));
if (rows.isEmpty()) {
return null;
}
MallOrderLogistics logistics = rows.get(0);
Map<String, Object> record = new LinkedHashMap<>();
record.put("sendMode", logistics.getSendMode());
record.put("sendModeText", text(SEND_MODE_TEXT, logistics.getSendMode()));
record.put("logisticsId", logistics.getLogisticsId());
record.put("logisticsNo", logistics.getLogisticsNo());
record.put("logisticsName", logistics.getLogisticsName());
record.put("logisticsOrderNo", logistics.getLogisticsOrderNo());
record.put("deliverName", logistics.getDeliverName());
record.put("deliverNumber", logistics.getDeliverNumber());
record.put("state", logistics.getState());
record.put("stateText", text(LOGISTICS_STATE_TEXT, logistics.getState()));
record.put("sendTime", formatTime(logistics.getModifyTime()));
return record;
}
private Map<String, Object> goodsRecord(InstallDistributeDetail goods) {
Map<String, Object> record = new LinkedHashMap<>();
record.put("id", goods.getId());
record.put("materialNo", goods.getMaterialNo());
record.put("materielFullName", goods.getMaterielFullName());
record.put("materielSort", goods.getMaterielSort());
record.put("productSort", goods.getProductSort());
record.put("specifications", goods.getSpecifications());
record.put("snCode", goods.getSnCode());
record.put("outCount", goods.getOutCount());
record.put("outWarehouseId", goods.getOutWarehouseId());
record.put("outWarehouseName", goods.getOutWarehouseName());
record.put("inWarehouseId", goods.getInWarehouseId());
record.put("inWarehouseName", goods.getInWarehouseName());
record.put("outInOrderNo", goods.getOutInOrderNo());
record.put("actionType", goods.getActionType());
record.put("actionTypeText", text(DOCUMENT_TYPE_TEXT, goods.getActionType()));
record.put("mallOrderNo", goods.getMallOrderNo());
record.put("freeFlag", goods.getFreeFlag());
record.put("payFlag", goods.getPayFlag());
return record;
}
private List<Map<String, Object>> shippingFiles(String orderNo) {
return shippingPicMapper.selectList(new LambdaQueryWrapper<MallLogisticsPic>()
.eq(MallLogisticsPic::getSoftDelete, 0)
.eq(MallLogisticsPic::getState, 0)
.eq(MallLogisticsPic::getOrderNo, orderNo)
.orderByAsc(MallLogisticsPic::getId)).stream()
.map(pic -> fileRecord(pic.getId(), pic.getFileName(),
pic.getOssbucketname(), pic.getOssfolder()))
.collect(Collectors.toList());
}
private Map<Long, List<WarehouseInOutPic>> receiptFiles(List<DeliveryOrderProcess> processes) {
if (processes.isEmpty()) {
return Map.of();
}
List<Long> ids = processes.stream().map(DeliveryOrderProcess::getId).collect(Collectors.toList());
return receiptPicMapper.selectList(new LambdaQueryWrapper<WarehouseInOutPic>()
.eq(WarehouseInOutPic::getSoftDelete, 0)
.in(WarehouseInOutPic::getAggregateId, ids)
.eq(WarehouseInOutPic::getFileType, RECEIPT_FILE_TYPE)
.eq(WarehouseInOutPic::getInOutAllocationType, DELIVERY_AGGREGATE_TYPE)
.orderByAsc(WarehouseInOutPic::getId)).stream()
.collect(Collectors.groupingBy(WarehouseInOutPic::getAggregateId,
LinkedHashMap::new, Collectors.toList()));
}
private Map<String, Object> processRecord(DeliveryOrderProcess process,
List<WarehouseInOutPic> files) {
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("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("files", files.stream().map(pic -> fileRecord(pic.getId(), pic.getFileName(),
pic.getOssbucketname(), pic.getOssfolder())).collect(Collectors.toList()));
return record;
}
private Map<String, Object> relationRecord(InstallToOutWarehouse relation) {
Map<String, Object> record = new LinkedHashMap<>();
record.put("id", relation.getId());
record.put("documentType", relation.getOutWarehouseType());
record.put("documentTypeText", text(DOCUMENT_TYPE_TEXT, relation.getOutWarehouseType()));
record.put("documentNo", relation.getOrderNo());
return record;
}
private static Map<String, Object> fileRecord(Long id, String fileName, String bucket, String folder) {
Map<String, Object> record = new LinkedHashMap<>();
record.put("id", id);
record.put("fileName", fileName);
record.put("bucket", bucket);
record.put("objectKey", nullToEmpty(folder) + nullToEmpty(fileName));
return record;
}
private static User currentUser() {
return AuthContextHolder.get().getUser();
}
private static void like(LambdaQueryWrapper<DeliveryOrder> wrapper,
SFunction<DeliveryOrder, ?> 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 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 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 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;
}
}
@@ -10,6 +10,7 @@ import me.iiv.agent.entity.m0.Properyt;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallGoodsOrder;
import me.iiv.agent.entity.m2.ContractNew;
import me.iiv.agent.entity.m2.DeliveryOrder;
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
import me.iiv.agent.entity.m2.WarehouseIn;
import me.iiv.agent.entity.m2.WarehouseOut;
@@ -192,6 +193,34 @@ public class PermissionService {
.or().apply(participantExists, user.getId()));
}
/**
* 送货工单权限:总部看全部;销售非负责人按自己创建/代管物业;其余代理身份
* 可看申请代理、所属代理、代管物业范围,以及本人参与过流程的工单。
* 列表和详情必须复用本方法,避免详情绕过行权限。
*/
public void applyDeliveryOrderRowFilter(LambdaQueryWrapper<DeliveryOrder> wrapper, User user) {
RowScope scope = resolveRowScope(user);
if (scope.headquarters) {
return;
}
String participantExists = "EXISTS (SELECT 1 FROM delivery_order_process dop "
+ "WHERE dop.delivery_order_no = delivery_order.delivery_order_no "
+ "AND dop.soft_delete = 0 AND dop.user_id = {0})";
if (scope.salesPropertyIds != null) {
wrapper.and(w -> w.in(DeliveryOrder::getPropertyId, scope.salesPropertyIds)
.or().apply(participantExists, user.getId()));
return;
}
wrapper.and(w -> {
w.in(DeliveryOrder::getApplyAgentId, scope.ids)
.or().in(DeliveryOrder::getAgentId, scope.ids);
if (!scope.managedPropertyIds.isEmpty()) {
w.or().in(DeliveryOrder::getPropertyId, scope.managedPropertyIds);
}
w.or().apply(participantExists, user.getId());
});
}
private static String placeholders(int offset, int size) {
return IntStream.range(offset, offset + size)
.mapToObj(i -> "{" + i + "}")
@@ -0,0 +1,228 @@
package me.iiv.agent.mcp.tool;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallLogisticsPic;
import me.iiv.agent.entity.m1.MallOrderLogistics;
import me.iiv.agent.entity.m2.DeliveryOrder;
import me.iiv.agent.entity.m2.DeliveryOrderProcess;
import me.iiv.agent.entity.m2.InstallDistributeDetail;
import me.iiv.agent.entity.m2.InstallToOutWarehouse;
import me.iiv.agent.entity.m2.WarehouseInOutPic;
import me.iiv.agent.mapper.m1.MallLogisticsPicMapper;
import me.iiv.agent.mapper.m1.MallOrderLogisticsMapper;
import me.iiv.agent.mapper.m2.DeliveryOrderMapper;
import me.iiv.agent.mapper.m2.DeliveryOrderProcessMapper;
import me.iiv.agent.mapper.m2.InstallDistributeDetailMapper;
import me.iiv.agent.mapper.m2.InstallToOutWarehouseMapper;
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.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 DeliveryOrderToolsTest {
@Mock DeliveryOrderMapper deliveryOrderMapper;
@Mock DeliveryOrderProcessMapper processMapper;
@Mock InstallDistributeDetailMapper detailMapper;
@Mock InstallToOutWarehouseMapper relationMapper;
@Mock WarehouseInOutPicMapper receiptPicMapper;
@Mock MallOrderLogisticsMapper logisticsMapper;
@Mock MallLogisticsPicMapper shippingPicMapper;
@Mock PermissionService permissionService;
DeliveryOrderTools tools;
@BeforeEach
void setUp() {
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
TableInfoHelper.initTableInfo(assistant, DeliveryOrder.class);
TableInfoHelper.initTableInfo(assistant, DeliveryOrderProcess.class);
TableInfoHelper.initTableInfo(assistant, InstallDistributeDetail.class);
TableInfoHelper.initTableInfo(assistant, InstallToOutWarehouse.class);
TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class);
TableInfoHelper.initTableInfo(assistant, MallOrderLogistics.class);
TableInfoHelper.initTableInfo(assistant, MallLogisticsPic.class);
tools = new DeliveryOrderTools(deliveryOrderMapper, processMapper, detailMapper, relationMapper,
receiptPicMapper, logisticsMapper, shippingPicMapper, permissionService);
User user = new User();
user.setId(7L);
AuthContextHolder.set(new AuthContext(null, user));
}
@AfterEach
void tearDown() {
AuthContextHolder.clear();
}
@Test
void listReturnsTextsAndClampsRows() {
DeliveryOrder order = order();
when(deliveryOrderMapper.selectCount(any())).thenReturn(1L);
when(deliveryOrderMapper.selectList(any())).thenReturn(List.of(order));
Map<String, Object> result = tools.deliveryOrderListQuery("SH", "AZ", 10L, "项目",
0, 1, 0, "申请人", "2026-09-01", "2026-09-30",
"2026-09-01", "2026-09-30", 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("orderStateText"));
assertEquals("装机单送货", record.get("orderTypeText"));
assertEquals("快递公司", record.get("sendModeText"));
verify(permissionService).applyDeliveryOrderRowFilter(any(), any());
}
@Test
void listRejectsInvalidCodesAndDatesBeforeQuery() {
assertThrows(IllegalArgumentException.class, () -> tools.deliveryOrderListQuery(null, null,
null, null, 3, null, null, null, null, null, null, null, null, null));
assertThrows(IllegalArgumentException.class, () -> tools.deliveryOrderListQuery(null, null,
null, null, null, null, null, null, "2026-09-30", "2026-09-01",
null, null, null, null));
verifyNoInteractions(deliveryOrderMapper, permissionService);
}
@Test
void detailUsesSamePermissionAndHidesExistence() {
when(deliveryOrderMapper.selectList(any())).thenReturn(List.of());
ToolCallException error = assertThrows(ToolCallException.class,
() -> tools.deliveryOrderDetailQuery(20L, null));
assertEquals(McpErrorCode.NOT_FOUND_OR_FORBIDDEN, error.getCode());
verify(permissionService).applyDeliveryOrderRowFilter(any(), any());
verifyNoInteractions(detailMapper, processMapper, logisticsMapper, shippingPicMapper);
}
@Test
void detailReturnsGoodsLogisticsFilesProcessesAndRelations() {
when(deliveryOrderMapper.selectList(any())).thenReturn(List.of(order()));
InstallDistributeDetail goods = new InstallDistributeDetail();
goods.setId(3L);
goods.setMaterialNo("MAT-1");
goods.setSnCode("SN-1");
goods.setActionType(1);
when(detailMapper.selectList(any())).thenReturn(List.of(goods));
DeliveryOrderProcess process = new DeliveryOrderProcess();
process.setId(4L);
process.setScheduleState(2);
process.setOperationTheme(1);
process.setOperationType(6);
when(processMapper.selectList(any())).thenReturn(List.of(process));
WarehouseInOutPic receipt = new WarehouseInOutPic();
receipt.setId(5L);
receipt.setAggregateId(4L);
receipt.setFileName("receipt.jpg");
receipt.setOssbucketname("bucket-a");
receipt.setOssfolder("receipt/");
when(receiptPicMapper.selectList(any())).thenReturn(List.of(receipt));
InstallToOutWarehouse relation = new InstallToOutWarehouse();
relation.setId(6L);
relation.setOutWarehouseType(3);
relation.setOrderNo("DB-1");
when(relationMapper.selectList(any())).thenReturn(List.of(relation));
MallOrderLogistics logistics = new MallOrderLogistics();
logistics.setSendMode(0);
logistics.setLogisticsName("快递公司");
logistics.setState(7);
when(logisticsMapper.selectList(any())).thenReturn(List.of(logistics));
MallLogisticsPic shipping = new MallLogisticsPic();
shipping.setId(7L);
shipping.setFileName("ship.jpg");
shipping.setOssbucketname("bucket-b");
shipping.setOssfolder("shipping/");
when(shippingPicMapper.selectList(any())).thenReturn(List.of(shipping));
Map<String, Object> result = tools.deliveryOrderDetailQuery(20L, "SH-001");
@SuppressWarnings("unchecked")
Map<String, Object> logisticsResult = (Map<String, Object>) result.get("logistics");
assertEquals("已签收", logisticsResult.get("stateText"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> goodsResult = (List<Map<String, Object>>) result.get("goods");
assertEquals("MAT-1", goodsResult.get(0).get("materialNo"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> shippingFiles = (List<Map<String, Object>>) result.get("shippingFiles");
assertEquals("shipping/ship.jpg", shippingFiles.get(0).get("objectKey"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> processes = (List<Map<String, Object>>) result.get("processes");
assertEquals("收货确认", processes.get(0).get("operationTypeText"));
assertEquals(1, ((List<?>) processes.get(0).get("files")).size());
@SuppressWarnings("unchecked")
List<Map<String, Object>> documents = (List<Map<String, Object>>) result.get("relatedDocuments");
assertEquals("调拨单", documents.get(0).get("documentTypeText"));
assertEquals("AZ-001", result.get("relatedInstallOrderNo"));
verify(permissionService).applyDeliveryOrderRowFilter(any(), any());
}
@Test
void detailReturnsNullLogisticsWhenNoRecordExists() {
when(deliveryOrderMapper.selectList(any())).thenReturn(List.of(order()));
when(detailMapper.selectList(any())).thenReturn(List.of());
when(processMapper.selectList(any())).thenReturn(List.of());
when(relationMapper.selectList(any())).thenReturn(List.of());
when(logisticsMapper.selectList(any())).thenReturn(List.of());
when(shippingPicMapper.selectList(any())).thenReturn(List.of());
Map<String, Object> result = tools.deliveryOrderDetailQuery(20L, null);
assertNull(result.get("logistics"));
assertEquals(List.of(), result.get("processes"));
verifyNoInteractions(receiptPicMapper);
}
@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("delivery.order.list.query").contains("finishDateTo"));
assertTrue(schemas.get("delivery.order.detail.query").contains("deliveryOrderNo"));
}
private static DeliveryOrder order() {
DeliveryOrder order = new DeliveryOrder();
order.setId(20L);
order.setDeliveryOrderNo("SH-001");
order.setInstallOrderNo("AZ-001");
order.setOrderType(0);
order.setOrderState(1);
order.setSendMode(0);
return order;
}
}
@@ -0,0 +1,110 @@
package me.iiv.agent.permission;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import me.iiv.agent.entity.m0.Agent;
import me.iiv.agent.entity.m0.AgentManageDistrictAuth;
import me.iiv.agent.entity.m0.Authority;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m2.DeliveryOrder;
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.StringRedisTemplate;
import java.util.Collection;
import java.util.List;
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.when;
@ExtendWith(MockitoExtension.class)
class PermissionServiceDeliveryOrderRowFilterTest {
@Mock AuthorityMapper authorityMapper;
@Mock AgentMapper agentMapper;
@Mock WarehouseAgentAuthMapper warehouseAgentAuthMapper;
@Mock AgentManageDistrictAuthMapper agentManageDistrictAuthMapper;
@Mock ProperytMapper properytMapper;
@Mock StringRedisTemplate redisTemplate;
PermissionService service;
@BeforeEach
void setUp() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), DeliveryOrder.class);
service = new PermissionService(authorityMapper, agentMapper, warehouseAgentAuthMapper,
agentManageDistrictAuthMapper, properytMapper, redisTemplate);
}
@Test
void headquartersAddsNoFilter() {
when(authorityMapper.selectById(9L)).thenReturn(authority(0));
LambdaQueryWrapper<DeliveryOrder> wrapper = new LambdaQueryWrapper<>();
service.applyDeliveryOrderRowFilter(wrapper, user(7L, 9L, null));
assertTrue(wrapper.getSqlSegment().isBlank());
}
@Test
void agentCombinesApplyAgentOwnerManagedPropertyAndParticipant() {
when(authorityMapper.selectById(9L)).thenReturn(authority(1));
Agent child = new Agent();
child.setId(11L);
when(agentMapper.selectList(any())).thenReturn(List.of(child));
WarehouseAgentAuth managed = new WarehouseAgentAuth();
managed.setManagerAgentId(12L);
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managed));
AgentManageDistrictAuth property = new AgentManageDistrictAuth();
property.setPropertyId(30L);
when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of(property));
LambdaQueryWrapper<DeliveryOrder> wrapper = new LambdaQueryWrapper<>();
service.applyDeliveryOrderRowFilter(wrapper, user(7L, 9L, 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("delivery_order_process"));
assertTrue(values.containsAll(List.of(7L, 10L, 11L, 12L, 30L)));
}
@Test
void agentWithoutAgentFailsClosed() {
when(authorityMapper.selectById(9L)).thenReturn(authority(1));
assertThrows(IllegalStateException.class, () -> service.applyDeliveryOrderRowFilter(
new LambdaQueryWrapper<>(), user(7L, 9L, null)));
}
private static User user(Long id, Long authorityId, Long agentId) {
User user = new User();
user.setId(id);
user.setAuthorityId(authorityId);
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;
}
}