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,