diff --git a/xiaoqu-agent-service/README.md b/xiaoqu-agent-service/README.md index 2772803..96d4b6f 100644 --- a/xiaoqu-agent-service/README.md +++ b/xiaoqu-agent-service/README.md @@ -87,6 +87,8 @@ xiaoqu-agent-service/ | `warehouse.todo.list.query` | warehouse | read | 当前用户仓库待办统一查询(REQ-20260915-0008),合并老系统出入库/调拨三个待办接口;流程视图 user_id 过滤,不按代理商过滤;调拨派生出入库单带 derivedFrom 标记 | | `delivery.order.list.query` | workorder | read | 送货工单分页查询;申请/完成日期独立过滤,代理、代管物业及流程参与人权限取并集 | | `delivery.order.detail.query` | workorder | read | 送货工单详情;返回物流、配发物品、发货/签收附件、流程及关联单据 | +| `install.order.list.query` | workorder | read | 装机工单分页查询;仅保留申请日期范围,返回状态文案与安装数量汇总 | +| `install.order.detail.query` | workorder | read | 装机工单详情;返回明细、作业人员、地址、商城订单、流程、配发 SN、出入库关联及凭证 | 分页统一规范:page 从 1 开始,rows 默认 20、上限 100,超出截断为 100 并在返回 `notice` 字段提示。 diff --git a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md index 04e4651..693ac76 100644 --- a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md +++ b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md @@ -615,6 +615,39 @@ --- +## 22. install.order.list.query + +分页查询装机工单,对应旧系统 `POST /installOrder/pageList`。 + +- 分类:workorder | 类型:读 +- 数据来源:m2 `install_order`、`install_order_details`。 +- 入参:`installOrderNo`、`mallOrderNo`、`scheduleTransferOrder`、`deliveryOrderNo`、 + `pickupOrderNo`、`installType`、`orderState`、`agentId`、`districtName`、`applyUserName`、 + `maintenanceUserName`、`cooperateUserName`、`applyTimeFrom/applyTimeTo`、`page/rows`。 +- 时间范围首版只保留申请日期,格式 `yyyy-MM-dd`,结束日期含当天;不接受自由排序,固定 + `apply_time DESC, id DESC`。 +- 行级权限:总部看全部;普通代理可看自己、授权代管公司、授权代管物业及本人参与的工单, + 不包含直属下级代理;销售非负责人只看自己创建及授权代管公司名下物业的工单。 +- 返回 `total/page/rows/records`;每条记录含 `installTypeText`、`orderStateText` 和 + `totalInstallCount`(本单设备明细安装数量合计)。 + +## 23. install.order.detail.query + +按 `id` 或 `installOrderNo` 查询装机工单详情,两者至少传一个;同时传入时必须匹配同一条可见记录。 +`includeProcess` 默认 true,可关闭流程聚合。 + +- 行级权限与列表完全一致;不存在与无权访问统一返回 `record=null`,防止枚举。 +- `record/details/totalInstallCount`:主单、设备明细及安装数量合计。 +- `maintenanceUsers/cooperateUsers`:当前装机运维和协同人员。 +- `address`:m1 收货地址;`mallOrder`:m1 商城订单及商品明细聚合。 +- `processes`:已办理且非抄送的流程,按操作时间、ID 正序;附流程人员、配发物品/SN、 + 出入库关联单及图片凭证 URL。 +- `processLimit` 默认 100、最大 500;同时返回 `processTotal/processTruncated`。 + +详细设计见 `install-order-mcp-tool-design.md`。 + +--- + ## 附录:在线查看工具清单 ```bash diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderMaterial.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderMaterial.java new file mode 100644 index 0000000..0f34952 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderMaterial.java @@ -0,0 +1,43 @@ +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; +import java.math.BigDecimal; + +/** 商城订单物料(m1,只读)。 */ +@Getter +@Setter +@ToString +@TableName("mall_order_materials") +public class MallOrderMaterial implements Serializable { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + private Integer softDelete; + private String spuNo; + private String skuNo; + private String skuLable; + private String skuDesc; + private String spuName; + private String orderNo; + private Long mallOrderDetailsId; + private Integer warehouseState; + private Long warehouseOutId; + private Long materielDefinitionId; + private Long parentNodeId; + private String materialNo; + private String materielFullName; + private String materielSort; + private Long materielIndexId; + private String specifications; + private Integer outCount; + private Integer notOutCount; + private Integer beenOutCount; + private BigDecimal price; + private Integer freeFlag; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrder.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrder.java new file mode 100644 index 0000000..fad9253 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrder.java @@ -0,0 +1,63 @@ +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("install_order") +public class InstallOrder 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 Integer installType; + private String installOrderNo; + private Long agentId; + private Long propertyId; + private Long districtId; + private String agentName; + private String propertyName; + private String districtName; + private String hopeInstallDate; + private String trueInstallDate; + private String customerContact; + private String contactMobile; + private String projectAddress; + private String projectMemo; + private Integer orderState; + private Long applyUserId; + private String applyUserName; + private Date applyTime; + private Date auditTime; + private Date dispatchTime; + private Date allocationTime; + private Date signatureTime; + private Date pickupTime; + private Date warehousingTime; + private Date finishDate; + private String auditUserName; + private String dispatchUserName; + private String maintenanceUserName; + private String cooperateUserName; + private String scheduleUserName; + private String scheduleTransferOrder; + private String deliveryOrderNo; + private String pickupOrderNo; + private String mallOrderNo; + private Long addressId; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderDetail.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderDetail.java new file mode 100644 index 0000000..04850d5 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderDetail.java @@ -0,0 +1,37 @@ +package me.iiv.agent.entity.m2; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** 装机工单设备明细(m2,只读)。 */ +@Getter +@Setter +@ToString +@TableName("install_order_details") +public class InstallOrderDetail implements Serializable { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + private Integer softDelete; + private String installOrderNo; + private Integer deviceTypeId; + private String deviceTypeName; + private Integer installCount; + private Integer installPositionId; + private String installPositionName; + private Integer installMaterialId; + private String installMaterialName; + private Integer consumablesId; + private String consumablesName; + private String consumablesMemo; + private Integer consumablesCount; + private Integer deviceRemoveFlag; + private Integer useElectricityFlag; + private Integer devicePayFlag; + private Integer consumablesPayFlag; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderProcess.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderProcess.java new file mode 100644 index 0000000..6456670 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderProcess.java @@ -0,0 +1,31 @@ +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("install_order_process") +public class InstallOrderProcess implements Serializable { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + private Integer softDelete; + private String installOrderNo; + private Long userId; + private String userName; + private Date operationTime; + private Integer scheduleState; + private Integer auditState; + private Integer operationTheme; + private Integer operationType; + private String memo; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderUser.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderUser.java new file mode 100644 index 0000000..9180777 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/InstallOrderUser.java @@ -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_order_user") +public class InstallOrderUser implements Serializable { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + private Integer softDelete; + private Long installOrderProcessId; + private String installOrderNo; + private Long userId; + private String userName; + private Integer userType; + private Integer type; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderMaterialMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderMaterialMapper.java new file mode 100644 index 0000000..0f25160 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderMaterialMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m1; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m1.MallOrderMaterial; + +public interface MallOrderMaterialMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderDetailMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderDetailMapper.java new file mode 100644 index 0000000..ede6985 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderDetailMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m2; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m2.InstallOrderDetail; + +public interface InstallOrderDetailMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderMapper.java new file mode 100644 index 0000000..81460be --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m2; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m2.InstallOrder; + +public interface InstallOrderMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderProcessMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderProcessMapper.java new file mode 100644 index 0000000..3b5c83b --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderProcessMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m2; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m2.InstallOrderProcess; + +public interface InstallOrderProcessMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderUserMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderUserMapper.java new file mode 100644 index 0000000..ef1a91e --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m2/InstallOrderUserMapper.java @@ -0,0 +1,7 @@ +package me.iiv.agent.mapper.m2; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m2.InstallOrderUser; + +public interface InstallOrderUserMapper extends BaseMapper { +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/InstallOrderTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/InstallOrderTools.java new file mode 100644 index 0000000..4ccf9b9 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/InstallOrderTools.java @@ -0,0 +1,605 @@ +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.MallGoodsOrder; +import me.iiv.agent.entity.m1.MallLogisticsPic; +import me.iiv.agent.entity.m1.MallOrderDetails; +import me.iiv.agent.entity.m1.MallOrderMaterial; +import me.iiv.agent.entity.m1.MallShippingAddress; +import me.iiv.agent.entity.m2.InstallDistributeDetail; +import me.iiv.agent.entity.m2.InstallOrder; +import me.iiv.agent.entity.m2.InstallOrderDetail; +import me.iiv.agent.entity.m2.InstallOrderProcess; +import me.iiv.agent.entity.m2.InstallOrderUser; +import me.iiv.agent.entity.m2.InstallToOutWarehouse; +import me.iiv.agent.entity.m2.WarehouseInOutPic; +import me.iiv.agent.mapper.m1.MallGoodsOrderMapper; +import me.iiv.agent.mapper.m1.MallLogisticsPicMapper; +import me.iiv.agent.mapper.m1.MallOrderDetailsMapper; +import me.iiv.agent.mapper.m1.MallOrderMaterialMapper; +import me.iiv.agent.mapper.m1.MallShippingAddressMapper; +import me.iiv.agent.mapper.m2.InstallDistributeDetailMapper; +import me.iiv.agent.mapper.m2.InstallOrderDetailMapper; +import me.iiv.agent.mapper.m2.InstallOrderMapper; +import me.iiv.agent.mapper.m2.InstallOrderProcessMapper; +import me.iiv.agent.mapper.m2.InstallOrderUserMapper; +import me.iiv.agent.mapper.m2.InstallToOutWarehouseMapper; +import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper; +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.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** 装机工单查询工具,对齐老系统 /installOrder/pageList 与 /lookDetails。 */ +@Component +public class InstallOrderTools implements ToolObject { + + private static final int DEFAULT_ROWS = 20; + private static final int MAX_ROWS = 100; + private static final int DEFAULT_PROCESS_LIMIT = 100; + private static final int MAX_PROCESS_LIMIT = 500; + private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai"); + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ISO_LOCAL_DATE.withResolverStyle(ResolverStyle.STRICT); + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(BUSINESS_ZONE); + + private static final Map ORDER_STATE_TEXT = Map.of( + 0, "待审核", 1, "被驳回", 2, "调度中", 3, "待出库", 4, "待送货", + 5, "作业中", 6, "待取回", 7, "已结束", 8, "已取消"); + private static final Map INSTALL_TYPE_TEXT = Map.of( + 0, "正式装机", 1, "测试装机", 2, "销售装机"); + private static final Map SCHEDULE_STATE_TEXT = Map.of( + 0, "无需待办", 1, "待办", 2, "已办"); + private static final Map 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, "取货结单")); + + final InstallOrderMapper orderMapper; + final InstallOrderDetailMapper detailMapper; + final InstallOrderProcessMapper processMapper; + final InstallOrderUserMapper userMapper; + final InstallDistributeDetailMapper distributeMapper; + final InstallToOutWarehouseMapper warehouseRelationMapper; + final WarehouseInOutPicMapper picMapper; + final MallGoodsOrderMapper mallOrderMapper; + final MallOrderDetailsMapper mallDetailMapper; + final MallOrderMaterialMapper mallMaterialMapper; + final MallShippingAddressMapper addressMapper; + final MallLogisticsPicMapper mallPicMapper; + final PermissionService permissionService; + + public InstallOrderTools(InstallOrderMapper orderMapper, + InstallOrderDetailMapper detailMapper, + InstallOrderProcessMapper processMapper, + InstallOrderUserMapper userMapper, + InstallDistributeDetailMapper distributeMapper, + InstallToOutWarehouseMapper warehouseRelationMapper, + WarehouseInOutPicMapper picMapper, + MallGoodsOrderMapper mallOrderMapper, + MallOrderDetailsMapper mallDetailMapper, + MallOrderMaterialMapper mallMaterialMapper, + MallShippingAddressMapper addressMapper, + MallLogisticsPicMapper mallPicMapper, + PermissionService permissionService) { + this.orderMapper = orderMapper; + this.detailMapper = detailMapper; + this.processMapper = processMapper; + this.userMapper = userMapper; + this.distributeMapper = distributeMapper; + this.warehouseRelationMapper = warehouseRelationMapper; + this.picMapper = picMapper; + this.mallOrderMapper = mallOrderMapper; + this.mallDetailMapper = mallDetailMapper; + this.mallMaterialMapper = mallMaterialMapper; + this.addressMapper = addressMapper; + this.mallPicMapper = mallPicMapper; + this.permissionService = permissionService; + } + + @XqTool(category = "workorder", kind = "read") + @Tool(name = "install.order.list.query", description = "分页查询当前用户可见的装机工单,对齐老系统 /installOrder/pageList。" + + "支持装机单、商城订单、待办划转单、送货单、取货单模糊查询,类型、状态、公司精确查询,以及项目/人员模糊查询。" + + "申请日期格式 yyyy-MM-dd,结束日期含当天。page 默认1,rows 默认20、上限100,固定按申请时间和 id 倒序。" + + "返回 orderStateText 与设备明细安装数量汇总 totalInstallCount。") + public Map installOrderListQuery( + @ToolParam(required = false, description = "装机工单号,模糊匹配") String installOrderNo, + @ToolParam(required = false, description = "商城订单号,模糊匹配") String mallOrderNo, + @ToolParam(required = false, description = "待办划转单号,模糊匹配") String scheduleTransferOrder, + @ToolParam(required = false, description = "送货工单号,模糊匹配") String deliveryOrderNo, + @ToolParam(required = false, description = "取货工单号,模糊匹配") String pickupOrderNo, + @ToolParam(required = false, description = "装机类型:0正式装机 1测试装机 2销售装机") Integer installType, + @ToolParam(required = false, description = "装机工单状态:0待审核 1被驳回 2调度中 3待出库 4待送货 5作业中 6待取回 7已结束 8已取消") Integer orderState, + @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 applyTimeFrom, + @ToolParam(required = false, description = "申请日期止,yyyy-MM-dd,含当天") String applyTimeTo, + @ToolParam(required = false, description = "页码,从1开始,默认1") Integer page, + @ToolParam(required = false, description = "每页条数,默认20,上限100") Integer rows) { + LocalDate start = parseDate(applyTimeFrom, "applyTimeFrom"); + LocalDate end = parseDate(applyTimeTo, "applyTimeTo"); + if (start != null && end != null && start.isAfter(end)) { + throw new IllegalArgumentException("applyTimeFrom 不能晚于 applyTimeTo"); + } + int pageNo = page == null || page < 1 ? 1 : page; + boolean truncated = rows != null && rows > MAX_ROWS; + int pageSize = rows == null || rows < 1 ? DEFAULT_ROWS : Math.min(rows, MAX_ROWS); + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(InstallOrder::getSoftDelete, 0); + like(wrapper, InstallOrder::getInstallOrderNo, installOrderNo); + like(wrapper, InstallOrder::getMallOrderNo, mallOrderNo); + like(wrapper, InstallOrder::getScheduleTransferOrder, scheduleTransferOrder); + like(wrapper, InstallOrder::getDeliveryOrderNo, deliveryOrderNo); + like(wrapper, InstallOrder::getPickupOrderNo, pickupOrderNo); + like(wrapper, InstallOrder::getDistrictName, districtName); + like(wrapper, InstallOrder::getApplyUserName, applyUserName); + like(wrapper, InstallOrder::getMaintenanceUserName, maintenanceUserName); + like(wrapper, InstallOrder::getCooperateUserName, cooperateUserName); + wrapper.eq(installType != null, InstallOrder::getInstallType, installType) + .eq(orderState != null, InstallOrder::getOrderState, orderState) + .eq(agentId != null, InstallOrder::getAgentId, agentId) + .ge(start != null, InstallOrder::getApplyTime, start == null ? null : startOfDay(start)) + .lt(end != null, InstallOrder::getApplyTime, end == null ? null : nextDayStart(end)); + permissionService.applyInstallOrderRowFilter(wrapper, AuthContextHolder.get().getUser()); + + Long total = orderMapper.selectCount(wrapper); + wrapper.orderByDesc(InstallOrder::getApplyTime).orderByDesc(InstallOrder::getId) + .last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize); + List orders = orderMapper.selectList(wrapper); + Map installCounts = totalInstallCounts(orders); + List> records = orders.stream() + .map(order -> { + Map record = record(order); + record.put("totalInstallCount", installCounts.getOrDefault(order.getInstallOrderNo(), 0)); + return record; + }).collect(Collectors.toList()); + + Map result = new LinkedHashMap<>(); + result.put("total", total); + result.put("page", pageNo); + result.put("rows", pageSize); + if (truncated) result.put("notice", "rows 超过上限 100,已截断为 100"); + result.put("records", records); + return result; + } + + @XqTool(category = "workorder", kind = "read") + @Tool(name = "install.order.detail.query", description = "按 id 或装机工单号查询装机工单详情。" + + "行级权限与列表一致;不存在或无权访问统一返回 record=null。" + + "返回主单、设备明细、运维/协同人员、收货地址、商城订单聚合和已办理流程;" + + "流程包含配发 SN、关联出入库单及图片凭证 URL。processLimit 默认100、上限500。") + public Map installOrderDetailQuery( + @ToolParam(required = false, description = "装机工单主键 id,与 installOrderNo 至少传一个") Long id, + @ToolParam(required = false, description = "装机工单号,与 id 至少传一个") String installOrderNo, + @ToolParam(required = false, description = "是否返回流程记录,默认 true") Boolean includeProcess, + @ToolParam(required = false, description = "流程返回上限,默认100、最大500") Integer processLimit) { + if ((id == null || id <= 0) && !StringUtils.hasText(installOrderNo)) { + throw new IllegalArgumentException("id、installOrderNo 至少传一个"); + } + if (id != null && id <= 0) throw new IllegalArgumentException("id 必须为正整数"); + int limit = processLimit == null || processLimit < 1 + ? DEFAULT_PROCESS_LIMIT : Math.min(processLimit, MAX_PROCESS_LIMIT); + boolean include = includeProcess == null || includeProcess; + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(InstallOrder::getSoftDelete, 0) + .eq(id != null, InstallOrder::getId, id) + .eq(StringUtils.hasText(installOrderNo), InstallOrder::getInstallOrderNo, + StringUtils.hasText(installOrderNo) ? installOrderNo.trim() : installOrderNo); + permissionService.applyInstallOrderRowFilter(wrapper, AuthContextHolder.get().getUser()); + List visible = orderMapper.selectList(wrapper.orderByDesc(InstallOrder::getId).last("LIMIT 1")); + if (visible.isEmpty()) { + Map result = new LinkedHashMap<>(); + result.put("record", null); + result.put("notice", "工单不存在或无权查看"); + return result; + } + + InstallOrder order = visible.get(0); + String no = order.getInstallOrderNo(); + List details = detailMapper.selectList(new LambdaQueryWrapper() + .eq(InstallOrderDetail::getSoftDelete, 0) + .eq(InstallOrderDetail::getInstallOrderNo, no) + .orderByAsc(InstallOrderDetail::getId)); + List users = userMapper.selectList(new LambdaQueryWrapper() + .eq(InstallOrderUser::getSoftDelete, 0) + .eq(InstallOrderUser::getInstallOrderNo, no) + .eq(InstallOrderUser::getType, 0) + .orderByAsc(InstallOrderUser::getId)); + Long processTotal = include ? processMapper.selectCount(processBase(no)) : 0L; + List processes = include ? processMapper.selectList(processBase(no) + .orderByAsc(InstallOrderProcess::getOperationTime) + .orderByAsc(InstallOrderProcess::getId).last("LIMIT " + limit)) : List.of(); + + Map> processUsers = processUsers(processes); + List distributes = include ? distributeMapper.selectList( + new LambdaQueryWrapper() + .eq(InstallDistributeDetail::getSoftDelete, 0) + .eq(InstallDistributeDetail::getInstallOrderNo, no) + .orderByAsc(InstallDistributeDetail::getId)) : List.of(); + List warehouseRelations = include ? warehouseRelationMapper.selectList( + new LambdaQueryWrapper() + .eq(InstallToOutWarehouse::getSoftDelete, 0) + .eq(InstallToOutWarehouse::getInstallOrderNo, no) + .orderByAsc(InstallToOutWarehouse::getId)) : List.of(); + Map> pics = processPics(processes); + Map> mallPics = mallProcessPics(processes); + + Map result = new LinkedHashMap<>(); + result.put("record", record(order)); + result.put("details", details.stream().map(this::detailRecord).collect(Collectors.toList())); + result.put("totalInstallCount", details.stream().map(InstallOrderDetail::getInstallCount) + .filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); + result.put("maintenanceUsers", personnel(users, 0)); + result.put("cooperateUsers", personnel(users, 1)); + result.put("addressInfo", address(order)); + result.put("mallOrder", mallOrder(order)); + result.put("processes", processes.stream().map(process -> processRecord(process, + processUsers.getOrDefault(process.getId(), List.of()), distributes, + warehouseRelations, pics.getOrDefault(process.getId(), List.of()), + mallPics.getOrDefault(process.getId(), List.of()))).collect(Collectors.toList())); + result.put("processTotal", processTotal); + result.put("processTruncated", processTotal != null && processTotal > processes.size()); + return result; + } + + private LambdaQueryWrapper processBase(String no) { + return new LambdaQueryWrapper() + .eq(InstallOrderProcess::getSoftDelete, 0) + .eq(InstallOrderProcess::getInstallOrderNo, no) + .eq(InstallOrderProcess::getScheduleState, 2) + .ne(InstallOrderProcess::getOperationTheme, 12); + } + + private Map totalInstallCounts(List orders) { + if (orders.isEmpty()) return Map.of(); + List orderNos = orders.stream().map(InstallOrder::getInstallOrderNo) + .filter(StringUtils::hasText).collect(Collectors.toList()); + if (orderNos.isEmpty()) return Map.of(); + return detailMapper.selectList(new LambdaQueryWrapper() + .eq(InstallOrderDetail::getSoftDelete, 0) + .in(InstallOrderDetail::getInstallOrderNo, orderNos)) + .stream().collect(Collectors.groupingBy(InstallOrderDetail::getInstallOrderNo, + LinkedHashMap::new, Collectors.summingInt(row -> + row.getInstallCount() == null ? 0 : row.getInstallCount()))); + } + + private Map> processUsers(List processes) { + if (processes.isEmpty()) return Map.of(); + List ids = processes.stream().map(InstallOrderProcess::getId).collect(Collectors.toList()); + return userMapper.selectList(new LambdaQueryWrapper() + .eq(InstallOrderUser::getSoftDelete, 0) + .in(InstallOrderUser::getInstallOrderProcessId, ids) + .orderByAsc(InstallOrderUser::getId)).stream() + .collect(Collectors.groupingBy(InstallOrderUser::getInstallOrderProcessId, + LinkedHashMap::new, Collectors.toList())); + } + + private Map> processPics(List processes) { + if (processes.isEmpty()) return Map.of(); + List ids = processes.stream().map(InstallOrderProcess::getId).collect(Collectors.toList()); + return picMapper.selectList(new LambdaQueryWrapper() + .eq(WarehouseInOutPic::getSoftDelete, 0) + .in(WarehouseInOutPic::getAggregateId, ids) + .orderByAsc(WarehouseInOutPic::getId)).stream() + .collect(Collectors.groupingBy(WarehouseInOutPic::getAggregateId, + LinkedHashMap::new, Collectors.toList())); + } + + private Map> mallProcessPics(List processes) { + if (processes.isEmpty()) return Map.of(); + Map processIds = processes.stream().collect(Collectors.toMap( + process -> process.getId().toString(), InstallOrderProcess::getId)); + return mallPicMapper.selectList(new LambdaQueryWrapper() + .eq(MallLogisticsPic::getSoftDelete, 0) + .in(MallLogisticsPic::getOrderNo, processIds.keySet()) + .orderByAsc(MallLogisticsPic::getId)).stream() + .filter(pic -> processIds.containsKey(pic.getOrderNo())) + .collect(Collectors.groupingBy(pic -> processIds.get(pic.getOrderNo()), + LinkedHashMap::new, Collectors.toList())); + } + + private Map record(InstallOrder order) { + Map r = new LinkedHashMap<>(); + r.put("id", order.getId()); + r.put("createTime", formatTime(order.getCreateTime())); + r.put("modifyTime", formatTime(order.getModifyTime())); + r.put("installOrderNo", order.getInstallOrderNo()); + r.put("installType", order.getInstallType()); + r.put("installTypeText", text(INSTALL_TYPE_TEXT, order.getInstallType())); + r.put("orderState", order.getOrderState()); + r.put("orderStateText", text(ORDER_STATE_TEXT, order.getOrderState())); + r.put("applyAgentId", order.getApplyAgentId()); + r.put("applyAgentName", order.getApplyAgentName()); + r.put("agentId", order.getAgentId()); + r.put("agentName", order.getAgentName()); + r.put("propertyId", order.getPropertyId()); + r.put("propertyName", order.getPropertyName()); + r.put("districtId", order.getDistrictId()); + r.put("districtName", order.getDistrictName()); + r.put("hopeInstallDate", order.getHopeInstallDate()); + r.put("trueInstallDate", order.getTrueInstallDate()); + r.put("customerContact", order.getCustomerContact()); + r.put("contactMobile", order.getContactMobile()); + r.put("projectAddress", order.getProjectAddress()); + r.put("projectMemo", order.getProjectMemo()); + r.put("applyUserId", order.getApplyUserId()); + r.put("applyUserName", order.getApplyUserName()); + r.put("applyTime", formatTime(order.getApplyTime())); + r.put("auditTime", formatTime(order.getAuditTime())); + r.put("dispatchTime", formatTime(order.getDispatchTime())); + r.put("allocationTime", formatTime(order.getAllocationTime())); + r.put("signatureTime", formatTime(order.getSignatureTime())); + r.put("pickupTime", formatTime(order.getPickupTime())); + r.put("warehousingTime", formatTime(order.getWarehousingTime())); + r.put("finishDate", formatTime(order.getFinishDate())); + r.put("auditUserName", order.getAuditUserName()); + r.put("dispatchUserName", order.getDispatchUserName()); + r.put("maintenanceUserName", order.getMaintenanceUserName()); + r.put("cooperateUserName", order.getCooperateUserName()); + r.put("scheduleUserName", order.getScheduleUserName()); + r.put("scheduleTransferOrder", order.getScheduleTransferOrder()); + r.put("deliveryOrderNo", order.getDeliveryOrderNo()); + r.put("pickupOrderNo", order.getPickupOrderNo()); + r.put("mallOrderNo", order.getMallOrderNo()); + r.put("addressId", order.getAddressId()); + return r; + } + + private Map detailRecord(InstallOrderDetail d) { + Map r = new LinkedHashMap<>(); + r.put("id", d.getId()); + r.put("deviceTypeId", d.getDeviceTypeId()); + r.put("deviceTypeName", d.getDeviceTypeName()); + r.put("installCount", d.getInstallCount()); + r.put("installPositionId", d.getInstallPositionId()); + r.put("installPositionName", d.getInstallPositionName()); + r.put("installMaterialId", d.getInstallMaterialId()); + r.put("installMaterialName", d.getInstallMaterialName()); + r.put("consumablesId", d.getConsumablesId()); + r.put("consumablesName", d.getConsumablesName()); + r.put("consumablesMemo", d.getConsumablesMemo()); + r.put("consumablesCount", d.getConsumablesCount()); + r.put("deviceRemoveFlag", d.getDeviceRemoveFlag()); + r.put("useElectricityFlag", d.getUseElectricityFlag()); + r.put("devicePayFlag", d.getDevicePayFlag()); + r.put("consumablesPayFlag", d.getConsumablesPayFlag()); + return r; + } + + private List> personnel(List users, int type) { + return users.stream().filter(user -> Integer.valueOf(type).equals(user.getUserType())) + .map(user -> { + Map r = new LinkedHashMap<>(); + r.put("id", user.getId()); + r.put("userId", user.getUserId()); + r.put("userName", user.getUserName()); + return r; + }).collect(Collectors.toList()); + } + + private Map address(InstallOrder order) { + if (order.getAddressId() == null || order.getAddressId() <= 0) return null; + MallShippingAddress address = addressMapper.selectById(order.getAddressId()); + if (address == null || !Integer.valueOf(0).equals(address.getSoftDelete())) return null; + Map r = new LinkedHashMap<>(); + r.put("id", address.getId()); + r.put("receiverName", address.getReveiveName()); + r.put("receiverPhone", address.getReveiveNumber()); + r.put("province", address.getProvince()); + r.put("city", address.getCity()); + r.put("county", address.getCountyseat()); + r.put("detail", address.getAddress()); + r.put("fullAddress", join(address.getProvince(), address.getCity(), address.getCountyseat(), address.getAddress())); + r.put("memo", address.getMemo()); + return r; + } + + private Map mallOrder(InstallOrder installOrder) { + if (!StringUtils.hasText(installOrder.getMallOrderNo())) return null; + List orders = mallOrderMapper.selectList(new LambdaQueryWrapper() + .eq(MallGoodsOrder::getSoftDelete, 0) + .eq(MallGoodsOrder::getOrderNo, installOrder.getMallOrderNo().trim()) + .orderByDesc(MallGoodsOrder::getId).last("LIMIT 1")); + if (orders.isEmpty()) return null; + MallGoodsOrder order = orders.get(0); + List items = mallDetailMapper.selectList(new LambdaQueryWrapper() + .eq(MallOrderDetails::getSoftDelete, 0) + .eq(MallOrderDetails::getOrderNo, order.getOrderNo()) + .orderByDesc(MallOrderDetails::getId)); + List materials = mallMaterialMapper.selectList( + new LambdaQueryWrapper() + .eq(MallOrderMaterial::getSoftDelete, 0) + .eq(MallOrderMaterial::getOrderNo, order.getOrderNo()) + .orderByAsc(MallOrderMaterial::getId)); + Map r = new LinkedHashMap<>(); + r.put("id", order.getId()); + r.put("orderNo", order.getOrderNo()); + r.put("orderState", order.getOrderState()); + r.put("payState", order.getPayState()); + r.put("orderTime", formatTime(order.getOrderTime())); + r.put("memo", order.getMemo()); + r.put("totalMoney", order.getTotalMoney()); + r.put("items", items.stream().map(this::mallItem).collect(Collectors.toList())); + r.put("materials", materials.stream().map(this::mallMaterial).collect(Collectors.toList())); + return r; + } + + private Map mallItem(MallOrderDetails item) { + Map r = new LinkedHashMap<>(); + r.put("id", item.getId()); + r.put("spuNo", item.getSpuNo()); + r.put("skuNo", item.getSkuNo()); + r.put("spuName", item.getSpuName()); + r.put("spuType", item.getSpuType()); + r.put("count", item.getCount()); + r.put("price", item.getPrice()); + r.put("goodsType", item.getGoodsType()); + r.put("skuDesc", item.getSkuDesc()); + r.put("skuLabel", item.getSkuLable()); + r.put("picUrl", item.getPicUrl()); + return r; + } + + private Map mallMaterial(MallOrderMaterial material) { + Map r = new LinkedHashMap<>(); + r.put("id", material.getId()); + r.put("mallOrderDetailsId", material.getMallOrderDetailsId()); + r.put("materialDefinitionId", material.getMaterielDefinitionId()); + r.put("materialNo", material.getMaterialNo()); + r.put("materialName", material.getMaterielFullName()); + r.put("materialCategory", material.getMaterielSort()); + r.put("specifications", material.getSpecifications()); + r.put("outCount", material.getOutCount()); + r.put("notOutCount", material.getNotOutCount()); + r.put("beenOutCount", material.getBeenOutCount()); + r.put("warehouseState", material.getWarehouseState()); + r.put("price", material.getPrice()); + r.put("freeFlag", material.getFreeFlag()); + return r; + } + + private Map processRecord(InstallOrderProcess process, List users, + List distributes, + List relations, + List pics, + List mallPics) { + Map r = new LinkedHashMap<>(); + r.put("id", process.getId()); + r.put("userId", process.getUserId()); + r.put("userName", process.getUserName()); + r.put("operationTime", formatTime(process.getOperationTime())); + r.put("scheduleState", process.getScheduleState()); + r.put("scheduleStateText", text(SCHEDULE_STATE_TEXT, process.getScheduleState())); + r.put("auditState", process.getAuditState()); + r.put("operationTheme", process.getOperationTheme()); + r.put("operationThemeText", text(OPERATION_THEME_TEXT, process.getOperationTheme())); + r.put("operationType", process.getOperationType()); + r.put("memo", process.getMemo()); + r.put("maintenanceUsers", personnel(users, 0)); + r.put("cooperateUsers", personnel(users, 1)); + r.put("distributedItems", Integer.valueOf(3).equals(process.getOperationTheme()) + ? distributes.stream().map(this::distributeRecord).collect(Collectors.toList()) : List.of()); + r.put("warehouseOrders", relations.stream().map(this::warehouseRelationRecord).collect(Collectors.toList())); + List> certificates = pics.stream().map(this::picRecord) + .collect(Collectors.toCollection(ArrayList::new)); + certificates.addAll(mallPics.stream().map(this::mallPicRecord).collect(Collectors.toList())); + r.put("certificates", certificates); + return r; + } + + private Map distributeRecord(InstallDistributeDetail d) { + Map r = new LinkedHashMap<>(); + r.put("id", d.getId()); + r.put("materialDefinitionId", d.getMaterielDefinitionId()); + r.put("materialNo", d.getMaterialNo()); + r.put("materialName", d.getMaterielFullName()); + r.put("specifications", d.getSpecifications()); + r.put("snCode", d.getSnCode()); + r.put("outCount", d.getOutCount()); + r.put("outWarehouseId", d.getOutWarehouseId()); + r.put("outWarehouseName", d.getOutWarehouseName()); + r.put("inWarehouseId", d.getInWarehouseId()); + r.put("inWarehouseName", d.getInWarehouseName()); + r.put("outInOrderNo", d.getOutInOrderNo()); + r.put("actionType", d.getActionType()); + r.put("mallOrderNo", d.getMallOrderNo()); + return r; + } + + private Map warehouseRelationRecord(InstallToOutWarehouse relation) { + Map r = new LinkedHashMap<>(); + r.put("id", relation.getId()); + r.put("orderType", relation.getOrderType()); + r.put("outWarehouseType", relation.getOutWarehouseType()); + r.put("orderNo", relation.getOrderNo()); + r.put("deliveryOrderNo", relation.getDeliveryOrderNo()); + r.put("pickupOrderNo", relation.getPickupOrderNo()); + return r; + } + + private Map picRecord(WarehouseInOutPic pic) { + Map r = new LinkedHashMap<>(); + r.put("id", pic.getId()); + r.put("fileName", pic.getFileName()); + r.put("fileType", pic.getFileType()); + r.put("url", ossUrl(pic.getOssbucketname(), pic.getOssfolder(), pic.getFileName())); + return r; + } + + private Map mallPicRecord(MallLogisticsPic pic) { + Map r = new LinkedHashMap<>(); + r.put("id", pic.getId()); + r.put("fileName", pic.getFileName()); + r.put("source", "mall_logistics_pic"); + r.put("url", ossUrl(pic.getOssbucketname(), pic.getOssfolder(), pic.getFileName())); + return r; + } + + private static void like(LambdaQueryWrapper wrapper, SFunction column, String value) { + if (StringUtils.hasText(value)) wrapper.like(column, value.trim()); + } + + private static LocalDate parseDate(String value, String field) { + if (!StringUtils.hasText(value)) return null; + try { + return LocalDate.parse(value.trim(), DATE_FORMATTER); + } catch (DateTimeParseException ex) { + throw new IllegalArgumentException(field + " 必须是有效的 yyyy-MM-dd 日期"); + } + } + + private static Date startOfDay(LocalDate date) { + return Date.from(date.atStartOfDay(BUSINESS_ZONE).toInstant()); + } + + private static Date nextDayStart(LocalDate date) { + return startOfDay(date.plusDays(1)); + } + + private static String formatTime(Date value) { + return value == null ? null : TIME_FORMATTER.format(value.toInstant()); + } + + private static String text(Map texts, Integer value) { + return value == null ? null : texts.getOrDefault(value, "未知(" + value + ")"); + } + + private static String join(String... values) { + StringBuilder result = new StringBuilder(); + for (String value : values) if (StringUtils.hasText(value)) result.append(value.trim()); + return result.toString(); + } + + private static String ossUrl(String bucket, String folder, String fileName) { + if (!StringUtils.hasText(bucket) || !StringUtils.hasText(fileName)) return null; + return "https://" + bucket + ".oss-cn-beijing.aliyuncs.com/" + + (folder == null ? "" : folder) + fileName; + } +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java index 3569e7c..9dabfda 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java @@ -11,6 +11,7 @@ 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.InstallOrder; import me.iiv.agent.entity.m2.WarehouseAgentAuth; import me.iiv.agent.entity.m2.WarehouseIn; import me.iiv.agent.entity.m2.WarehouseOut; @@ -221,6 +222,64 @@ public class PermissionService { }); } + /** + * 装机工单权限,1:1 对齐老 getWorkOrderAuth(type=0) 的有效业务口径:总部看全部; + * 销售非负责人只看自己创建物业及授权代管公司的物业;其他代理用户可看自己、 + * 授权代管公司、授权代管物业,以及本人出现在流程或作业人员表中的工单。 + * 与通用物业权限不同,本规则不包含直属下级代理。 + */ + public void applyInstallOrderRowFilter(LambdaQueryWrapper wrapper, User user) { + if (!isAgentAuthority(user.getAuthorityId())) { + return; + } + if (user.getAgentId() == null) { + throw new IllegalStateException("代理商用户未关联代理商,拒绝访问业务数据"); + } + + List auths = managedAuths(user.getId()); + List agentIds = new ArrayList<>(); + agentIds.add(user.getAgentId()); + agentIds.addAll(auths.stream().map(WarehouseAgentAuth::getManagerAgentId) + .collect(Collectors.toList())); + + if (isSalesNonLeader(user)) { + List propertyIds = properytMapper.selectList(new LambdaQueryWrapper() + .eq(Properyt::getSoftDelete, 0) + .eq(Properyt::getCreateId, user.getId()) + .select(Properyt::getId)) + .stream().map(Properyt::getId).collect(Collectors.toList()); + List managedAgentIds = auths.stream().map(WarehouseAgentAuth::getManagerAgentId) + .collect(Collectors.toList()); + if (!managedAgentIds.isEmpty()) { + propertyIds.addAll(properytMapper.selectList(new LambdaQueryWrapper() + .eq(Properyt::getSoftDelete, 0) + .in(Properyt::getAgentId, managedAgentIds) + .select(Properyt::getId)) + .stream().map(Properyt::getId).collect(Collectors.toList())); + } + wrapper.in(InstallOrder::getPropertyId, + propertyIds.isEmpty() ? List.of(0L) : new LinkedHashSet<>(propertyIds)); + return; + } + + List managedPropertyIds = managedPropertyIds(agentIds); + String processParticipant = "EXISTS (SELECT 1 FROM install_order_process iop " + + "WHERE iop.install_order_no = install_order.install_order_no " + + "AND iop.soft_delete = 0 AND iop.user_id = {0})"; + String workerParticipant = "EXISTS (SELECT 1 FROM install_order_user iou " + + "WHERE iou.install_order_no = install_order.install_order_no " + + "AND iou.soft_delete = 0 AND iou.user_id = {0})"; + wrapper.and(w -> { + w.in(InstallOrder::getApplyAgentId, agentIds) + .or().in(InstallOrder::getAgentId, agentIds); + if (!managedPropertyIds.isEmpty()) { + w.or().in(InstallOrder::getPropertyId, managedPropertyIds); + } + w.or().apply(processParticipant, user.getId()) + .or().apply(workerParticipant, user.getId()); + }); + } + private static String placeholders(int offset, int size) { return IntStream.range(offset, offset + size) .mapToObj(i -> "{" + i + "}") diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/codegen/CodeGenerator.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/codegen/CodeGenerator.java index fbfd83b..ee9d69b 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/codegen/CodeGenerator.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/codegen/CodeGenerator.java @@ -41,8 +41,12 @@ public class CodeGenerator { public static void main(String[] args) { generate(envOr("CODEGEN_SCHEMA_M0", "xiaoqu_comples_d"), "m0", "user", "agent", "authority", "properyt", "agent_manage_district_auth", "invoice_info"); + generate(envOr("CODEGEN_SCHEMA_M1", "xiaoqu_mall_d"), "m1", + "mall_goods_order", "mall_order_details", "mall_order_materials", "mall_shipping_address", + "mall_logistics_pic"); generate(envOr("CODEGEN_SCHEMA_M2", "xiaoqu_franchisee_d"), "m2", - "warehouse_agent_auth"); + "warehouse_agent_auth", "install_order", "install_order_details", "install_order_process", + "install_order_user", "install_distribute_details", "install_to_out_warehouse"); generate(envOr("CODEGEN_SCHEMA_SVC", "xiaoqu_agent_svc_d"), "svc", "agent_api_key", "agent_scope", "tool", "scope_tool", "token_scope", "audit_log", "admin_user"); } diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/InstallOrderToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/InstallOrderToolsTest.java new file mode 100644 index 0000000..b58ea20 --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/InstallOrderToolsTest.java @@ -0,0 +1,291 @@ +package me.iiv.agent.mcp.tool; + +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.User; +import me.iiv.agent.entity.m1.MallGoodsOrder; +import me.iiv.agent.entity.m1.MallLogisticsPic; +import me.iiv.agent.entity.m1.MallOrderDetails; +import me.iiv.agent.entity.m1.MallOrderMaterial; +import me.iiv.agent.entity.m1.MallShippingAddress; +import me.iiv.agent.entity.m2.InstallDistributeDetail; +import me.iiv.agent.entity.m2.InstallOrder; +import me.iiv.agent.entity.m2.InstallOrderDetail; +import me.iiv.agent.entity.m2.InstallOrderProcess; +import me.iiv.agent.entity.m2.InstallOrderUser; +import me.iiv.agent.entity.m2.InstallToOutWarehouse; +import me.iiv.agent.entity.m2.WarehouseInOutPic; +import me.iiv.agent.mapper.m1.MallGoodsOrderMapper; +import me.iiv.agent.mapper.m1.MallLogisticsPicMapper; +import me.iiv.agent.mapper.m1.MallOrderDetailsMapper; +import me.iiv.agent.mapper.m1.MallOrderMaterialMapper; +import me.iiv.agent.mapper.m1.MallShippingAddressMapper; +import me.iiv.agent.mapper.m2.InstallDistributeDetailMapper; +import me.iiv.agent.mapper.m2.InstallOrderDetailMapper; +import me.iiv.agent.mapper.m2.InstallOrderMapper; +import me.iiv.agent.mapper.m2.InstallOrderProcessMapper; +import me.iiv.agent.mapper.m2.InstallOrderUserMapper; +import me.iiv.agent.mapper.m2.InstallToOutWarehouseMapper; +import me.iiv.agent.mapper.m2.WarehouseInOutPicMapper; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +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 InstallOrderToolsTest { + @Mock InstallOrderMapper orderMapper; + @Mock InstallOrderDetailMapper detailMapper; + @Mock InstallOrderProcessMapper processMapper; + @Mock InstallOrderUserMapper userMapper; + @Mock InstallDistributeDetailMapper distributeMapper; + @Mock InstallToOutWarehouseMapper warehouseRelationMapper; + @Mock WarehouseInOutPicMapper picMapper; + @Mock MallGoodsOrderMapper mallOrderMapper; + @Mock MallOrderDetailsMapper mallDetailMapper; + @Mock MallOrderMaterialMapper mallMaterialMapper; + @Mock MallShippingAddressMapper addressMapper; + @Mock MallLogisticsPicMapper mallPicMapper; + @Mock PermissionService permissionService; + InstallOrderTools tools; + + @BeforeEach + void setUp() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, InstallOrder.class); + TableInfoHelper.initTableInfo(assistant, InstallOrderDetail.class); + TableInfoHelper.initTableInfo(assistant, InstallOrderProcess.class); + TableInfoHelper.initTableInfo(assistant, InstallOrderUser.class); + TableInfoHelper.initTableInfo(assistant, InstallDistributeDetail.class); + TableInfoHelper.initTableInfo(assistant, InstallToOutWarehouse.class); + TableInfoHelper.initTableInfo(assistant, WarehouseInOutPic.class); + TableInfoHelper.initTableInfo(assistant, MallGoodsOrder.class); + TableInfoHelper.initTableInfo(assistant, MallOrderDetails.class); + TableInfoHelper.initTableInfo(assistant, MallOrderMaterial.class); + TableInfoHelper.initTableInfo(assistant, MallShippingAddress.class); + TableInfoHelper.initTableInfo(assistant, MallLogisticsPic.class); + tools = new InstallOrderTools(orderMapper, detailMapper, processMapper, userMapper, + distributeMapper, warehouseRelationMapper, picMapper, mallOrderMapper, + mallDetailMapper, mallMaterialMapper, addressMapper, mallPicMapper, permissionService); + User user = new User(); + user.setId(7L); + AuthContextHolder.set(new AuthContext(null, user)); + } + + @AfterEach + void tearDown() { + AuthContextHolder.clear(); + } + + @Test + void listAppliesFiltersPermissionPaginationAndTotalInstallCount() { + InstallOrder order = order(); + InstallOrderDetail first = detail(2); + InstallOrderDetail second = detail(3); + when(orderMapper.selectCount(any())).thenReturn(1L); + when(orderMapper.selectList(any())).thenReturn(List.of(order)); + when(detailMapper.selectList(any())).thenReturn(List.of(first, second)); + + Map result = tools.installOrderListQuery(" ZJ ", " XS ", null, + null, null, 2, 5, 10L, " 项目 ", " 张 ", " 李 ", " 王 ", + "2026-09-01", "2026-09-16", 0, 500); + + 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 record = ((List>) result.get("records")).get(0); + assertEquals("作业中", record.get("orderStateText")); + assertEquals("销售装机", record.get("installTypeText")); + assertEquals(5, record.get("totalInstallCount")); + verify(permissionService).applyInstallOrderRowFilter(any(), any()); + + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(orderMapper).selectList(captor.capture()); + String sql = captor.getValue().getSqlSegment(); + assertTrue(sql.contains("install_order_no LIKE")); + assertTrue(sql.contains("apply_time >=")); + assertTrue(sql.contains("apply_time <")); + assertTrue(sql.contains("apply_time DESC")); + assertTrue(sql.contains("id DESC")); + assertTrue(sql.contains("LIMIT 0,100")); + } + + @Test + void listRejectsInvalidAndReversedApplyDates() { + assertThrows(IllegalArgumentException.class, () -> listWithDates("2026-02-30", null)); + assertThrows(IllegalArgumentException.class, () -> listWithDates("2026-09-17", "2026-09-16")); + verifyNoInteractions(orderMapper, permissionService); + } + + @Test + void detailHidesMissingAndForbiddenOrder() { + when(orderMapper.selectList(any())).thenReturn(List.of()); + + Map result = tools.installOrderDetailQuery(20L, null, null, null); + + assertNull(result.get("record")); + assertEquals("工单不存在或无权查看", result.get("notice")); + verify(permissionService).applyInstallOrderRowFilter(any(), any()); + verifyNoInteractions(detailMapper, processMapper, userMapper, distributeMapper, + warehouseRelationMapper, picMapper, mallOrderMapper, mallDetailMapper, + mallMaterialMapper, addressMapper, mallPicMapper); + } + + @Test + void detailCanSkipProcesses() { + InstallOrder order = order(); + order.setMallOrderNo(null); + when(orderMapper.selectList(any())).thenReturn(List.of(order)); + when(detailMapper.selectList(any())).thenReturn(List.of()); + when(userMapper.selectList(any())).thenReturn(List.of()); + + Map result = tools.installOrderDetailQuery(20L, null, false, null); + + assertEquals(List.of(), result.get("processes")); + assertEquals(0L, result.get("processTotal")); + verifyNoInteractions(processMapper, distributeMapper, warehouseRelationMapper, picMapper, mallPicMapper); + } + + @Test + void detailReturnsPeopleMallAggregationAndProcessRelations() { + InstallOrder order = order(); + order.setAddressId(30L); + when(orderMapper.selectList(any())).thenReturn(List.of(order)); + when(detailMapper.selectList(any())).thenReturn(List.of(detail(2))); + InstallOrderUser worker = new InstallOrderUser(); + worker.setId(1L); + worker.setInstallOrderNo("ZJ-001"); + worker.setUserType(0); + worker.setType(0); + worker.setUserName("运维甲"); + InstallOrderUser collaborator = new InstallOrderUser(); + collaborator.setId(2L); + collaborator.setInstallOrderNo("ZJ-001"); + collaborator.setUserType(1); + collaborator.setType(0); + collaborator.setUserName("协同乙"); + when(userMapper.selectList(any())).thenReturn(List.of(worker, collaborator), List.of()); + InstallOrderProcess process = new InstallOrderProcess(); + process.setId(40L); + process.setOperationTheme(3); + process.setScheduleState(2); + when(processMapper.selectCount(any())).thenReturn(1L); + when(processMapper.selectList(any())).thenReturn(List.of(process)); + InstallDistributeDetail distribute = new InstallDistributeDetail(); + distribute.setId(50L); + distribute.setSnCode("SN-1"); + when(distributeMapper.selectList(any())).thenReturn(List.of(distribute)); + InstallToOutWarehouse relation = new InstallToOutWarehouse(); + relation.setId(60L); + relation.setOrderNo("CK-1"); + when(warehouseRelationMapper.selectList(any())).thenReturn(List.of(relation)); + WarehouseInOutPic pic = new WarehouseInOutPic(); + pic.setId(70L); + pic.setAggregateId(40L); + pic.setOssbucketname("bucket"); + pic.setOssfolder("folder/"); + pic.setFileName("proof.jpg"); + when(picMapper.selectList(any())).thenReturn(List.of(pic)); + when(mallPicMapper.selectList(any())).thenReturn(List.of()); + MallShippingAddress address = new MallShippingAddress(); + address.setId(30L); + address.setSoftDelete(0); + address.setProvince("北京"); + address.setCity("北京市"); + address.setAddress("朝阳区1号"); + when(addressMapper.selectById(30L)).thenReturn(address); + MallGoodsOrder mallOrder = new MallGoodsOrder(); + mallOrder.setId(80L); + mallOrder.setOrderNo("XS-001"); + mallOrder.setTotalMoney(BigDecimal.TEN); + when(mallOrderMapper.selectList(any())).thenReturn(List.of(mallOrder)); + MallOrderDetails mallItem = new MallOrderDetails(); + mallItem.setId(81L); + mallItem.setSpuName("纸机"); + when(mallDetailMapper.selectList(any())).thenReturn(List.of(mallItem)); + MallOrderMaterial material = new MallOrderMaterial(); + material.setId(82L); + material.setMaterialNo("MAT-1"); + when(mallMaterialMapper.selectList(any())).thenReturn(List.of(material)); + + Map result = tools.installOrderDetailQuery(null, "ZJ-001", true, 1); + + assertEquals(2, result.get("totalInstallCount")); + assertEquals(1, ((List) result.get("maintenanceUsers")).size()); + assertEquals(1, ((List) result.get("cooperateUsers")).size()); + @SuppressWarnings("unchecked") + Map mall = (Map) result.get("mallOrder"); + assertEquals("XS-001", mall.get("orderNo")); + assertEquals(1, ((List) mall.get("items")).size()); + assertEquals(1, ((List) mall.get("materials")).size()); + @SuppressWarnings("unchecked") + Map processRecord = ((List>) result.get("processes")).get(0); + assertEquals(1, ((List) processRecord.get("distributedItems")).size()); + assertEquals(1, ((List) processRecord.get("warehouseOrders")).size()); + @SuppressWarnings("unchecked") + Map certificate = ((List>) processRecord.get("certificates")).get(0); + assertEquals("https://bucket.oss-cn-beijing.aliyuncs.com/folder/proof.jpg", certificate.get("url")); + } + + @Test + void springAiPublishesBothWorkorderSchemas() { + var callbacks = org.springframework.ai.tool.method.MethodToolCallbackProvider.builder() + .toolObjects(tools).build().getToolCallbacks(); + Map schemas = Arrays.stream(callbacks).collect(java.util.stream.Collectors.toMap( + callback -> callback.getToolDefinition().name(), callback -> callback.getToolDefinition().inputSchema())); + + assertEquals(2, schemas.size()); + assertTrue(schemas.get("install.order.list.query").contains("applyTimeFrom")); + assertTrue(schemas.get("install.order.list.query").contains("maintenanceUserName")); + assertTrue(schemas.get("install.order.detail.query").contains("processLimit")); + assertTrue(schemas.get("install.order.detail.query").contains("includeProcess")); + } + + private Map listWithDates(String from, String to) { + return tools.installOrderListQuery(null, null, null, null, null, null, null, null, + null, null, null, null, from, to, 1, 20); + } + + private static InstallOrder order() { + InstallOrder order = new InstallOrder(); + order.setId(20L); + order.setInstallOrderNo("ZJ-001"); + order.setMallOrderNo("XS-001"); + order.setInstallType(2); + order.setOrderState(5); + return order; + } + + private static InstallOrderDetail detail(int count) { + InstallOrderDetail detail = new InstallOrderDetail(); + detail.setId(100L + count); + detail.setInstallOrderNo("ZJ-001"); + detail.setInstallCount(count); + return detail; + } +} diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceInstallOrderRowFilterTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceInstallOrderRowFilterTest.java new file mode 100644 index 0000000..2f7b804 --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceInstallOrderRowFilterTest.java @@ -0,0 +1,132 @@ +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.Authority; +import me.iiv.agent.entity.m0.Properyt; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.entity.m2.InstallOrder; +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 org.springframework.data.redis.core.HashOperations; + +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PermissionServiceInstallOrderRowFilterTest { + @Mock AuthorityMapper authorityMapper; + @Mock AgentMapper agentMapper; + @Mock WarehouseAgentAuthMapper warehouseAgentAuthMapper; + @Mock AgentManageDistrictAuthMapper agentManageDistrictAuthMapper; + @Mock ProperytMapper properytMapper; + @Mock StringRedisTemplate redisTemplate; + @Mock HashOperations hashOperations; + PermissionService service; + + @BeforeEach + void setUp() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, InstallOrder.class); + TableInfoHelper.initTableInfo(assistant, Properyt.class); + service = new PermissionService(authorityMapper, agentMapper, warehouseAgentAuthMapper, + agentManageDistrictAuthMapper, properytMapper, redisTemplate); + } + + @Test + void headquartersAddsNoFilter() { + User user = user(7L, 9L, null); + when(authorityMapper.selectById(9L)).thenReturn(authority(9L, 0)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyInstallOrderRowFilter(wrapper, user); + + assertTrue(wrapper.getSqlSegment().isBlank()); + } + + @Test + void agentScopeExcludesSubordinatesAndIncludesManagedAndParticipants() { + User user = user(7L, 9L, 10L); + when(authorityMapper.selectById(9L)).thenReturn(authority(9L, 1)); + WarehouseAgentAuth managed = new WarehouseAgentAuth(); + managed.setManagerAgentId(12L); + when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managed)); + when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyInstallOrderRowFilter(wrapper, user); + + String sql = wrapper.getSqlSegment(); + Collection values = wrapper.getParamNameValuePairs().values(); + assertTrue(sql.contains("apply_agent_id IN")); + assertTrue(sql.contains("agent_id IN")); + assertTrue(sql.contains("install_order_process")); + assertTrue(sql.contains("install_order_user")); + assertTrue(values.containsAll(List.of(7L, 10L, 12L))); + verifyNoInteractions(agentMapper); + } + + @Test + void salesNonLeaderUsesOnlyOwnAndManagedCompanyProperties() { + User user = user(7L, 9L, 10L); + user.setDeptId(6L); + user.setPostId(8L); + when(authorityMapper.selectById(9L)).thenReturn(authority(9L, 1)); + WarehouseAgentAuth managed = new WarehouseAgentAuth(); + managed.setManagerAgentId(12L); + when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managed)); + 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); + Properyt managedProperty = new Properyt(); + managedProperty.setId(22L); + when(properytMapper.selectList(any())).thenReturn(List.of(own), List.of(managedProperty)); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + service.applyInstallOrderRowFilter(wrapper, user); + + String sql = wrapper.getSqlSegment(); + Collection values = wrapper.getParamNameValuePairs().values(); + assertTrue(sql.contains("property_id IN")); + assertTrue(values.containsAll(List.of(21L, 22L))); + assertFalse(sql.contains("install_order_process")); + assertFalse(sql.contains("apply_agent_id")); + verifyNoInteractions(agentMapper, agentManageDistrictAuthMapper); + } + + 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(Long id, int isAgent) { + Authority authority = new Authority(); + authority.setId(id); + authority.setIsagent(isAgent); + authority.setSoftDelete(0); + return authority; + } +}