You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
48 lines
5.2 KiB
48 lines
5.2 KiB
|
17 hours ago
|
---
|
||
|
|
status: accepted
|
||
|
|
---
|
||
|
|
|
||
|
|
# 客户目录 inbound seam:CustomerCatalogPort(crm-customer 声明并实现,crm-opportunity 调用)
|
||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
架构审查(`/improve-codebase-architecture`,2026-09-06 第三轮,候选 #1):商机详情「关联客户」子域对客户域只有 **3 个只读调用点**——候选池搜索(`searchCustomers`)、联系人级联(`listCustomerContacts`)、关联客户存在性校验(`addCustomer` 里的 `getById`)——却为此 import 了客户域两套宽服务接口 `ICustomerService`(8 方法 + CRUD)/ `ICustomerContactService`(8 方法 + CRUD)、实体 `Customer`、域内 `CustomerSearchParam`/`CustomerSearchItemDTO`。与 ADR-0031 修掉的 crm-lead 宽接口泄漏同形(方向相反:`opportunity → customer`)。
|
||
|
|
|
||
|
|
审查同时发现**归档口径缝隙**(本次一并拍板修复):
|
||
|
|
|
||
|
|
1. `getById` 只滤 `@TableLogic` 软删、不滤 `archive_status`——已归档客户可通过「关联客户存在」校验被**新关联**;
|
||
|
|
2. `CustomerMapper.searchForOpportunity` 的 `archiveStatus` 是可空 `<if>` 条件——商机侧没传时候选池**含归档客户**。
|
||
|
|
|
||
|
|
方向约束:`crm-opportunity → crm-customer` 为既定 Maven 单向依赖(对称 ADR-0029 记录的 lead 方向债),本 ADR 不改 pom 方向,只收窄跨域**契约面**。
|
||
|
|
|
||
|
|
## Considered Options
|
||
|
|
|
||
|
|
- **A(选中):crm-customer 声明窄 inbound port `CustomerCatalogPort`(3 方法 + 随包 record)并自实现,crm-opportunity 只依赖该 port。** 归档口径由端口钉死:`search` 与 `requireLive` 一律只认 `archive_status=1 有效`;宽服务退回客户域内部(本域 Controller 照用)。对称 ADR-0031 的 `ConvertibleLeadCatalogPort`。
|
||
|
|
- **B:保留宽服务注入,仅修归档缝隙。** 契约面未收窄——两套 8 方法接口 + 实体 + 域内 param/DTO 仍在商机侧编译面,同 ADR-0031 选项 B 的否决理由,否。
|
||
|
|
- **C:商机侧直查 customer 表。** 跨域 DB 耦合、商机侧自己判定归档口径,违背 seam 精神(同 ADR-0025 选项 B 否决理由),否。
|
||
|
|
|
||
|
|
## Decision
|
||
|
|
|
||
|
|
crm-customer 新增 `com.crm.customer.port`(既有扁平 port 包):
|
||
|
|
|
||
|
|
- `CustomerCatalogPort`:接口 3 方法——`PageResult<CustomerBrief> search(CustomerCatalogQuery)` / `List<ContactBrief> contactsOf(Long customerId)` / `CustomerBrief requireLive(Long customerId)`;
|
||
|
|
- 随包 record:`CustomerBrief`(id/编号/名称/类型/负责人名——名快照真源)、`ContactBrief`(contactId/name/phone/company/position——商机字段17级联列)、`CustomerCatalogQuery`(keyword/customerType/current/size/excludeCustomerIds;**不含 archiveStatus**,消费方无法放宽归档口径);
|
||
|
|
- `CustomerCatalogException`:unchecked,住 port 包;
|
||
|
|
- `impl/CustomerCatalogPortImpl`:`@Service`,委托 `ICustomerService`/`ICustomerContactService`——`search` 强制 `archiveStatus=ARCHIVE_STATUS_ACTIVE`;`requireLive` 委托 `getById` 后自查归档,null → 「客户不存在」、归档 → 「客户已归档,不可被关联」。
|
||
|
|
|
||
|
|
三项配套拍板:
|
||
|
|
|
||
|
|
1. **归档口径**:目录 = 有效客户目录。新关联拒归档(`requireLive`);候选池拒归档(`search` 强制有效);**存量已关联行不动**(不回溯清理)。排除已关联客户(`excludeCustomerIds`)是商机候选池语义,由消费方传入——端口不知 opportunity 表(同 ADR-0025 职责边界)。
|
||
|
|
2. **宽服务方法处置**:`ICustomerService.searchForOpportunity` 改名 `search` 留在本域(客户域 Controller 的 `POST /api/customer/search` 端点是第二消费方,HTTP 契约零变化;mapper 方法名不动)。
|
||
|
|
3. **异常不跨 seam**(ADR-0030 口径):商机侧 catch `CustomerCatalogException` 后翻译为自己的 `CODE_OPP_INVALID`(66001)+「关联客户不存在或已被归档」。
|
||
|
|
|
||
|
|
crm-opportunity 侧 `OpportunitySubServiceImpl` 三调用点改注入 port,删全部 `com.crm.customer.service.*`/`com.crm.customer.domain.*` import;测试换 mock 单 port。
|
||
|
|
|
||
|
|
## Consequences
|
||
|
|
|
||
|
|
- 商机侧对客户域的编译契约面从「两套宽服务(各 8 方法 + CRUD)+ 实体 + 域内 param/DTO」收窄到「3 方法 port + 随包 record」。宽接口演进不再迫使商机侧重编译;port 即商机侧的测试面(mock 单接口)。
|
||
|
|
- **行为收紧**(本次拍板的产品口径):已归档客户不再出现在商机候选池、不可被新关联;存量已关联行不动。`addCustomer` 拒绝消息从「关联客户不存在或已被删除」改为「关联客户不存在或已被归档」(同一 66001 错误码)。
|
||
|
|
- 客户域本域消费不受影响:`listByCustomer`(客户 Controller 在用)、`search`(HTTP 端点在用)留在宽服务。
|
||
|
|
- 测试:商机单测 mock 单 port(归档翻译路径用例);新增 `CustomerCatalogPortImplIntegrationTest`(H2)钉死归档口径——含「裸宽服务同条件返回全部、走 port 只返回有效」的对照断言(证明口径由 port 强制而非 mapper 天生)。
|
||
|
|
- 决策来源:`/improve-codebase-architecture` 审查(2026-09-06,候选 #1)+ grilling 七项拍板(归档口径 / 命名 / interface 形状 / 异常契约 / 机制归属 / 单票范围 / 留痕)。
|