fix(mcp): 合同同步补续约关联字段 renewal_of_contract_id

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 通过
This commit is contained in:
zhiyuncai
2026-10-15 15:14:03 +08:00
parent b7308c836a
commit 32b6719376
4 changed files with 44 additions and 1 deletions
@@ -159,4 +159,10 @@ public class ContractNew implements Serializable {
* 终止详细原因
*/
private String stopReason;
/**
* 续签来源合同id(新合同指向其续签的旧合同,单向;旧合同行不反向引用)。
* 2026-10-15 核对:表实际存在 renew_source_id 列,此前遗漏导致续约关联未进入同步报文。
*/
private Long renewSourceId;
}
@@ -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));
}
@@ -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) {
@@ -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<SyncRow> rows = entity.fetchPage(null, 2000L, null, null, 3, null);
assertEquals("22", rows.get(0).item().get("renewal_of_contract_id"), "变更流必须输出续约前合同");
List<Map<String, Object>> 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<SyncRow> 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,