From 417c388eaca501b46f676be191209d017b626631 Mon Sep 17 00:00:00 2001 From: zhiyuncai Date: Fri, 16 Oct 2026 12:48:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20=E9=80=80=E5=9C=BA/=E7=A7=BB?= =?UTF-8?q?=E6=9C=BA/=E5=AE=89=E8=A3=85=E5=B7=A5=E5=8D=95=20changes=20?= =?UTF-8?q?=E6=B5=81=EF=BC=88P2=20=E6=B5=81=E5=A4=B1=E9=A2=84=E8=AD=A6?= =?UTF-8?q?=E4=B8=8E=E4=BA=A4=E4=BB=98=E8=B7=9F=E8=B8=AA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - withdraw-orders.changes.list:客户撤设备=流失前置信号,销售挽单依据 - relocation-orders.changes.list:客户经营变动信号(搬迁=收缩/扩张背景) - install-orders.changes.list:新签客户交付跟踪,mall_order_no 关联商城订单 - 状态位整型透传不造枚举;行权限复用各工单既有 list.query ACL; 表内无销售列,下游经 property_id→properties 流归属负责人 - 全量 463 项测试通过 --- .../mcp/sync/InstallOrdersSyncEntity.java | 82 +++++++++++ .../mcp/sync/RelocationOrdersSyncEntity.java | 83 +++++++++++ .../iiv/agent/mcp/sync/SyncChangeSchemas.java | 102 ++++++++++++++ .../mcp/sync/WithdrawOrdersSyncEntity.java | 81 +++++++++++ .../me/iiv/agent/mcp/tool/ChangesTools.java | 53 ++++++- .../iiv/agent/mcp/sync/SyncEntitiesTest.java | 131 ++++++++++++++++++ .../iiv/agent/mcp/tool/ChangesToolsTest.java | 16 ++- 7 files changed, 544 insertions(+), 4 deletions(-) create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/InstallOrdersSyncEntity.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/RelocationOrdersSyncEntity.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/WithdrawOrdersSyncEntity.java diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/InstallOrdersSyncEntity.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/InstallOrdersSyncEntity.java new file mode 100644 index 0000000..c1dc88a --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/InstallOrdersSyncEntity.java @@ -0,0 +1,82 @@ +package me.iiv.agent.mcp.sync; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.entity.m2.InstallOrder; +import me.iiv.agent.mapper.m2.InstallOrderMapper; +import me.iiv.agent.permission.PermissionService; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * install-orders 同步实体(m2.install_order 安装工单)。 + * 用途:P2 新签客户交付跟踪——已签约未安装超期影响客户满意度与首期回款, + * 销售需催交付;mall_order_no 关联商城订单事实。 + * order_state/install_type 取值域未逐一确认,整型透传不造枚举; + * hope_install_date/true_install_date 为 SaaS 既有口径,只读透传。 + * 行级权限:复用 PermissionService.applyInstallOrderRowFilter(与 install.order.list.query 同一 ACL)。 + * 下游负责人解析:表内无销售列,CRM 侧经 property_id → properties 流归属销售。 + */ +@Component +public class InstallOrdersSyncEntity extends AbstractSyncEntity implements SyncEntity { + + public static final String NAME = "install-orders"; + + final InstallOrderMapper installOrderMapper; + final PermissionService permissionService; + + public InstallOrdersSyncEntity(InstallOrderMapper installOrderMapper, + PermissionService permissionService) { + this.installOrderMapper = installOrderMapper; + this.permissionService = permissionService; + } + + @Override + public String name() { + return NAME; + } + + @Override + public List fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId, + int limit, User user) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (user != null) { + permissionService.applyInstallOrderRowFilter(wrapper, user); + } + applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId); + applyOrderAndLimit(wrapper, limit); + List rows = new ArrayList<>(); + for (InstallOrder row : installOrderMapper.selectList(wrapper)) { + long mt = ChangesEngine.effectiveTs(row.getModifyTime(), row.getCreateTime()); + rows.add(new SyncRow(toItem(row, mt), mt, row.getId())); + } + return rows; + } + + private Map toItem(InstallOrder row, long mt) { + boolean deleted = row.getSoftDelete() != null && row.getSoftDelete() == 1; + Date effective = row.getModifyTime() != null ? row.getModifyTime() : row.getCreateTime(); + return SyncChangeSchemas.toMap(new SyncChangeSchemas.InstallOrderChange( + stableId(row.getId()), row.getInstallOrderNo(), row.getInstallType(), row.getOrderState(), + stableId(row.getAgentId()), row.getAgentName(), + stableId(row.getPropertyId()), row.getPropertyName(), + stableId(row.getDistrictId()), row.getDistrictName(), + stableId(row.getApplyAgentId()), row.getApplyAgentName(), + stableId(row.getApplyUserId()), row.getApplyUserName(), + row.getHopeInstallDate(), row.getTrueInstallDate(), + row.getCustomerContact(), row.getContactMobile(), + row.getProjectAddress(), row.getProjectMemo(), + row.getMallOrderNo(), row.getDeliveryOrderNo(), row.getPickupOrderNo(), + ChangesEngine.iso(row.getApplyTime()), ChangesEngine.iso(row.getAuditTime()), + ChangesEngine.iso(row.getDispatchTime()), ChangesEngine.iso(row.getFinishDate()), + ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null)); + } + + private static String stableId(Long id) { + return id == null ? null : id.toString(); + } +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/RelocationOrdersSyncEntity.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/RelocationOrdersSyncEntity.java new file mode 100644 index 0000000..a4db1ff --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/RelocationOrdersSyncEntity.java @@ -0,0 +1,83 @@ +package me.iiv.agent.mcp.sync; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.entity.m2.RelocationOrder; +import me.iiv.agent.mapper.m2.RelocationOrderMapper; +import me.iiv.agent.permission.PermissionService; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * relocation-orders 同步实体(m2.relocation_order 移机工单)。 + * 用途:P2 客户经营变动信号——站点搬迁可能意味着业务收缩或扩张,续约谈判前必须掌握。 + * order_state 取值域未逐一确认,整型透传不造枚举; + * hope_relocation_date/true_relocation_date 为 SaaS 既有口径,只读透传。 + * 行级权限:复用 PermissionService.applyRelocationOrderRowFilter(与 relocation.order.list.query 同一 ACL)。 + * 下游负责人解析:表内无销售列,CRM 侧经 property_id → properties 流归属销售。 + */ +@Component +public class RelocationOrdersSyncEntity extends AbstractSyncEntity implements SyncEntity { + + public static final String NAME = "relocation-orders"; + + final RelocationOrderMapper relocationOrderMapper; + final PermissionService permissionService; + + public RelocationOrdersSyncEntity(RelocationOrderMapper relocationOrderMapper, + PermissionService permissionService) { + this.relocationOrderMapper = relocationOrderMapper; + this.permissionService = permissionService; + } + + @Override + public String name() { + return NAME; + } + + @Override + public List fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId, + int limit, User user) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (user != null) { + permissionService.applyRelocationOrderRowFilter(wrapper, user); + } + applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId); + applyOrderAndLimit(wrapper, limit); + List rows = new ArrayList<>(); + for (RelocationOrder row : relocationOrderMapper.selectList(wrapper)) { + long mt = ChangesEngine.effectiveTs(row.getModifyTime(), row.getCreateTime()); + rows.add(new SyncRow(toItem(row, mt), mt, row.getId())); + } + return rows; + } + + private Map toItem(RelocationOrder row, long mt) { + boolean deleted = row.getSoftDelete() != null && row.getSoftDelete() == 1; + Date effective = row.getModifyTime() != null ? row.getModifyTime() : row.getCreateTime(); + return SyncChangeSchemas.toMap(new SyncChangeSchemas.RelocationOrderChange( + stableId(row.getId()), row.getOrderNo(), row.getOrderState(), + stableId(row.getAgentId()), row.getAgentName(), + stableId(row.getPropertyId()), row.getPropertyName(), + stableId(row.getDistrictId()), row.getDistrictName(), + stableId(row.getApplyAgentId()), row.getApplyAgentName(), + stableId(row.getApplyUserId()), row.getApplyUserName(), + row.getRelocationReason(), + row.getHopeRelocationDate(), row.getTrueRelocationDate(), + row.getHopeRelocationCount(), row.getTrueRelocationCount(), + row.getCustomerContact(), row.getContactMobile(), + row.getProjectAddress(), row.getProjectMemo(), + ChangesEngine.iso(row.getApplyTime()), ChangesEngine.iso(row.getAuditTime()), + ChangesEngine.iso(row.getDispatchTime()), ChangesEngine.iso(row.getFinishTime()), + ChangesEngine.iso(row.getCancelTime()), + ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null)); + } + + private static String stableId(Long id) { + return id == null ? null : id.toString(); + } +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/SyncChangeSchemas.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/SyncChangeSchemas.java index ce7cb84..d121b90 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/SyncChangeSchemas.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/SyncChangeSchemas.java @@ -265,4 +265,106 @@ final class SyncChangeSchemas { long version, @JsonProperty("deleted_at") String deletedAt) { } + + /** 退场工单:客户撤设备=流失前置信号;状态位整型透传,日期为 SaaS 既有口径。 */ + record WithdrawOrderChange( + @JsonProperty("withdraw_order_id") String withdrawOrderId, + @JsonProperty("withdraw_order_no") String withdrawOrderNo, + @JsonProperty("pickup_order_no") String pickupOrderNo, + @JsonProperty("withdraw_type") Integer withdrawType, + @JsonProperty("order_state") Integer orderState, + @JsonProperty("agent_id") String agentId, + @JsonProperty("agent_name") String agentName, + @JsonProperty("property_id") String propertyId, + @JsonProperty("property_name") String propertyName, + @JsonProperty("district_id") String districtId, + @JsonProperty("district_name") String districtName, + @JsonProperty("apply_agent_id") String applyAgentId, + @JsonProperty("apply_agent_name") String applyAgentName, + @JsonProperty("apply_user_id") String applyUserId, + @JsonProperty("apply_user_name") String applyUserName, + @JsonProperty("planned_withdraw_date") String plannedWithdrawDate, + @JsonProperty("actual_withdraw_date") String actualWithdrawDate, + @JsonProperty("withdraw_goods_count") Integer withdrawGoodsCount, + @JsonProperty("customer_contact") String customerContact, + @JsonProperty("contact_mobile") String contactMobile, + @JsonProperty("project_address") String projectAddress, + @JsonProperty("project_memo") String projectMemo, + String memo, + @JsonProperty("applied_at") String appliedAt, + @JsonProperty("audited_at") String auditedAt, + @JsonProperty("dispatched_at") String dispatchedAt, + @JsonProperty("finished_at") String finishedAt, + @JsonProperty("updated_at") String updatedAt, + long version, + @JsonProperty("deleted_at") String deletedAt) { + } + + /** 移机工单:客户经营变动信号;状态位整型透传,日期为 SaaS 既有口径。 */ + record RelocationOrderChange( + @JsonProperty("relocation_order_id") String relocationOrderId, + @JsonProperty("order_no") String orderNo, + @JsonProperty("order_state") Integer orderState, + @JsonProperty("agent_id") String agentId, + @JsonProperty("agent_name") String agentName, + @JsonProperty("property_id") String propertyId, + @JsonProperty("property_name") String propertyName, + @JsonProperty("district_id") String districtId, + @JsonProperty("district_name") String districtName, + @JsonProperty("apply_agent_id") String applyAgentId, + @JsonProperty("apply_agent_name") String applyAgentName, + @JsonProperty("apply_user_id") String applyUserId, + @JsonProperty("apply_user_name") String applyUserName, + @JsonProperty("relocation_reason") String relocationReason, + @JsonProperty("planned_relocation_date") String plannedRelocationDate, + @JsonProperty("actual_relocation_date") String actualRelocationDate, + @JsonProperty("planned_relocation_count") Integer plannedRelocationCount, + @JsonProperty("actual_relocation_count") Integer actualRelocationCount, + @JsonProperty("customer_contact") String customerContact, + @JsonProperty("contact_mobile") String contactMobile, + @JsonProperty("project_address") String projectAddress, + @JsonProperty("project_memo") String projectMemo, + @JsonProperty("applied_at") String appliedAt, + @JsonProperty("audited_at") String auditedAt, + @JsonProperty("dispatched_at") String dispatchedAt, + @JsonProperty("finished_at") String finishedAt, + @JsonProperty("cancelled_at") String cancelledAt, + @JsonProperty("updated_at") String updatedAt, + long version, + @JsonProperty("deleted_at") String deletedAt) { + } + + /** 安装工单:新签客户交付跟踪;状态位整型透传,日期为 SaaS 既有口径。 */ + record InstallOrderChange( + @JsonProperty("install_order_id") String installOrderId, + @JsonProperty("install_order_no") String installOrderNo, + @JsonProperty("install_type") Integer installType, + @JsonProperty("order_state") Integer orderState, + @JsonProperty("agent_id") String agentId, + @JsonProperty("agent_name") String agentName, + @JsonProperty("property_id") String propertyId, + @JsonProperty("property_name") String propertyName, + @JsonProperty("district_id") String districtId, + @JsonProperty("district_name") String districtName, + @JsonProperty("apply_agent_id") String applyAgentId, + @JsonProperty("apply_agent_name") String applyAgentName, + @JsonProperty("apply_user_id") String applyUserId, + @JsonProperty("apply_user_name") String applyUserName, + @JsonProperty("planned_install_date") String plannedInstallDate, + @JsonProperty("actual_install_date") String actualInstallDate, + @JsonProperty("customer_contact") String customerContact, + @JsonProperty("contact_mobile") String contactMobile, + @JsonProperty("project_address") String projectAddress, + @JsonProperty("project_memo") String projectMemo, + @JsonProperty("mall_order_no") String mallOrderNo, + @JsonProperty("delivery_order_no") String deliveryOrderNo, + @JsonProperty("pickup_order_no") String pickupOrderNo, + @JsonProperty("applied_at") String appliedAt, + @JsonProperty("audited_at") String auditedAt, + @JsonProperty("dispatched_at") String dispatchedAt, + @JsonProperty("finished_at") String finishedAt, + @JsonProperty("updated_at") String updatedAt, + long version, + @JsonProperty("deleted_at") String deletedAt) { + } } diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/WithdrawOrdersSyncEntity.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/WithdrawOrdersSyncEntity.java new file mode 100644 index 0000000..a3e0f5f --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/WithdrawOrdersSyncEntity.java @@ -0,0 +1,81 @@ +package me.iiv.agent.mcp.sync; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import me.iiv.agent.entity.m0.User; +import me.iiv.agent.entity.m2.WithdrawOrder; +import me.iiv.agent.mapper.m2.WithdrawOrderMapper; +import me.iiv.agent.permission.PermissionService; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * withdraw-orders 同步实体(m2.withdraw_order 退场工单)。 + * 用途:P2 流失预警——客户撤设备是流失前置信号,销售需第一时间介入挽单。 + * order_state/type 取值域未逐一确认,整型透传不造枚举; + * pre_withdraw_date/true_withdraw_date 为 SaaS 既有口径,只读透传。 + * 行级权限:复用 PermissionService.applyWithdrawOrderRowFilter(与 withdraw.order.list.query 同一 ACL)。 + * 下游负责人解析:表内无销售列,CRM 侧经 property_id → properties 流归属销售。 + */ +@Component +public class WithdrawOrdersSyncEntity extends AbstractSyncEntity implements SyncEntity { + + public static final String NAME = "withdraw-orders"; + + final WithdrawOrderMapper withdrawOrderMapper; + final PermissionService permissionService; + + public WithdrawOrdersSyncEntity(WithdrawOrderMapper withdrawOrderMapper, + PermissionService permissionService) { + this.withdrawOrderMapper = withdrawOrderMapper; + this.permissionService = permissionService; + } + + @Override + public String name() { + return NAME; + } + + @Override + public List fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId, + int limit, User user) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (user != null) { + permissionService.applyWithdrawOrderRowFilter(wrapper, user); + } + applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId); + applyOrderAndLimit(wrapper, limit); + List rows = new ArrayList<>(); + for (WithdrawOrder row : withdrawOrderMapper.selectList(wrapper)) { + long mt = ChangesEngine.effectiveTs(row.getModifyTime(), row.getCreateTime()); + rows.add(new SyncRow(toItem(row, mt), mt, row.getId())); + } + return rows; + } + + private Map toItem(WithdrawOrder row, long mt) { + boolean deleted = row.getSoftDelete() != null && row.getSoftDelete() == 1; + Date effective = row.getModifyTime() != null ? row.getModifyTime() : row.getCreateTime(); + return SyncChangeSchemas.toMap(new SyncChangeSchemas.WithdrawOrderChange( + stableId(row.getId()), row.getWithdrawOrderNo(), row.getPickupOrderNo(), row.getType(), + row.getOrderState(), + stableId(row.getAgentId()), row.getAgentName(), + stableId(row.getPropertyId()), row.getPropertyName(), + stableId(row.getDistrictId()), row.getDistrictName(), + stableId(row.getApplyAgentId()), row.getApplyAgentName(), + stableId(row.getApplyUserId()), row.getApplyUserName(), + row.getPreWithdrawDate(), row.getTrueWithdrawDate(), + row.getWithdrawGoodsCount(), row.getCustomerContact(), row.getContactMobile(), + row.getProjectAddress(), row.getProjectMemo(), row.getMemo(), + ChangesEngine.iso(row.getApplyTime()), ChangesEngine.iso(row.getAuditTime()), + ChangesEngine.iso(row.getDispatchTime()), ChangesEngine.iso(row.getFinishTime()), + ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null)); + } + + private static String stableId(Long id) { + return id == null ? null : id.toString(); + } +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/ChangesTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/ChangesTools.java index 683e09a..cd939a0 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/ChangesTools.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/ChangesTools.java @@ -9,11 +9,14 @@ import me.iiv.agent.mcp.sync.ChangesEngine; import me.iiv.agent.mcp.sync.ContractsSyncEntity; import me.iiv.agent.mcp.sync.DeviceLifecyclesSyncEntity; import me.iiv.agent.mcp.sync.DevicesSyncEntity; +import me.iiv.agent.mcp.sync.InstallOrdersSyncEntity; import me.iiv.agent.mcp.sync.MallOrdersSyncEntity; import me.iiv.agent.mcp.sync.PropertiesSyncEntity; +import me.iiv.agent.mcp.sync.RelocationOrdersSyncEntity; import me.iiv.agent.mcp.sync.RepaymentDetailsSyncEntity; import me.iiv.agent.mcp.sync.RepaymentTargetsSyncEntity; import me.iiv.agent.mcp.sync.UsersSyncEntity; +import me.iiv.agent.mcp.sync.WithdrawOrdersSyncEntity; import me.iiv.agent.security.AuthContext; import me.iiv.agent.security.AuthContextHolder; import org.springframework.ai.tool.annotation.Tool; @@ -31,7 +34,8 @@ import java.util.Map; /** * 增量同步工具(REQ-20260911-0013;P2 域扩展于 REQ-20260902-0002): * users/properties/contracts/repayment-targets/repayment-details/mall-orders/ - * device-lifecycles/battery-faults/devices 九个 changes 工具, + * device-lifecycles/battery-faults/devices/withdraw-orders/relocation-orders/ + * install-orders 十二个 changes 工具, * 协议(snapshot/cursor/tombstone/sync_token)由 ChangesEngine 统一实现。 * 数据域硬约束:本类不声明 tenant_id/scope 入参,数据域只能从 Token 推导; * GuardedToolCallback 对 *.changes.list 工具的原始入参做二次拦截,传 tenant_id/scope 一律拒绝。 @@ -213,6 +217,53 @@ public class ChangesTools implements ToolObject { return changesEngine.execute(DevicesSyncEntity.NAME, syncToken, cursor, limit); } + @XqTool(category = "sync", kind = "read") + @Tool(name = "withdraw-orders.changes.list", description = "增量同步退场工单(m2.withdraw_order)——客户撤设备=流失前置信号。" + PROTOCOL_DESC + + "items 字段:withdraw_order_id/withdraw_order_no/pickup_order_no/withdraw_type/" + + "order_state(整型透传)/agent/property/district 及名称/apply_agent_id/apply_user_id 及申请人名称/" + + "planned_withdraw_date/actual_withdraw_date(SaaS 既有口径只读透传)/withdraw_goods_count/" + + "customer_contact/contact_mobile/project_address/project_memo/memo/" + + "applied_at/audited_at/dispatched_at/finished_at/updated_at/version/deleted_at。" + + "行级权限跟随 Token 绑定的人(与 withdraw.order.list.query 同一 ACL)。") + public Map withdrawOrdersChangesList( + @ToolParam(required = false, description = SYNC_TOKEN_DESC) String syncToken, + @ToolParam(required = false, description = CURSOR_DESC) String cursor, + @ToolParam(required = false, description = LIMIT_DESC) Integer limit) { + return changesEngine.execute(WithdrawOrdersSyncEntity.NAME, syncToken, cursor, limit); + } + + @XqTool(category = "sync", kind = "read") + @Tool(name = "relocation-orders.changes.list", description = "增量同步移机工单(m2.relocation_order)——客户经营变动信号。" + PROTOCOL_DESC + + "items 字段:relocation_order_id/order_no/order_state(整型透传)/" + + "agent/property/district 及名称/apply_agent_id/apply_user_id 及申请人名称/" + + "relocation_reason/planned_relocation_date/actual_relocation_date(SaaS 既有口径只读透传)/" + + "planned_relocation_count/actual_relocation_count/customer_contact/contact_mobile/" + + "project_address/project_memo/applied_at/audited_at/dispatched_at/finished_at/cancelled_at/" + + "updated_at/version/deleted_at。" + + "行级权限跟随 Token 绑定的人(与 relocation.order.list.query 同一 ACL)。") + public Map relocationOrdersChangesList( + @ToolParam(required = false, description = SYNC_TOKEN_DESC) String syncToken, + @ToolParam(required = false, description = CURSOR_DESC) String cursor, + @ToolParam(required = false, description = LIMIT_DESC) Integer limit) { + return changesEngine.execute(RelocationOrdersSyncEntity.NAME, syncToken, cursor, limit); + } + + @XqTool(category = "sync", kind = "read") + @Tool(name = "install-orders.changes.list", description = "增量同步安装工单(m2.install_order)——新签客户交付跟踪。" + PROTOCOL_DESC + + "items 字段:install_order_id/install_order_no/install_type/order_state(整型透传)/" + + "agent/property/district 及名称/apply_agent_id/apply_user_id 及申请人名称/" + + "planned_install_date/actual_install_date(SaaS 既有口径只读透传)/" + + "customer_contact/contact_mobile/project_address/project_memo/" + + "mall_order_no/delivery_order_no/pickup_order_no(关联单号)/" + + "applied_at/audited_at/dispatched_at/finished_at/updated_at/version/deleted_at。" + + "行级权限跟随 Token 绑定的人(与 install.order.list.query 同一 ACL)。") + public Map installOrdersChangesList( + @ToolParam(required = false, description = SYNC_TOKEN_DESC) String syncToken, + @ToolParam(required = false, description = CURSOR_DESC) String cursor, + @ToolParam(required = false, description = LIMIT_DESC) Integer limit) { + return changesEngine.execute(InstallOrdersSyncEntity.NAME, syncToken, cursor, limit); + } + private static List parseContractIds(List contractIds) { if (contractIds == null || contractIds.isEmpty()) { throw new ToolCallException(McpErrorCode.INVALID_ARGUMENT, "contractIds必须包含1至200个合同ID"); diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/sync/SyncEntitiesTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/sync/SyncEntitiesTest.java index 31a596b..f00d921 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/sync/SyncEntitiesTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/sync/SyncEntitiesTest.java @@ -12,6 +12,9 @@ import me.iiv.agent.entity.m1.MallGoodsOrder; import me.iiv.agent.entity.m1.SalerRepayment; import me.iiv.agent.entity.m1.SalerRepaymentCustomer; import me.iiv.agent.entity.m2.ContractNew; +import me.iiv.agent.entity.m2.InstallOrder; +import me.iiv.agent.entity.m2.RelocationOrder; +import me.iiv.agent.entity.m2.WithdrawOrder; import me.iiv.agent.mapper.m0.BatteryFaultLogMapper; import me.iiv.agent.mapper.m0.DeviceLifeCycleMapper; import me.iiv.agent.mapper.m0.HardwareViewMapper; @@ -21,6 +24,9 @@ import me.iiv.agent.mapper.m1.MallGoodsOrderMapper; import me.iiv.agent.mapper.m1.SalerRepaymentCustomerMapper; import me.iiv.agent.mapper.m1.SalerRepaymentMapper; import me.iiv.agent.mapper.m2.ContractNewMapper; +import me.iiv.agent.mapper.m2.InstallOrderMapper; +import me.iiv.agent.mapper.m2.RelocationOrderMapper; +import me.iiv.agent.mapper.m2.WithdrawOrderMapper; import me.iiv.agent.mcp.McpErrorCode; import me.iiv.agent.mcp.ToolCallException; import me.iiv.agent.permission.PermissionService; @@ -75,6 +81,12 @@ class SyncEntitiesTest { @Mock HardwareViewMapper hardwareViewMapper; @Mock + WithdrawOrderMapper withdrawOrderMapper; + @Mock + RelocationOrderMapper relocationOrderMapper; + @Mock + InstallOrderMapper installOrderMapper; + @Mock PermissionService permissionService; @BeforeEach @@ -88,6 +100,9 @@ class SyncEntitiesTest { TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), DeviceLifeCycle.class); TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), BatteryFaultLog.class); TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), HardwareView.class); + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), WithdrawOrder.class); + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), RelocationOrder.class); + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), InstallOrder.class); } @Test @@ -639,4 +654,120 @@ class SyncEntitiesTest { assertEquals(McpErrorCode.DATA_INTEGRITY_ERROR, error.getCode()); } + + @Test + void withdrawOrdersMapsFieldsAndAppliesFilter() { + WithdrawOrdersSyncEntity entity = new WithdrawOrdersSyncEntity(withdrawOrderMapper, permissionService); + WithdrawOrder order = new WithdrawOrder(); + order.setId(91L); + order.setWithdrawOrderNo("WD20261001"); + order.setType(1); + order.setOrderState(2); + order.setAgentId(21L); + order.setAgentName("趣致北京"); + order.setPropertyId(9L); + order.setPropertyName("望京SOHO"); + order.setDistrictId(3L); + order.setApplyUserId(1001L); + order.setApplyUserName("张运营"); + order.setPreWithdrawDate("2026-10-20"); + order.setTrueWithdrawDate("2026-10-22"); + order.setWithdrawGoodsCount(4); + order.setCustomerContact("李经理"); + order.setApplyTime(Timestamp.valueOf("2026-10-10 09:00:00")); + order.setFinishTime(Timestamp.valueOf("2026-10-22 18:00:00")); + order.setSoftDelete(0); + order.setModifyTime(Timestamp.valueOf("2026-10-22 18:30:00")); + when(withdrawOrderMapper.selectList(any())).thenReturn(List.of(order)); + User user = new User(); + user.setId(7L); + + List rows = entity.fetchPage(null, 2000L, null, null, 3, user); + + verify(permissionService).applyWithdrawOrderRowFilter(any(), any()); + Map item = rows.get(0).item(); + assertEquals("91", item.get("withdraw_order_id")); + assertEquals("WD20261001", item.get("withdraw_order_no")); + assertEquals(2, item.get("order_state"), "工单状态整型透传"); + assertEquals("望京SOHO", item.get("property_name")); + assertEquals("2026-10-20", item.get("planned_withdraw_date")); + assertEquals("2026-10-22", item.get("actual_withdraw_date")); + assertEquals(4, item.get("withdraw_goods_count")); + assertEquals("2026-10-10T09:00:00+08:00", item.get("applied_at")); + assertEquals("2026-10-22T18:00:00+08:00", item.get("finished_at")); + assertNull(item.get("deleted_at")); + } + + @Test + void relocationOrdersMapsFieldsAndAppliesFilterWithTombstone() { + RelocationOrdersSyncEntity entity = new RelocationOrdersSyncEntity(relocationOrderMapper, permissionService); + RelocationOrder order = new RelocationOrder(); + order.setId(92L); + order.setOrderNo("RL20261001"); + order.setOrderState(1); + order.setAgentId(21L); + order.setPropertyId(9L); + order.setPropertyName("望京SOHO"); + order.setRelocationReason("楼层改造"); + order.setHopeRelocationDate("2026-10-25"); + order.setHopeRelocationCount(2); + order.setApplyTime(Timestamp.valueOf("2026-10-10 09:00:00")); + order.setCancelTime(Timestamp.valueOf("2026-10-12 10:00:00")); + order.setSoftDelete(1); + order.setModifyTime(Timestamp.valueOf("2026-10-12 10:30:00")); + when(relocationOrderMapper.selectList(any())).thenReturn(List.of(order)); + User user = new User(); + user.setId(7L); + + List rows = entity.fetchPage(null, 2000L, null, null, 3, user); + + verify(permissionService).applyRelocationOrderRowFilter(any(), any()); + Map item = rows.get(0).item(); + assertEquals("92", item.get("relocation_order_id")); + assertEquals("RL20261001", item.get("order_no")); + assertEquals("楼层改造", item.get("relocation_reason")); + assertEquals("2026-10-25", item.get("planned_relocation_date")); + assertEquals(2, item.get("planned_relocation_count")); + assertEquals("2026-10-12T10:00:00+08:00", item.get("cancelled_at")); + assertEquals("2026-10-12T10:30:00+08:00", item.get("deleted_at"), "软删行返回 tombstone"); + } + + @Test + void installOrdersMapsFieldsAndAppliesFilter() { + InstallOrdersSyncEntity entity = new InstallOrdersSyncEntity(installOrderMapper, permissionService); + InstallOrder order = new InstallOrder(); + order.setId(93L); + order.setInstallOrderNo("IN20261001"); + order.setInstallType(0); + order.setOrderState(3); + order.setAgentId(21L); + order.setPropertyId(9L); + order.setPropertyName("望京SOHO"); + order.setDistrictId(3L); + order.setApplyUserId(1001L); + order.setApplyUserName("张运营"); + order.setHopeInstallDate("2026-10-18"); + order.setTrueInstallDate("2026-10-19"); + order.setCustomerContact("李经理"); + order.setMallOrderNo("SO20261001"); + order.setApplyTime(Timestamp.valueOf("2026-10-10 09:00:00")); + order.setFinishDate(Timestamp.valueOf("2026-10-19 17:00:00")); + order.setSoftDelete(0); + order.setModifyTime(Timestamp.valueOf("2026-10-19 17:30:00")); + when(installOrderMapper.selectList(any())).thenReturn(List.of(order)); + User user = new User(); + user.setId(7L); + + List rows = entity.fetchPage(null, 2000L, null, null, 3, user); + + verify(permissionService).applyInstallOrderRowFilter(any(), any()); + Map item = rows.get(0).item(); + assertEquals("93", item.get("install_order_id")); + assertEquals("IN20261001", item.get("install_order_no")); + assertEquals("2026-10-18", item.get("planned_install_date")); + assertEquals("2026-10-19", item.get("actual_install_date")); + assertEquals("SO20261001", item.get("mall_order_no"), "关联商城订单号必须透传"); + assertEquals("2026-10-19T17:00:00+08:00", item.get("finished_at")); + assertNull(item.get("deleted_at")); + } } diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/ChangesToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/ChangesToolsTest.java index 9e307d2..c695343 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/ChangesToolsTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/ChangesToolsTest.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** - * 九个 changes 工具的注册与路由(REQ-20260911-0013,P2 域扩展于 REQ-20260902-0002)。 + * 十二个 changes 工具的注册与路由(REQ-20260911-0013,P2 域扩展于 REQ-20260902-0002)。 */ @ExtendWith(MockitoExtension.class) class ChangesToolsTest { @@ -41,11 +41,12 @@ class ChangesToolsTest { callback -> callback.getToolDefinition().name(), callback -> callback.getToolDefinition().inputSchema())); - assertEquals(10, schemas.size()); + assertEquals(13, schemas.size()); for (String name : new String[]{"users.changes.list", "properties.changes.list", "contracts.changes.list", "repayment-targets.changes.list", "repayment-details.changes.list", "mall-orders.changes.list", "device-lifecycles.changes.list", "battery-faults.changes.list", - "devices.changes.list"}) { + "devices.changes.list", "withdraw-orders.changes.list", "relocation-orders.changes.list", + "install-orders.changes.list"}) { assertTrue(schemas.containsKey(name), "缺少工具 " + name); String schema = schemas.get(name); assertTrue(schema.contains("syncToken")); @@ -89,6 +90,15 @@ class ChangesToolsTest { tools.devicesChangesList("t6", null, 20); verify(changesEngine).execute("devices", "t6", null, 20); + + tools.withdrawOrdersChangesList(null, null, null); + verify(changesEngine).execute("withdraw-orders", null, null, null); + + tools.relocationOrdersChangesList("t7", "c7", null); + verify(changesEngine).execute("relocation-orders", "t7", "c7", null); + + tools.installOrdersChangesList(null, null, 80); + verify(changesEngine).execute("install-orders", null, null, 80); } @Test