feat(mcp): 退场/移机/安装工单 changes 流(P2 流失预警与交付跟踪)
- withdraw-orders.changes.list:客户撤设备=流失前置信号,销售挽单依据 - relocation-orders.changes.list:客户经营变动信号(搬迁=收缩/扩张背景) - install-orders.changes.list:新签客户交付跟踪,mall_order_no 关联商城订单 - 状态位整型透传不造枚举;行权限复用各工单既有 list.query ACL; 表内无销售列,下游经 property_id→properties 流归属负责人 - 全量 463 项测试通过
This commit is contained in:
@@ -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<SyncRow> fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId,
|
||||
int limit, User user) {
|
||||
LambdaQueryWrapper<InstallOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyInstallOrderRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> 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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
+83
@@ -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<SyncRow> fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId,
|
||||
int limit, User user) {
|
||||
LambdaQueryWrapper<RelocationOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyRelocationOrderRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> 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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
+81
@@ -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<SyncRow> fetchPage(Long fromTs, long cutoffTs, Long cursorMt, Long cursorId,
|
||||
int limit, User user) {
|
||||
LambdaQueryWrapper<WithdrawOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyWithdrawOrderRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> 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<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<Long> parseContractIds(List<String> contractIds) {
|
||||
if (contractIds == null || contractIds.isEmpty()) {
|
||||
throw new ToolCallException(McpErrorCode.INVALID_ARGUMENT, "contractIds必须包含1至200个合同ID");
|
||||
|
||||
@@ -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<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, user);
|
||||
|
||||
verify(permissionService).applyWithdrawOrderRowFilter(any(), any());
|
||||
Map<String, Object> 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<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, user);
|
||||
|
||||
verify(permissionService).applyRelocationOrderRowFilter(any(), any());
|
||||
Map<String, Object> 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<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, user);
|
||||
|
||||
verify(permissionService).applyInstallOrderRowFilter(any(), any());
|
||||
Map<String, Object> 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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user