From 32b6719376de2a5d1038723dcb867ded7551fcde Mon Sep 17 00:00:00 2001 From: zhiyuncai Date: Thu, 15 Oct 2026 15:14:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(mcp):=20=E5=90=88=E5=90=8C=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E8=A1=A5=E7=BB=AD=E7=BA=A6=E5=85=B3=E8=81=94=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=20renewal=5Fof=5Fcontract=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit m2.contract_new 实际存在 renew_source_id 列(新合同单向指向续签前合同), 但 ContractNew 实体未映射该列,ContractsSyncEntity 按'表无此列'将续约字段 从变更流与 current 报文中整体省略,导致下游 CRM 无法识别旧合同已被续约, R04 到期事项无法自动关闭。 - ContractNew 补映射 renew_source_id - SyncChangeSchemas.ContractChange 增加 renewal_of_contract_id 输出分量 - ContractsSyncEntity.toItem 输出稳定字符串 id;变更流与 current 共用,一次修好 - SyncEntitiesTest 新增回归:变更流/current 均输出、无续约来源时字段省略 测试:SyncEntitiesTest 9/9,全量 448/448 通过 --- .../me/iiv/agent/entity/m2/ContractNew.java | 6 ++++ .../agent/mcp/sync/ContractsSyncEntity.java | 4 ++- .../iiv/agent/mcp/sync/SyncChangeSchemas.java | 1 + .../iiv/agent/mcp/sync/SyncEntitiesTest.java | 34 +++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractNew.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractNew.java index a567150..a098098 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractNew.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m2/ContractNew.java @@ -159,4 +159,10 @@ public class ContractNew implements Serializable { * 终止详细原因 */ private String stopReason; + + /** + * 续签来源合同id(新合同指向其续签的旧合同,单向;旧合同行不反向引用)。 + * 2026-10-15 核对:表实际存在 renew_source_id 列,此前遗漏导致续约关联未进入同步报文。 + */ + private Long renewSourceId; } diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/ContractsSyncEntity.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/ContractsSyncEntity.java index 65e5d8d..d995a72 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/ContractsSyncEntity.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/sync/ContractsSyncEntity.java @@ -27,7 +27,8 @@ import java.util.stream.Collectors; * 字段口径(2026-09-14 字段核对):owner_user_id → sale_id(稳定 user.id); * customer_id 经 property_id 回查 properyt.customerId(金蝶客户 id);金额转分(amount_minor); * start_at/end_at 为带时区 ISO;version 由 modify_time 派生(表无 version 列,决策 1)。 - * 表无 currency / renewal_of_contract_id / renewal_result / deleted_at 列,按铁律"表里不存在的字段不造"省略, + * 表无 currency / renewal_result 列,按铁律"表里不存在的字段不造"省略; + * renewal_of_contract_id 由表列 renew_source_id 映射(2026-10-15 核对补入,此前按无列省略属误判), * 软删 tombstone 的 deleted_at 以 modify_time 充当。 * 行级权限:Token 绑定人时走 PermissionService.applyContractRowFilter(与 contract.list.query 同一 ACL)。 */ @@ -121,6 +122,7 @@ public class ContractsSyncEntity extends AbstractSyncEntity implements SyncEntit stableId(row.getPropertyId()), stableId(row.getSaleId()), row.getSaleName(), Integer.valueOf(1).equals(row.getExpireRemind()), row.getRemindDays(), + stableId(row.getRenewSourceId()), ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null)); } 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 c68708d..a664cd6 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 @@ -70,6 +70,7 @@ final class SyncChangeSchemas { @JsonProperty("owner_name") String ownerName, @JsonProperty("expire_remind") boolean expireRemind, @JsonProperty("remind_days") Integer remindDays, + @JsonProperty("renewal_of_contract_id") String renewalOfContractId, @JsonProperty("updated_at") String updatedAt, long version, @JsonProperty("deleted_at") String deletedAt) { 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 8e14d4c..0b3bc06 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 @@ -22,9 +22,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -227,6 +230,37 @@ class SyncEntitiesTest { assertEquals("2026-09-01T13:54:21+08:00", rows.get(0).item().get("deleted_at"), "软删行返回 tombstone"); } + @Test + void contractsMapsRenewalSourceIdToWireField() { + // 回归:m2.contract_new.renew_source_id 是续签关联列(新合同单向指向旧合同)。 + // 2026-10-15 前实体未映射该列,变更流与 current 报文均丢续约关系, + // 下游 CRM 无法识别"旧合同已被续约",R04 事项永远不会自动关闭。 + ContractsSyncEntity entity = new ContractsSyncEntity(contractNewMapper, properytMapper, permissionService); + ContractNew contract = new ContractNew(); + contract.setId(23L); + contract.setContractNo("452542C007"); + contract.setContractStatus(2); + contract.setPropertyId(1L); + contract.setSoftDelete(0); + contract.setRenewSourceId(22L); + contract.setModifyTime(Timestamp.valueOf("2026-10-15 13:30:00")); + // fetchCurrent 会原地 sort mapper 返回的列表,桩必须每次给可变新实例 + when(contractNewMapper.selectList(any())).thenAnswer(inv -> new ArrayList<>(List.of(contract))); + when(properytMapper.selectBatchIds(anyList())).thenReturn(List.of()); + + List rows = entity.fetchPage(null, 2000L, null, null, 3, null); + assertEquals("22", rows.get(0).item().get("renewal_of_contract_id"), "变更流必须输出续约前合同"); + + List> current = entity.fetchCurrent(List.of(23L), null); + assertEquals(1, current.size()); + assertEquals("22", current.get(0).get("renewal_of_contract_id"), "current 接口必须同步输出续约前合同"); + + // 非续签合同的该列为 NULL:字段按 NON_NULL 整体省略,不得出现 null 值键 + contract.setRenewSourceId(null); + List plain = entity.fetchPage(null, 2000L, null, null, 3, null); + assertFalse(plain.get(0).item().containsKey("renewal_of_contract_id"), "无续约来源时字段应省略"); + } + @Test void contractsRejectUnknownLegacyStatus() { ToolCallException error = assertThrows(ToolCallException.class,