feat(sync): expose contract display context #17
@@ -100,6 +100,7 @@ arguments: {"contractIds":["1001","1002"]}
|
||||
|
||||
- 每批 1—200 个稳定合同 ID,返回 `checked_at/count/items/not_found_ids`。
|
||||
- `items` 使用与合同变化流相同的公开字段;软删合同返回删除标记,查无记录进入 `not_found_ids`。
|
||||
- 合同展示上下文包含`contract_no/contract_name/property_name/customer_no/customer_name/owner_name`;这些字段用于页面、AI解释和人工核对,关联、授权和接收人选择只能使用稳定ID。
|
||||
- CRM 必须逐项复核状态、到期日、负责人、客户归属、提醒配置和续约结果。任一变化、缺失、删除、响应不完整、证据时间异常或服务不可用,对应合同本次都不得发送。
|
||||
- 该工具属于 SYNC 能力和限流档,调用方传 `tenant_id/tenantId/scope` 会被拒绝;数据域只能来自服务 Token。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SaaS MCP → 小趣 AI 经营系统对接进度
|
||||
|
||||
> MCP 系统独立进度入口。最后核对:2026-09-15;代码位于`codex/crm-contract-sync-review`本地评审分支。
|
||||
> MCP 系统独立进度入口。最后核对:2026-09-15;当前字段扩展位于`codex/mcp-source-domain-identity`分支。
|
||||
>
|
||||
> 本文只记录 `xiaoqu_mcp_service` 的责任和状态。小趣 AI / CRM 的规则、事项、AI、页面与推送进度,以其仓库 `PROJECT.md` 为准。
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
|
||||
## 2. 当前结论
|
||||
|
||||
当前状态:**协议可连接,公开字段Schema已在本地修正并通过回归;可靠同步与服务身份仍未通过,不能用于真实销售合同到期推送。**
|
||||
当前状态:**协议可连接,基础公开字段Schema已部署;合同展示上下文字段已完成代码和回归、待合并部署;可靠同步与服务身份仍未通过,不能用于真实销售合同到期推送。**
|
||||
|
||||
交付状态:修正位于独立分支`codex/crm-contract-sync-review`,本地提交已形成;当前执行环境没有`wangheng/xiaoqu_mcp_service`写权限,尚未推送、合并或部署,生产服务仍是旧行为。
|
||||
交付状态:三条变化流和发送前复核代码已经合入主线并部署;生产`sync`作用域尚未绑定`contracts.current.batch_get`。本轮新增的`property_name/customer_no/customer_name/owner_name`仍待合并部署。
|
||||
|
||||
已经具备:
|
||||
|
||||
@@ -58,9 +58,9 @@
|
||||
|---|---|---|
|
||||
| users | `mobile`;数值型`user_id/organization_ids` | `verified_mobile`;字符串型稳定ID;状态使用公开枚举 |
|
||||
| properties | 数值型`property_id` | 字符串型稳定ID;状态使用公开枚举 |
|
||||
| contracts | 数值型`contract_id/property_id/owner_user_id`、数值状态`0..4`、数值`expire_remind`,缺`remind_days` | 字符串型稳定ID、`PENDING/REJECTED/ACTIVE/EXPIRED/TERMINATED/VOID`、布尔提醒标记、实际提醒天数 |
|
||||
| contracts | 数值型`contract_id/property_id/owner_user_id`、数值状态`0..4`、数值`expire_remind`,缺`remind_days`及客户/物业/负责人展示上下文 | 字符串型稳定ID、`PENDING/REJECTED/ACTIVE/EXPIRED/TERMINATED/VOID`、布尔提醒标记、实际提醒天数,以及`property_name/customer_no/customer_name/owner_name`展示字段 |
|
||||
|
||||
已完成:新增独立公开DTO,将三类稳定ID统一为字符串;`mobile`改为`verified_mobile`;合同状态转换为公开枚举;`expire_remind`转换为布尔值;补齐`remind_days`;变化页使用统一`stream`字段。未知合同状态按`DATA_INTEGRITY_ERROR`失败关闭,不伪造业务语义。
|
||||
已完成:新增独立公开DTO,将三类稳定ID统一为字符串;`mobile`改为`verified_mobile`;合同状态转换为公开枚举;`expire_remind`转换为布尔值;补齐`remind_days`;变化页使用统一`stream`字段。合同DTO继续补齐`property_name/customer_no/customer_name/owner_name`,这些字段只用于展示、AI解释和人工核对,身份关联与权限仍只认稳定ID。未知合同状态按`DATA_INTEGRITY_ERROR`失败关闭,不伪造业务语义。
|
||||
|
||||
验证:合并最新主分支后,Java编译、管理前端构建及MCP服务全量`229`项测试通过,`0`失败、`0`错误。尚需部署后的真实数据样例和小趣Gateway严格消费者契约联调,完成前仍不算生产通过。
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
小趣AI侧已经按同一契约实现短时发送凭证:只有状态、到期日、负责人、客户归属、提醒字段及续约结果与本地候选一致才允许R04进入推送;缺失、删除、字段变化、响应不完整或MCP不可用均失败关闭,R03不受影响。
|
||||
|
||||
尚未完成:本分支未推送和部署;生产`sync`作用域还必须绑定该新工具;需要用真实合同验证负责人变更、删除、到期日修改和服务不可用四类反向场景。
|
||||
尚未完成:生产`sync`作用域还必须绑定该新工具;需要用真实合同验证负责人变更、删除、到期日修改和服务不可用四类反向场景。
|
||||
|
||||
## 4. 依赖顺序
|
||||
|
||||
@@ -121,8 +121,8 @@
|
||||
| MCP协议与工具发现 | 已完成 | MCP | 保持兼容并纳入回归 |
|
||||
| 服务密钥与Token基础生命周期 | 已完成 | MCP | 在租户模型完成后升级幂等键 |
|
||||
| 源数据域标识与服务主体隔离 | 未完成,P0阻塞 | MCP | 增加稳定部署数据域标识和Open Client关联 |
|
||||
| 三流公开字段Schema | 本地代码完成,待部署联调 | MCP | 部署后用真实三流样例跑Gateway严格契约测试 |
|
||||
| 发送前合同当前状态批量复核 | 本地代码完成,待部署联调 | MCP + 小趣AI | 绑定sync作用域并完成四类真实反向用例 |
|
||||
| 三流公开字段Schema | 基础字段已部署;合同展示字段待合并部署 | MCP | 部署后用真实三流样例跑Gateway严格契约测试 |
|
||||
| 发送前合同当前状态批量复核 | 代码已部署、生产scope未开放 | MCP + 小趣AI | 绑定sync作用域并完成四类真实反向用例 |
|
||||
| 可靠变化日志与稳定快照 | 未完成,P0阻塞 | MCP | 设计Outbox/CDC及保留策略 |
|
||||
| 权限版本与交接位点生命周期 | 未完成,P0阻塞 | MCP | 与租户/变化日志模型一起实现 |
|
||||
| 小趣AI连接与消费代码 | 已完成,待部署联调 | 小趣AI | 见小趣AI仓库`PROJECT.md` |
|
||||
|
||||
@@ -114,10 +114,12 @@ public class ContractsSyncEntity extends AbstractSyncEntity implements SyncEntit
|
||||
Date effective = row.getModifyTime() != null ? row.getModifyTime() : row.getCreateTime();
|
||||
return SyncChangeSchemas.toMap(new SyncChangeSchemas.ContractChange(
|
||||
stableId(row.getId()), row.getContractNo(), row.getContractName(),
|
||||
row.getPropertyName(), row.getCustomerNo(), row.getCustomerName(),
|
||||
publicStatus(row.getContractStatus()), ChangesEngine.iso(row.getStartDate()),
|
||||
ChangesEngine.iso(row.getEndDate()), toMinor(row.getAmount()),
|
||||
row.getPropertyId() == null ? null : customerIds.get(row.getPropertyId()),
|
||||
stableId(row.getPropertyId()), stableId(row.getSaleId()),
|
||||
row.getSaleName(),
|
||||
Integer.valueOf(1).equals(row.getExpireRemind()), row.getRemindDays(),
|
||||
ChangesEngine.iso(effective), mt, deleted ? ChangesEngine.iso(effective) : null));
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ final class SyncChangeSchemas {
|
||||
@JsonProperty("contract_id") String contractId,
|
||||
@JsonProperty("contract_no") String contractNo,
|
||||
@JsonProperty("contract_name") String contractName,
|
||||
@JsonProperty("property_name") String propertyName,
|
||||
@JsonProperty("customer_no") String customerNo,
|
||||
@JsonProperty("customer_name") String customerName,
|
||||
String status,
|
||||
@JsonProperty("start_at") String startAt,
|
||||
@JsonProperty("end_at") String endAt,
|
||||
@@ -64,6 +67,7 @@ final class SyncChangeSchemas {
|
||||
@JsonProperty("customer_id") String customerId,
|
||||
@JsonProperty("property_id") String propertyId,
|
||||
@JsonProperty("owner_user_id") String ownerUserId,
|
||||
@JsonProperty("owner_name") String ownerName,
|
||||
@JsonProperty("expire_remind") boolean expireRemind,
|
||||
@JsonProperty("remind_days") Integer remindDays,
|
||||
@JsonProperty("updated_at") String updatedAt,
|
||||
|
||||
@@ -79,7 +79,8 @@ public class ChangesTools implements ToolObject {
|
||||
@Tool(name = "contracts.changes.list", description = "增量同步合同主数据(m2.contract_new)。" + PROTOCOL_DESC
|
||||
+ "items 字段:contract_id/contract_no/contract_name/status(PENDING/REJECTED/ACTIVE/EXPIRED/TERMINATED)/"
|
||||
+ "start_at/end_at(带时区)/amount_minor(金额,分)/customer_id/property_id/"
|
||||
+ "owner_user_id(负责人稳定 user.id)/expire_remind/remind_days/updated_at/version/deleted_at。"
|
||||
+ "property_name/customer_no/customer_name/owner_user_id(负责人稳定 user.id)/owner_name/"
|
||||
+ "expire_remind/remind_days/updated_at/version/deleted_at。"
|
||||
+ "行级权限跟随 Token 绑定的人(与 contract.list.query 同一 ACL)。")
|
||||
public Map<String, Object> contractsChangesList(
|
||||
@ToolParam(required = false, description = SYNC_TOKEN_DESC) String syncToken,
|
||||
|
||||
@@ -182,9 +182,13 @@ class SyncEntitiesTest {
|
||||
contract.setId(9L);
|
||||
contract.setContractNo("452542C001");
|
||||
contract.setContractName("望京SOHO厕纸服务合同");
|
||||
contract.setPropertyName("望京SOHO");
|
||||
contract.setCustomerNo("452542");
|
||||
contract.setCustomerName("金蝶客户名称");
|
||||
contract.setContractStatus(2);
|
||||
contract.setPropertyId(1L);
|
||||
contract.setSaleId(1001L);
|
||||
contract.setSaleName("张销售");
|
||||
contract.setExpireRemind(1);
|
||||
contract.setRemindDays(90);
|
||||
contract.setAmount(new BigDecimal("120000.00"));
|
||||
@@ -205,6 +209,9 @@ class SyncEntitiesTest {
|
||||
verify(permissionService).applyContractRowFilter(any(), any());
|
||||
assertEquals("9", rows.get(0).item().get("contract_id"));
|
||||
assertEquals("452542C001", rows.get(0).item().get("contract_no"));
|
||||
assertEquals("望京SOHO", rows.get(0).item().get("property_name"));
|
||||
assertEquals("452542", rows.get(0).item().get("customer_no"));
|
||||
assertEquals("金蝶客户名称", rows.get(0).item().get("customer_name"));
|
||||
assertEquals("ACTIVE", rows.get(0).item().get("status"));
|
||||
assertEquals("2026-01-01T00:00:00+08:00", rows.get(0).item().get("start_at"));
|
||||
assertEquals("2026-12-31T00:00:00+08:00", rows.get(0).item().get("end_at"));
|
||||
@@ -212,6 +219,7 @@ class SyncEntitiesTest {
|
||||
assertEquals("KD10086", rows.get(0).item().get("customer_id"));
|
||||
assertEquals("1", rows.get(0).item().get("property_id"));
|
||||
assertEquals("1001", rows.get(0).item().get("owner_user_id"), "负责人=稳定 user.id");
|
||||
assertEquals("张销售", rows.get(0).item().get("owner_name"));
|
||||
assertEquals(true, rows.get(0).item().get("expire_remind"));
|
||||
assertEquals(90, rows.get(0).item().get("remind_days"));
|
||||
assertEquals(Timestamp.valueOf("2026-09-01 13:54:21").getTime() / 1000,
|
||||
|
||||
Reference in New Issue
Block a user