feat(mcp): 商城订单/设备生命周期/电池故障 changes 流(P2 增购复购与设备域准入)
- mall-orders.changes.list:m1.mall_goods_order,order_state 0-7 映射公开枚举 fail-closed, 金额 BigDecimal 元转分,逾期/回款周期/开票状态等 SaaS 既有口径只读透传; 行权限复用 applyOrderRowFilter(与 mall.order.list.query 同一 ACL) - device-lifecycles.changes.list:m0.device_life_cycle,network_state 0/1→OFFLINE/ONLINE, 其余流转状态整型透传;行权限按代理维度 agent_id(表内无 property 维度) - battery-faults.changes.list:m0.battery_fault_log,故障类型/状态整型透传, 行权限复用 applyBatteryFaultLogRowFilter - 契约层统一 NON_NULL 省略空值键;测试:SyncEntitiesTest +5、ChangesToolsTest 注册数 6→9, 全量 458 项测试通过
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package me.iiv.agent.mcp.sync;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.m0.BatteryFaultLog;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.mapper.m0.BatteryFaultLogMapper;
|
||||
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;
|
||||
|
||||
/**
|
||||
* battery-faults 同步实体(m0.battery_fault_log 电池/设备故障日志)。
|
||||
* 用途:P2 设备经营域故障事项化底座(故障发生/恢复事件流)。
|
||||
* fault_type/fault_state/device_type 取值域未逐一确认,整型透传不造枚举;
|
||||
* fault_start_time/fault_recovery_time 为 SaaS 既有口径,只读透传。
|
||||
* 行级权限:复用 PermissionService.applyBatteryFaultLogRowFilter
|
||||
* (与 battery.fault.list.query 同一 ACL:代理/物业可见范围)。
|
||||
*/
|
||||
@Component
|
||||
public class BatteryFaultsSyncEntity extends AbstractSyncEntity implements SyncEntity {
|
||||
|
||||
public static final String NAME = "battery-faults";
|
||||
|
||||
final BatteryFaultLogMapper batteryFaultLogMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public BatteryFaultsSyncEntity(BatteryFaultLogMapper batteryFaultLogMapper,
|
||||
PermissionService permissionService) {
|
||||
this.batteryFaultLogMapper = batteryFaultLogMapper;
|
||||
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<BatteryFaultLog> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyBatteryFaultLogRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> rows = new ArrayList<>();
|
||||
for (BatteryFaultLog row : batteryFaultLogMapper.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(BatteryFaultLog 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.BatteryFaultChange(
|
||||
stableId(row.getId()), row.getSnCode(), row.getDeviceType(),
|
||||
row.getFaultType(), row.getFaultState(),
|
||||
ChangesEngine.iso(row.getFaultStartTime()), ChangesEngine.iso(row.getFaultAddTime()),
|
||||
ChangesEngine.iso(row.getFaultRecoveryTime()),
|
||||
row.getInstallPosition(),
|
||||
stableId(row.getDistrictId()), row.getDistrictName(),
|
||||
stableId(row.getPropertyId()), row.getPropertyName(),
|
||||
stableId(row.getAgentId()), row.getAgentName(),
|
||||
row.getPeakValue(), row.getStandardValue(),
|
||||
ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null));
|
||||
}
|
||||
|
||||
private static String stableId(Long id) {
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package me.iiv.agent.mcp.sync;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.m0.DeviceLifeCycle;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.mapper.m0.DeviceLifeCycleMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* device-lifecycles 同步实体(m0.device_life_cycle 设备生命周期)。
|
||||
* 用途:P2 设备经营域事项化底座(离线/上线/退场/资产属性变化)。
|
||||
* network_state 0/1 映射 OFFLINE/ONLINE,未知编码 fail-closed;
|
||||
* exchange_state/road_state/inventory_state/asset_state/device_type 取值域较宽且
|
||||
* SaaS 可能扩展,整型透传不造枚举(与 order_invoice_state 同一先例)。
|
||||
* 行级权限:PermissionService.applyDeviceLifeCycleRowFilter(代理维度 agent_id,
|
||||
* 表内无 property 维度)。
|
||||
*/
|
||||
@Component
|
||||
public class DeviceLifecyclesSyncEntity extends AbstractSyncEntity implements SyncEntity {
|
||||
|
||||
public static final String NAME = "device-lifecycles";
|
||||
|
||||
final DeviceLifeCycleMapper deviceLifeCycleMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public DeviceLifecyclesSyncEntity(DeviceLifeCycleMapper deviceLifeCycleMapper,
|
||||
PermissionService permissionService) {
|
||||
this.deviceLifeCycleMapper = deviceLifeCycleMapper;
|
||||
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<DeviceLifeCycle> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyDeviceLifeCycleRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> rows = new ArrayList<>();
|
||||
for (DeviceLifeCycle row : deviceLifeCycleMapper.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(DeviceLifeCycle 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.DeviceLifecycleChange(
|
||||
stableId(row.getId()), row.getSnCode(), row.getDeviceType(),
|
||||
row.getExchangeState(), row.getUpExchangeState(),
|
||||
row.getRoadState(), row.getInventoryState(),
|
||||
publicNetworkState(row.getNetworkState()), row.getAssetState(),
|
||||
stableId(row.getAgentId()), row.getAgentName(),
|
||||
stableId(row.getAssetOwnerId()), row.getAssetOwnerName(),
|
||||
ChangesEngine.iso(row.getFirstUseDate()), ChangesEngine.iso(row.getLastOnlineDate()),
|
||||
ChangesEngine.iso(row.getDiscardDate()), row.getLastOpUser(),
|
||||
ChangesEngine.iso(row.getLastOpDate()),
|
||||
ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null));
|
||||
}
|
||||
|
||||
/** Legacy network_state (0/1) to the stable public enum. */
|
||||
static String publicNetworkState(Integer state) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (state) {
|
||||
case 0 -> "OFFLINE";
|
||||
case 1 -> "ONLINE";
|
||||
default -> throw new ToolCallException(McpErrorCode.DATA_INTEGRITY_ERROR,
|
||||
"设备联网状态无法映射到公开契约: " + state);
|
||||
};
|
||||
}
|
||||
|
||||
private static String stableId(Long id) {
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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.m1.MallGoodsOrder;
|
||||
import me.iiv.agent.mapper.m1.MallGoodsOrderMapper;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.permission.PermissionService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* mall-orders 同步实体(m1.mall_goods_order 商城订单)。
|
||||
* 用途:P2 增购/复购域订单事实 + 订单级回款/发票事实(overdue_flag/overdue_days/
|
||||
* last_pay_time/order_invoice_state/invoice_money 均为 SaaS 既有口径,只读透传,本侧不重算)。
|
||||
* 金额列为 BigDecimal(元),契约层统一转分(HALF_UP);version 由 modify_time 派生。
|
||||
* order_state 0-7 映射公开枚举,未知编码 fail-closed 拒绝(与 contracts 同一铁律);
|
||||
* 其余状态位(pay_state/order_invoice_state/accounts_type 等)取值域未逐一确认,整型透传不造枚举。
|
||||
* 行级权限:复用 PermissionService.applyOrderRowFilter(与 mall.order.list.query 同一 ACL,
|
||||
* 移植自老系统 WrapperUtils.getSaleCompany)。
|
||||
*/
|
||||
@Component
|
||||
public class MallOrdersSyncEntity extends AbstractSyncEntity implements SyncEntity {
|
||||
|
||||
public static final String NAME = "mall-orders";
|
||||
|
||||
final MallGoodsOrderMapper mallGoodsOrderMapper;
|
||||
final PermissionService permissionService;
|
||||
|
||||
public MallOrdersSyncEntity(MallGoodsOrderMapper mallGoodsOrderMapper,
|
||||
PermissionService permissionService) {
|
||||
this.mallGoodsOrderMapper = mallGoodsOrderMapper;
|
||||
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<MallGoodsOrder> wrapper = new LambdaQueryWrapper<>();
|
||||
if (user != null) {
|
||||
permissionService.applyOrderRowFilter(wrapper, user);
|
||||
}
|
||||
applyWindow(wrapper, fromTs, cutoffTs, cursorMt, cursorId);
|
||||
applyOrderAndLimit(wrapper, limit);
|
||||
List<SyncRow> rows = new ArrayList<>();
|
||||
for (MallGoodsOrder row : mallGoodsOrderMapper.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(MallGoodsOrder 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.MallOrderChange(
|
||||
stableId(row.getId()), row.getOrderNo(), row.getBillNo(),
|
||||
publicOrderState(row.getOrderState()),
|
||||
row.getOrderType(), row.getAccountsType(), row.getPayState(),
|
||||
row.getOrderPayState(), row.getOrderInvoiceState(),
|
||||
stableId(row.getCreateId()), row.getCreateName(),
|
||||
stableId(row.getAgentId()), row.getAgentName(),
|
||||
stableId(row.getPropertyId()), row.getPropertyName(),
|
||||
stableId(row.getDistrictId()), row.getDistrictName(),
|
||||
stableId(row.getUserId()), row.getUserName(),
|
||||
stableId(row.getSaleCompanyId()), row.getSaleCompany(),
|
||||
toMinor(row.getTotalMoney()), toMinor(row.getGoodsMoney()),
|
||||
toMinor(row.getPayMoney()), toMinor(row.getPaidMoney()), toMinor(row.getUnPayMoney()),
|
||||
toMinor(row.getDiscountMoney()), toMinor(row.getInvoiceMoney()), toMinor(row.getUnInvoiceMoney()),
|
||||
row.getOverdueFlag(), row.getOverdueDays(), row.getPaymentCollection(),
|
||||
ChangesEngine.iso(row.getOrderTime()), ChangesEngine.iso(row.getPayTime()),
|
||||
ChangesEngine.iso(row.getLastPayTime()), ChangesEngine.iso(row.getSignTime()),
|
||||
ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null));
|
||||
}
|
||||
|
||||
/** BigDecimal(元)→ 分;null 原样返回 null。 */
|
||||
static Long toMinor(BigDecimal amount) {
|
||||
return amount == null ? null
|
||||
: amount.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValue();
|
||||
}
|
||||
|
||||
/** Legacy order_state (0-7) to the stable public enum. */
|
||||
static String publicOrderState(Integer state) {
|
||||
if (state == null) {
|
||||
throw invalidOrderState(null);
|
||||
}
|
||||
return switch (state) {
|
||||
case 0 -> "PENDING_PAYMENT";
|
||||
case 1 -> "PENDING_OUTBOUND";
|
||||
case 2 -> "PENDING_SHIPMENT";
|
||||
case 3 -> "SHIPPED";
|
||||
case 4 -> "SIGNED";
|
||||
case 5 -> "RECEIVED";
|
||||
case 6 -> "CANCELLED";
|
||||
case 7 -> "DELETED";
|
||||
default -> throw invalidOrderState(state);
|
||||
};
|
||||
}
|
||||
|
||||
private static ToolCallException invalidOrderState(Integer state) {
|
||||
return new ToolCallException(McpErrorCode.DATA_INTEGRITY_ERROR,
|
||||
"商城订单状态无法映射到公开契约: " + state);
|
||||
}
|
||||
|
||||
private static String stableId(Long id) {
|
||||
return id == null ? null : id.toString();
|
||||
}
|
||||
}
|
||||
@@ -137,4 +137,96 @@ final class SyncChangeSchemas {
|
||||
long version,
|
||||
@JsonProperty("deleted_at") String deletedAt) {
|
||||
}
|
||||
|
||||
/** 商城订单:金额均为分;逾期/回款周期/发票状态为 SaaS 既有口径,只读透传。 */
|
||||
record MallOrderChange(
|
||||
@JsonProperty("order_id") String orderId,
|
||||
@JsonProperty("order_no") String orderNo,
|
||||
@JsonProperty("bill_no") String billNo,
|
||||
@JsonProperty("order_state") String orderState,
|
||||
@JsonProperty("order_type") Integer orderType,
|
||||
@JsonProperty("accounts_type") Integer accountsType,
|
||||
@JsonProperty("pay_state") Integer payState,
|
||||
@JsonProperty("order_pay_state") Integer orderPayState,
|
||||
@JsonProperty("order_invoice_state") Integer orderInvoiceState,
|
||||
@JsonProperty("owner_user_id") String ownerUserId,
|
||||
@JsonProperty("owner_name") String ownerName,
|
||||
@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("customer_user_id") String customerUserId,
|
||||
@JsonProperty("customer_name") String customerName,
|
||||
@JsonProperty("sale_company_id") String saleCompanyId,
|
||||
@JsonProperty("sale_company_name") String saleCompanyName,
|
||||
@JsonProperty("total_amount_minor") Long totalAmountMinor,
|
||||
@JsonProperty("goods_amount_minor") Long goodsAmountMinor,
|
||||
@JsonProperty("payable_amount_minor") Long payableAmountMinor,
|
||||
@JsonProperty("paid_amount_minor") Long paidAmountMinor,
|
||||
@JsonProperty("unpaid_amount_minor") Long unpaidAmountMinor,
|
||||
@JsonProperty("discount_amount_minor") Long discountAmountMinor,
|
||||
@JsonProperty("invoiced_amount_minor") Long invoicedAmountMinor,
|
||||
@JsonProperty("uninvoiced_amount_minor") Long uninvoicedAmountMinor,
|
||||
@JsonProperty("overdue_flag") Integer overdueFlag,
|
||||
@JsonProperty("overdue_days") Integer overdueDays,
|
||||
@JsonProperty("payment_collection_days") Integer paymentCollectionDays,
|
||||
@JsonProperty("ordered_at") String orderedAt,
|
||||
@JsonProperty("paid_at") String paidAt,
|
||||
@JsonProperty("last_pay_due_at") String lastPayDueAt,
|
||||
@JsonProperty("signed_at") String signedAt,
|
||||
@JsonProperty("updated_at") String updatedAt,
|
||||
long version,
|
||||
@JsonProperty("deleted_at") String deletedAt) {
|
||||
}
|
||||
|
||||
/** 设备生命周期:network_state 映射 OFFLINE/ONLINE,其余状态位整型透传。 */
|
||||
record DeviceLifecycleChange(
|
||||
@JsonProperty("lifecycle_id") String lifecycleId,
|
||||
@JsonProperty("sn_code") String snCode,
|
||||
@JsonProperty("device_type") Integer deviceType,
|
||||
@JsonProperty("exchange_state") Integer exchangeState,
|
||||
@JsonProperty("up_exchange_state") Integer upExchangeState,
|
||||
@JsonProperty("road_state") Integer roadState,
|
||||
@JsonProperty("inventory_state") Integer inventoryState,
|
||||
@JsonProperty("network_state") String networkState,
|
||||
@JsonProperty("asset_state") Integer assetState,
|
||||
@JsonProperty("agent_id") String agentId,
|
||||
@JsonProperty("agent_name") String agentName,
|
||||
@JsonProperty("asset_owner_id") String assetOwnerId,
|
||||
@JsonProperty("asset_owner_name") String assetOwnerName,
|
||||
@JsonProperty("first_use_at") String firstUseAt,
|
||||
@JsonProperty("last_online_at") String lastOnlineAt,
|
||||
@JsonProperty("discarded_at") String discardedAt,
|
||||
@JsonProperty("last_op_user") String lastOpUser,
|
||||
@JsonProperty("last_op_at") String lastOpAt,
|
||||
@JsonProperty("updated_at") String updatedAt,
|
||||
long version,
|
||||
@JsonProperty("deleted_at") String deletedAt) {
|
||||
}
|
||||
|
||||
/** 电池/设备故障日志:故障类型与状态整型透传,发生/恢复时间只读透传。 */
|
||||
record BatteryFaultChange(
|
||||
@JsonProperty("fault_id") String faultId,
|
||||
@JsonProperty("sn_code") String snCode,
|
||||
@JsonProperty("device_type") Integer deviceType,
|
||||
@JsonProperty("fault_type") Integer faultType,
|
||||
@JsonProperty("fault_state") Integer faultState,
|
||||
@JsonProperty("fault_started_at") String faultStartedAt,
|
||||
@JsonProperty("fault_added_at") String faultAddedAt,
|
||||
@JsonProperty("fault_recovered_at") String faultRecoveredAt,
|
||||
@JsonProperty("install_position") String installPosition,
|
||||
@JsonProperty("district_id") String districtId,
|
||||
@JsonProperty("district_name") String districtName,
|
||||
@JsonProperty("property_id") String propertyId,
|
||||
@JsonProperty("property_name") String propertyName,
|
||||
@JsonProperty("agent_id") String agentId,
|
||||
@JsonProperty("agent_name") String agentName,
|
||||
@JsonProperty("peak_value") Integer peakValue,
|
||||
@JsonProperty("standard_value") Integer standardValue,
|
||||
@JsonProperty("updated_at") String updatedAt,
|
||||
long version,
|
||||
@JsonProperty("deleted_at") String deletedAt) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ import me.iiv.agent.mcp.ToolObject;
|
||||
import me.iiv.agent.mcp.McpErrorCode;
|
||||
import me.iiv.agent.mcp.ToolCallException;
|
||||
import me.iiv.agent.mcp.XqTool;
|
||||
import me.iiv.agent.mcp.sync.BatteryFaultsSyncEntity;
|
||||
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.MallOrdersSyncEntity;
|
||||
import me.iiv.agent.mcp.sync.PropertiesSyncEntity;
|
||||
import me.iiv.agent.mcp.sync.RepaymentDetailsSyncEntity;
|
||||
import me.iiv.agent.mcp.sync.RepaymentTargetsSyncEntity;
|
||||
@@ -25,7 +28,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 增量同步工具(REQ-20260911-0013):users/properties/contracts 三个 changes 工具,
|
||||
* 增量同步工具(REQ-20260911-0013;P2 域扩展于 REQ-20260902-0002):
|
||||
* users/properties/contracts/repayment-targets/repayment-details/mall-orders/
|
||||
* device-lifecycles/battery-faults 八个 changes 工具,
|
||||
* 协议(snapshot/cursor/tombstone/sync_token)由 ChangesEngine 统一实现。
|
||||
* 数据域硬约束:本类不声明 tenant_id/scope 入参,数据域只能从 Token 推导;
|
||||
* GuardedToolCallback 对 *.changes.list 工具的原始入参做二次拦截,传 tenant_id/scope 一律拒绝。
|
||||
@@ -146,6 +151,51 @@ public class ChangesTools implements ToolObject {
|
||||
return changesEngine.execute(RepaymentDetailsSyncEntity.NAME, syncToken, cursor, limit);
|
||||
}
|
||||
|
||||
@XqTool(category = "sync", kind = "read")
|
||||
@Tool(name = "mall-orders.changes.list", description = "增量同步商城订单(m1.mall_goods_order)。" + PROTOCOL_DESC
|
||||
+ "items 字段:order_id/order_no/bill_no/order_state(PENDING_PAYMENT/PENDING_OUTBOUND/"
|
||||
+ "PENDING_SHIPMENT/SHIPPED/SIGNED/RECEIVED/CANCELLED/DELETED)/order_type/accounts_type/"
|
||||
+ "pay_state/order_pay_state/order_invoice_state(状态位整型透传)/"
|
||||
+ "owner_user_id(销售稳定 user.id)/owner_name/agent_id/property_id/district_id/customer_user_id 及名称/"
|
||||
+ "sale_company_id/sale_company_name/金额八项(*_minor,分)/"
|
||||
+ "overdue_flag/overdue_days/payment_collection_days(SaaS 既有逾期口径只读透传)/"
|
||||
+ "ordered_at/paid_at/last_pay_due_at/signed_at/updated_at/version/deleted_at。"
|
||||
+ "行级权限跟随 Token 绑定的人(与 mall.order.list.query 同一 ACL)。")
|
||||
public Map<String, Object> mallOrdersChangesList(
|
||||
@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(MallOrdersSyncEntity.NAME, syncToken, cursor, limit);
|
||||
}
|
||||
|
||||
@XqTool(category = "sync", kind = "read")
|
||||
@Tool(name = "device-lifecycles.changes.list", description = "增量同步设备生命周期(m0.device_life_cycle)。" + PROTOCOL_DESC
|
||||
+ "items 字段:lifecycle_id/sn_code/device_type/exchange_state/up_exchange_state/"
|
||||
+ "road_state/inventory_state(流转状态整型透传)/network_state(OFFLINE/ONLINE)/asset_state/"
|
||||
+ "agent_id/agent_name/asset_owner_id/asset_owner_name/"
|
||||
+ "first_use_at/last_online_at/discarded_at/last_op_user/last_op_at/updated_at/version/deleted_at。"
|
||||
+ "行级权限跟随 Token 绑定的人(代理维度 agent_id)。")
|
||||
public Map<String, Object> deviceLifecyclesChangesList(
|
||||
@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(DeviceLifecyclesSyncEntity.NAME, syncToken, cursor, limit);
|
||||
}
|
||||
|
||||
@XqTool(category = "sync", kind = "read")
|
||||
@Tool(name = "battery-faults.changes.list", description = "增量同步设备故障日志(m0.battery_fault_log)。" + PROTOCOL_DESC
|
||||
+ "items 字段:fault_id/sn_code/device_type/fault_type/fault_state(故障类型与状态整型透传)/"
|
||||
+ "fault_started_at/fault_added_at/fault_recovered_at(SaaS 既有口径只读透传)/install_position/"
|
||||
+ "district_id/district_name/property_id/property_name/agent_id/agent_name/"
|
||||
+ "peak_value/standard_value/updated_at/version/deleted_at。"
|
||||
+ "行级权限跟随 Token 绑定的人(与 battery.fault.list.query 同一 ACL)。")
|
||||
public Map<String, Object> batteryFaultsChangesList(
|
||||
@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(BatteryFaultsSyncEntity.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");
|
||||
|
||||
@@ -5,6 +5,7 @@ import me.iiv.agent.entity.m0.Agent;
|
||||
import me.iiv.agent.entity.m0.AgentManageDistrictAuth;
|
||||
import me.iiv.agent.entity.m0.Authority;
|
||||
import me.iiv.agent.entity.m0.BatteryFaultLog;
|
||||
import me.iiv.agent.entity.m0.DeviceLifeCycle;
|
||||
import me.iiv.agent.entity.m0.DistrictDistrictStrategy;
|
||||
import me.iiv.agent.entity.m0.HardwareView;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
@@ -962,4 +963,16 @@ public class PermissionService {
|
||||
wrapper.in(SalerRepaymentCustomer::getSalerAgentId, scope.ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备生命周期(m0 device_life_cycle)行权限:表内无 property 维度,
|
||||
* 总部/服务号全量,其余按代理范围落到 agent_id(设备资产归属代理)。
|
||||
*/
|
||||
public void applyDeviceLifeCycleRowFilter(LambdaQueryWrapper<DeviceLifeCycle> wrapper, User user) {
|
||||
RowScope scope = resolveRowScope(user);
|
||||
if (scope.headquarters) {
|
||||
return;
|
||||
}
|
||||
wrapper.in(DeviceLifeCycle::getAgentId, scope.ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,19 @@ package me.iiv.agent.mcp.sync;
|
||||
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.BatteryFaultLog;
|
||||
import me.iiv.agent.entity.m0.DeviceLifeCycle;
|
||||
import me.iiv.agent.entity.m0.Properyt;
|
||||
import me.iiv.agent.entity.m0.User;
|
||||
import me.iiv.agent.entity.m1.MallGoodsOrder;
|
||||
import me.iiv.agent.entity.m1.SalerRepayment;
|
||||
import me.iiv.agent.entity.m1.SalerRepaymentCustomer;
|
||||
import me.iiv.agent.entity.m2.ContractNew;
|
||||
import me.iiv.agent.mapper.m0.BatteryFaultLogMapper;
|
||||
import me.iiv.agent.mapper.m0.DeviceLifeCycleMapper;
|
||||
import me.iiv.agent.mapper.m0.ProperytMapper;
|
||||
import me.iiv.agent.mapper.m0.UserMapper;
|
||||
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;
|
||||
@@ -42,7 +48,8 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 三个同步实体的窗口条件、tombstone 与字段映射测试(REQ-20260911-0013)。
|
||||
* 八个同步实体的窗口条件、tombstone 与字段映射测试
|
||||
* (REQ-20260911-0013;回款/订单/设备域扩展于 REQ-20260902-0002 P2)。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SyncEntitiesTest {
|
||||
@@ -58,6 +65,12 @@ class SyncEntitiesTest {
|
||||
@Mock
|
||||
SalerRepaymentCustomerMapper salerRepaymentCustomerMapper;
|
||||
@Mock
|
||||
MallGoodsOrderMapper mallGoodsOrderMapper;
|
||||
@Mock
|
||||
DeviceLifeCycleMapper deviceLifeCycleMapper;
|
||||
@Mock
|
||||
BatteryFaultLogMapper batteryFaultLogMapper;
|
||||
@Mock
|
||||
PermissionService permissionService;
|
||||
|
||||
@BeforeEach
|
||||
@@ -67,6 +80,9 @@ class SyncEntitiesTest {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), ContractNew.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), SalerRepayment.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), SalerRepaymentCustomer.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), MallGoodsOrder.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), DeviceLifeCycle.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), BatteryFaultLog.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -419,4 +435,149 @@ class SyncEntitiesTest {
|
||||
|
||||
assertEquals(McpErrorCode.DATA_INTEGRITY_ERROR, error.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mallOrdersMapsAmountsStateAndOverduePassthrough() {
|
||||
MallOrdersSyncEntity entity = new MallOrdersSyncEntity(mallGoodsOrderMapper, permissionService);
|
||||
MallGoodsOrder order = new MallGoodsOrder();
|
||||
order.setId(41L);
|
||||
order.setOrderNo("SO20261001");
|
||||
order.setBillNo("B202610-1");
|
||||
order.setOrderState(1);
|
||||
order.setOrderType(1);
|
||||
order.setAccountsType(1);
|
||||
order.setPayState(2);
|
||||
order.setOrderInvoiceState(1);
|
||||
order.setCreateId(1001L);
|
||||
order.setCreateName("张销售");
|
||||
order.setAgentId(21L);
|
||||
order.setAgentName("趣致北京");
|
||||
order.setPropertyId(9L);
|
||||
order.setPropertyName("望京SOHO");
|
||||
order.setDistrictId(3L);
|
||||
order.setUserId(501L);
|
||||
order.setUserName("物业采购");
|
||||
order.setSaleCompanyId(21L);
|
||||
order.setSaleCompany("趣致北京");
|
||||
order.setTotalMoney(new BigDecimal("20000.00"));
|
||||
order.setPaidMoney(new BigDecimal("12000.50"));
|
||||
order.setUnPayMoney(new BigDecimal("7999.50"));
|
||||
order.setInvoiceMoney(new BigDecimal("5000.00"));
|
||||
order.setOverdueFlag(1);
|
||||
order.setOverdueDays(12);
|
||||
order.setPaymentCollection(30);
|
||||
order.setOrderTime(Timestamp.valueOf("2026-09-01 10:00:00"));
|
||||
order.setLastPayTime(Timestamp.valueOf("2026-10-01 00:00:00"));
|
||||
order.setSoftDelete(0);
|
||||
order.setModifyTime(Timestamp.valueOf("2026-10-11 08:30:00"));
|
||||
when(mallGoodsOrderMapper.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).applyOrderRowFilter(any(), any());
|
||||
Map<String, Object> item = rows.get(0).item();
|
||||
assertEquals("41", item.get("order_id"));
|
||||
assertEquals("SO20261001", item.get("order_no"));
|
||||
assertEquals("PENDING_OUTBOUND", item.get("order_state"));
|
||||
assertEquals("1001", item.get("owner_user_id"), "负责人=create_id 销售稳定 user.id");
|
||||
assertEquals("望京SOHO", item.get("property_name"));
|
||||
assertEquals(2000000L, item.get("total_amount_minor"), "BigDecimal 元按分输出");
|
||||
assertEquals(1200050L, item.get("paid_amount_minor"));
|
||||
assertEquals(799950L, item.get("unpaid_amount_minor"));
|
||||
assertEquals(500000L, item.get("invoiced_amount_minor"));
|
||||
assertEquals(1, item.get("overdue_flag"), "SaaS 逾期口径只读透传");
|
||||
assertEquals(12, item.get("overdue_days"));
|
||||
assertEquals(30, item.get("payment_collection_days"));
|
||||
assertEquals(1, item.get("order_invoice_state"), "开票状态整型透传");
|
||||
assertEquals("2026-10-01T00:00:00+08:00", item.get("last_pay_due_at"));
|
||||
assertNull(item.get("deleted_at"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mallOrdersRejectsUnknownOrderState() {
|
||||
ToolCallException error = assertThrows(ToolCallException.class,
|
||||
() -> MallOrdersSyncEntity.publicOrderState(42));
|
||||
|
||||
assertEquals(McpErrorCode.DATA_INTEGRITY_ERROR, error.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceLifecyclesMapsNetworkStateAndAppliesFilter() {
|
||||
DeviceLifecyclesSyncEntity entity = new DeviceLifecyclesSyncEntity(deviceLifeCycleMapper, permissionService);
|
||||
DeviceLifeCycle cycle = new DeviceLifeCycle();
|
||||
cycle.setId(61L);
|
||||
cycle.setSnCode("SN123456");
|
||||
cycle.setDeviceType(0);
|
||||
cycle.setExchangeState(9);
|
||||
cycle.setNetworkState(0);
|
||||
cycle.setAssetState(2);
|
||||
cycle.setAgentId(21L);
|
||||
cycle.setAgentName("趣致北京");
|
||||
cycle.setLastOnlineDate(Timestamp.valueOf("2026-10-01 12:00:00"));
|
||||
cycle.setSoftDelete(0);
|
||||
cycle.setModifyTime(Timestamp.valueOf("2026-10-11 08:30:00"));
|
||||
when(deviceLifeCycleMapper.selectList(any())).thenReturn(List.of(cycle));
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
|
||||
List<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, user);
|
||||
|
||||
verify(permissionService).applyDeviceLifeCycleRowFilter(any(), any());
|
||||
Map<String, Object> item = rows.get(0).item();
|
||||
assertEquals("61", item.get("lifecycle_id"));
|
||||
assertEquals("SN123456", item.get("sn_code"));
|
||||
assertEquals("OFFLINE", item.get("network_state"));
|
||||
assertEquals(9, item.get("exchange_state"), "流转状态整型透传");
|
||||
assertEquals("21", item.get("agent_id"));
|
||||
assertEquals("2026-10-01T12:00:00+08:00", item.get("last_online_at"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deviceLifecyclesRejectsUnknownNetworkState() {
|
||||
ToolCallException error = assertThrows(ToolCallException.class,
|
||||
() -> DeviceLifecyclesSyncEntity.publicNetworkState(7));
|
||||
|
||||
assertEquals(McpErrorCode.DATA_INTEGRITY_ERROR, error.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void batteryFaultsMapsFieldsAndAppliesFilterWithTombstone() {
|
||||
BatteryFaultsSyncEntity entity = new BatteryFaultsSyncEntity(batteryFaultLogMapper, permissionService);
|
||||
BatteryFaultLog fault = new BatteryFaultLog();
|
||||
fault.setId(71L);
|
||||
fault.setSnCode("SN123456");
|
||||
fault.setDeviceType(2);
|
||||
fault.setFaultType(3);
|
||||
fault.setFaultState(1);
|
||||
fault.setFaultStartTime(Timestamp.valueOf("2026-10-10 09:00:00"));
|
||||
fault.setFaultRecoveryTime(Timestamp.valueOf("2026-10-11 09:00:00"));
|
||||
fault.setInstallPosition("3F 男卫");
|
||||
fault.setDistrictId(3L);
|
||||
fault.setDistrictName("望京SOHO T3");
|
||||
fault.setPropertyId(9L);
|
||||
fault.setAgentId(21L);
|
||||
fault.setPeakValue(88);
|
||||
fault.setStandardValue(50);
|
||||
fault.setSoftDelete(1);
|
||||
fault.setModifyTime(Timestamp.valueOf("2026-10-11 10:00:00"));
|
||||
when(batteryFaultLogMapper.selectList(any())).thenReturn(List.of(fault));
|
||||
User user = new User();
|
||||
user.setId(7L);
|
||||
|
||||
List<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, user);
|
||||
|
||||
verify(permissionService).applyBatteryFaultLogRowFilter(any(), any());
|
||||
Map<String, Object> item = rows.get(0).item();
|
||||
assertEquals("71", item.get("fault_id"));
|
||||
assertEquals("SN123456", item.get("sn_code"));
|
||||
assertEquals(3, item.get("fault_type"), "故障类型整型透传");
|
||||
assertEquals("2026-10-10T09:00:00+08:00", item.get("fault_started_at"));
|
||||
assertEquals("2026-10-11T09:00:00+08:00", item.get("fault_recovered_at"));
|
||||
assertEquals("3F 男卫", item.get("install_position"));
|
||||
assertEquals("望京SOHO T3", item.get("district_name"));
|
||||
assertEquals(88, item.get("peak_value"));
|
||||
assertEquals("2026-10-11T10:00:00+08:00", item.get("deleted_at"), "软删行返回 tombstone");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 五个 changes 工具的注册与路由(REQ-20260911-0013,回款域扩展于 REQ-20260902-0002 P2)。
|
||||
* 八个 changes 工具的注册与路由(REQ-20260911-0013,P2 域扩展于 REQ-20260902-0002)。
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ChangesToolsTest {
|
||||
@@ -41,9 +41,10 @@ class ChangesToolsTest {
|
||||
callback -> callback.getToolDefinition().name(),
|
||||
callback -> callback.getToolDefinition().inputSchema()));
|
||||
|
||||
assertEquals(6, schemas.size());
|
||||
assertEquals(9, schemas.size());
|
||||
for (String name : new String[]{"users.changes.list", "properties.changes.list", "contracts.changes.list",
|
||||
"repayment-targets.changes.list", "repayment-details.changes.list"}) {
|
||||
"repayment-targets.changes.list", "repayment-details.changes.list",
|
||||
"mall-orders.changes.list", "device-lifecycles.changes.list", "battery-faults.changes.list"}) {
|
||||
assertTrue(schemas.containsKey(name), "缺少工具 " + name);
|
||||
String schema = schemas.get(name);
|
||||
assertTrue(schema.contains("syncToken"));
|
||||
@@ -75,6 +76,15 @@ class ChangesToolsTest {
|
||||
|
||||
tools.repaymentDetailsChangesList(null, null, 100);
|
||||
verify(changesEngine).execute("repayment-details", null, null, 100);
|
||||
|
||||
tools.mallOrdersChangesList("t4", null, null);
|
||||
verify(changesEngine).execute("mall-orders", "t4", null, null);
|
||||
|
||||
tools.deviceLifecyclesChangesList(null, "c5", 10);
|
||||
verify(changesEngine).execute("device-lifecycles", null, "c5", 10);
|
||||
|
||||
tools.batteryFaultsChangesList(null, null, null);
|
||||
verify(changesEngine).execute("battery-faults", null, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user