diff --git a/crm-lead/CONTEXT.md b/crm-lead/CONTEXT.md index 288f1ea..75fc0b2 100644 --- a/crm-lead/CONTEXT.md +++ b/crm-lead/CONTEXT.md @@ -92,6 +92,10 @@ _Avoid_: 商机接口、转化服务、OpportunityService 公海池删除守卫的入站实现 `PoolLeadOccupationAdapter`(crm-lead 实现 crm-rule 声明的 `PoolLeadOccupationPort`,ADR-0025)。查 lead 表 `pool_id` 匹配且 `status` 为非终态(1/2/3/4)的 count > 0。供 `LeadPoolServiceImpl.deletePool()` 调用——crm-rule 不依赖 crm-lead,通过依赖倒置跨模块查询。与「转商机端口」方向相反的同型 seam:转商机是 crm-lead 声明等外部实现,池占用是 crm-rule 声明等本模块实现。 _Avoid_: 占用检查服务、pool 查询服务(要点是「实现 crm-rule 的出站端口、跨模块查 lead 表」) +**可转商机候选线索目录端口**: +商机侧直接新建入口(票 12,oppSource=线索转入)「关联线索」下拉数据源的入站契约 `ConvertibleLeadCatalogPort`(crm-lead 声明并自实现,crm-opportunity 调用,ADR-0031)。单方法 `listConvertibleLeads(ownerUserId)`,准入 = 该用户名下 `status IN (已领取 3 / 跟进中 4)`(与转商机前置对称,线索域判定);「未被商机占用」过滤由商机侧补(本端口不知 opportunity 表)。随端口同包的轻量投影 `ConvertibleLeadView`(id/名称/电话)是线索域对商机侧发布的契约面,外域不得 import 线索域 `service`/`domain` 内部包。与「池占用查询适配器」方向相反的同型 seam:池占用是 crm-rule 声明的出站端口、本模块实现;本端口是 crm-lead 自声明的入站契约、供下游 crm-opportunity 调用(收窄 ADR-0029 的 `ILeadService` 泄漏)。 +_Avoid_: 候选线索服务、关联线索查询(要点是「线索域自发布的窄入站契约、只暴露一个查询方法」) + **状态机守卫**: 状态迁移的统一前置校验(ADR-0022 候选 1),收在 `LeadTransition` 内的 `guard`(原散落在 service 与 transition 两处的 19 处判定)。每条 `TransitionCmd` 自带声明 `allowedFromStatuses()`(合法起始态集合)与 `requiresOwner()`(是否要求操作人=领取人,纯代码常量、不建表);`execute` 分派前统一跑一次,在 ADR-0021 的 CAS 之上给出友好前置——非法起始态抛 `CODE_STATUS_NOT_ALLOWED`、非领取人抛 `CODE_NOT_OWNER`。「起始态 × 命令内容」的组合子规则(作废+反馈无效、作废自环换 owner)仍留在各 `applyXxx`。ADMIN_ONLY 池规则、转商机端口调用时序等要查 crm-rule/port 的前置留在 `LeadServiceImpl`,守住 `LeadTransition` 对 crm-rule/crm-auth 的零依赖契约。 _Avoid_: 前置校验、参数校验、状态检查(要点是「声明式起始态集合 + 一处 guard」) diff --git a/docs/adr/0031-convertible-lead-catalog-port-seam.md b/docs/adr/0031-convertible-lead-catalog-port-seam.md new file mode 100644 index 0000000..90c36cb --- /dev/null +++ b/docs/adr/0031-convertible-lead-catalog-port-seam.md @@ -0,0 +1,41 @@ +--- +status: accepted +--- + +# 可转商机候选线索目录 inbound seam:ConvertibleLeadCatalogPort(crm-lead 声明并实现,crm-opportunity 调用) + +## Context + +架构审查(`/improve-codebase-architecture`,2026-08-25,候选 #2):商机侧直接新建入口(票 12,oppSource=线索转入)需要一份「关联线索」下拉数据源——该用户名下 `status IN (已领取 3 / 跟进中 4)` 的候选线索。此前 `OpportunityCreateServiceImpl` 为这**唯一一处查询**注入了 crm-lead 的宽服务接口 `ILeadService`(20+ 方法),并 import `com.crm.lead.domain.dto.ConvertibleLeadView`。 + +这放大了 [ADR-0029](0029-opportunity-depends-on-lead-tech-debt.md) 记录的 `crm-opportunity → crm-lead` 依赖:商机侧不仅背着 pom 依赖,还把一整个宽域服务接口 + 域内 DTO 包拖进了自己的编译面,契约面远宽于实际所需(仅一个方法)。宽接口一旦演进(新增/改签名),商机侧被动重编译;`domain/dto` 是线索域内部包,本不该对外域可见。 + +方向约束:`crm-lead → crm-rule` 为既定单向;商机侧对线索的依赖是 ADR-0029 已接受的技术债,本 ADR **不改 pom 方向**,只收窄跨域**契约面**。 + +## Considered Options + +- **A(选中):crm-lead 声明窄 inbound port `ConvertibleLeadCatalogPort`(单方法)并自实现,crm-opportunity 只依赖该 port + 随包 view。** 宽服务接口 `ILeadService.listConvertibleLeads` 及其实现删除,逻辑原样迁入 `ConvertibleLeadCatalogPortImpl`(直查 `LeadMapper`,票 12 口径不变)。`ConvertibleLeadView` 从 `domain/dto` 移到 `port/inbound` 随端口同包,成为线索域对外发布的契约面(对称 `port/outbound` 的 `CreateOpportunityCmd`/`OpportunityCreationException` cmd/异常随端口同包先例)。 +- **B:保留 `ILeadService` 注入,仅移 `ConvertibleLeadView` 到契约包。** 契约面仍是 20+ 方法宽接口,泄漏未收窄,否。 +- **C:商机侧直查 lead 表。** 无 Java 依赖但 DB 层耦合、跨域判定线索准入口径,违背 seam 精神(同 ADR-0025 选项 B 的否决理由),否。 + +与 [ADR-0025](0025-pool-lead-occupation-port-seam.md) 的关系:两者都是把跨域协作收进一个窄 port,但**方向相反**。ADR-0025 是 crm-rule 声明 **outbound** port、crm-lead 实现(下游声明、上游依赖倒置实现);本 ADR 是 crm-lead 声明 **inbound** port 并**自实现**、crm-opportunity 调用(上游发布契约面、下游消费)。对称 `port/outbound` 的 `OpportunityCreationPort`:出站契约 lead 声明他域实现,入站契约本域声明本域实现供他域调用。 + +## Decision + +crm-lead 新增 `com.crm.lead.port.inbound` 包: + +- `ConvertibleLeadCatalogPort`:接口,单方法 `List listConvertibleLeads(Long ownerUserId)`; +- `ConvertibleLeadView`:record(id/名称/电话轻量投影),从 `domain/dto` 迁入随端口同包——本包类型是线索域对商机侧发布的契约面,外域不得 import 线索域 `service`/`domain` 内部包; +- `impl/ConvertibleLeadCatalogPortImpl`:`@Service`,逻辑自 `LeadServiceImpl.listConvertibleLeads` 原样迁入(直查 `LeadMapper`:归属 + `status IN (STATUS_CLAIMED 3 / STATUS_FOLLOWING 4)`),不复用宽服务接口。 + +删除 `ILeadService.listConvertibleLeads` 声明 + `LeadServiceImpl` 实现 + `domain/dto/ConvertibleLeadView`。 + +crm-opportunity 侧 4 处改注入 port:`OpportunityCreateServiceImpl`(`ILeadService` → `ConvertibleLeadCatalogPort`)、`IOpportunityCreateService`、`OpportunityCreateController`、及测试;「未被商机占用」过滤(查 `opportunity.source_lead_id`)仍留商机侧——线索域不知 opportunity 表,职责边界与 ADR-0025 一致。 + +## Consequences + +- crm-opportunity 对 crm-lead 的编译契约面从「宽服务接口 `ILeadService`(20+ 方法)+ 域内 `domain/dto`」收窄到「单方法 `ConvertibleLeadCatalogPort` + 随包 view」。`grep` 确认商机侧不再 import 任何 `com.crm.lead.service.*` / `com.crm.lead.domain.*`,只剩 `com.crm.lead.port.*`(契约面)。ADR-0029 泄漏的 `ILeadService` 半边自此消除。 +- **pom 依赖方向不变**(仍 `crm-opportunity → crm-lead`,ADR-0029 技术债照旧)——本 ADR 只收窄契约面,方向随 ADR-0020 独立部署时再重设。 +- 准入口径不变(status IN 3/4,线索域判定);占用过滤留商机侧。逻辑原样迁移,无行为变化。 +- 测试随迁不 layer:`LeadConvertibleQueryTest`(测宽服务方法)重命名为 `ConvertibleLeadCatalogPortImplTest`(测端口实现),断言口径不变;`OpportunityCreateServiceImplTest` 的候选线索用例改 mock `ConvertibleLeadCatalogPort`。验证:crm-lead + crm-opportunity 相关单测全绿。 +- 决策来源:`/improve-codebase-architecture` 审查(2026-08-25,候选 #2)+ 代码审查(`/code-review`,2026-08-25)——审查发现实现已落但引用的本 ADR 尚未写下,本文补齐该悬空引用。