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.

333 lines
26 KiB

8 hours ago
# crm-project (A5 项目管理) 实现交付 Spec
> Hand-off spec,由 crm-project-wayfinding Map(issues/01–07)的决策收敛而成,并经 grilling 会话(20260821)逐条拷问补拍。
> 实现会话拿此文档即可开写。核心承重决策已全部 grill 拍板;剩余未决项集中在 §8「实现会话仍需 grill」,动手前先读。
> **性质:交付规格,非业务代码**。落地前先读对称参照 crm-opportunity / crm-lead。
## 0. 决策溯源速查
### Map 阶段(issues 01–07)
| 决策 | 来源 ticket |
|---|---|
| 上游接缝契约(4 个 Port/Cmd) | 01 upstream-seam-contract |
| 阶段-动作矩阵(原型事实) | 02 stage-model-facts |
| 双轴状态机 | 03 stage-machine-decision |
| 视图/workspace/端点清单 | 04 views-workspaces-endpoints |
| ProjectRule 消费边界 | 05 projectrule-consumption |
| 方案卡解耦(A 就地改列) | 07 schemecard-decouple |
### Grilling 会话拍板(20260821,本轮补拍,已回写各节)
| # | 决策 | 结论 | 落点 |
|---|---|---|---|
| 1 | 建模路线甲/乙 | **甲**:独立 `crm_project` 表(推翻票 07 共表定案) | §2.1 |
| 2 | 移交 Port 迁移真实爆炸半径 | 摘项目域子类、断 extends、迁 crm-project、删旧类、crm-auth 零改 | §5 |
| 3 | 入站回调可达性 | 本期 spin-out 零调用,`OpportunityConvertPort` 降级为预留接线 | §5 |
| 4.1 | 独立建项目客户前置 | 客户必填 + 目录选(`CustomerCatalogPort`)+ 不可现场新建 | §5.1 |
| 5 | 唯一可达建项目入口 | `POST /project/create`(自动转项目本期不可达) | §5.1 |
| 6 | 项目关联客户存储 | **乙**:主表单 `customer_id` 列,无 `project_customer` 子表 | §2.2/§2.3/§7 |
| 7 | 方案卡冲突检测(stage0) | **甲**:本期占位、检测留桩、拉群+多维匹配 spin-out;点明原型↔配置字段缺口 | §3/§6/§8 |
| 8 | 阶段状态机 | stage5 赢/输分叉(仅输单进 stage6);回退=弹窗多步选前序节点、下限 stage1、限管理员+销售总监;结项/已关闭/已标记赢单不可回退 | §3 |
| 9 | 看板结构 | 7 列全画(stage0~6),列头走 stageAliasJson 别名,stage0 附冲突入口+计数 badge | §4 |
| 10 | 阶段筛选枚举 | 就 7 个 project_stage 值,剔除原型混轴的「报备中」(filing_status 独立筛) | §4 |
| 11 | 阶段专属数据落点 | **乙**:方案设计/报价/招投标/开标全落主表扩展列,本期零阶段子表;清单明细行不落 CRM(外部平台文件引用) | §2.3 |
## 1. 模块定位与依赖
**crm-project(A5 项目管理)** = 商机的下游终态业务域。承接"商机转项目",管理项目从冲突处理到结项的全生命周期。
- **依赖方向**:`crm-project → crm-rule → (crm-auth / crm-dict / crm-file)`,对称 crm-opportunity。
- **与 crm-opportunity 的关系**:项目落**独立 `crm_project` 表**(grill#1 拍板路线甲,推翻票 07 共表)。商机侧 `opp_status=5`(已转项目) 仅作**终态标记**,不再兼作项目载体;`opp_status=5` 切域的旧移交代码需迁移,见 §5。
- **纯读消费 crm-rule**:ProjectRule(阶段别名/标底认定等,§6)。
- **跨域读客户**:走 crm-customer `CustomerCatalogPort`(对齐 opportunity 关联客户)。
- **文件本体**:复用 crm-file,项目附件子表仅存 biz 定位。
## 2. 数据模型(实体/表)
### 2.1 建模路线抉择(实现会话首个必读决策)
01 发现既有代码把"项目"塞在 `crm_opportunity` 表用 `opp_status=5` 切域(`AbstractOpportunityUserTransferPort` / `ProjectUserTransferPortImpl` 已依赖此假设)。但 03/04 定的项目双轴、7 阶段、独立子表(project_follow/project_attachment)都指向**独立 crm_project 表**更干净。
**决策:选路线甲(grill 拍板,本 spec 定案)。**
- **路线甲(独立表,已选)**:新建 `crm_project` 主表 + 项目专属子表;项目双轴(`project_stage`/`project_status`)住 `crm_project`;商机侧 `opp_status=5 已转项目` **降级为纯标记**(商机终态),不再兼作项目载体。
- **路线乙(共表,已否)**:沿用 `crm_opportunity` + `opp_status=5` 加列。否决理由:污染商机聚合根(商机词汇表明确「项目是下游实体」),一张表扛两套正交轴语义(商机双轴 ≠ 项目双轴),冲突不可调和。
> ⚠️ **本决策推翻票 07 已上线的共表模型**(`AbstractOpportunityUserTransferPort` 类 Javadoc「票 07 定案」)。这不是绿地选择,是对 `main` 已有代码的反转。迁移代价见 §5「移交 Port 迁移」,是一等工作项,非「改个查询」。
### 2.2 主表 `crm_project`
| 列 | 类型 | 说明 | 来源 |
|---|---|---|---|
| id | bigint PK | | |
| project_name | varchar | 项目名称 | 02 |
| project_stage | tinyint | 业务阶段 0~6(§3 轴1) | 03 |
| project_status | tinyint | 横切状态 1/2/3(§3 轴2;赢单本期不落 3) | 03 |
| filing_status | tinyint | 报备 0未/1审核中/2通过/3驳回 | 03 |
| bid_result | tinyint | 赢单/输单/未定(stage5 标记;赢单锁死不可回退、输单进 stage6) | 03 |
| source_opp_id | bigint NULL | 来源商机(独立新建可空) | 03/07 |
| scheme_card_id | bigint NOT NULL | 关联方案卡(**必填**,07/D1) | 07 |
| customer_id | bigint NOT NULL | 关联客户(必填,目录选;项目关联客户走乙=主表单列,PROJECT 方案卡校验源) | 4.1/§7 |
| owner_user_id | bigint | 当前负责人 | 01 |
| owner_name_snapshot | varchar | 负责人名快照 | 01 |
| owner_dept_id | bigint | 负责人部门快照(DataScope 锚点) | 01 |
| project_amount | decimal | 项目金额 | 02 |
| region_code | varchar | 地区 | 02 |
| brand | varchar | 品牌 | 02 |
| bid_time | datetime NULL | 中标时间 | 02 |
| created_time / version | | 审计+乐观锁(BaseEntity) | |
命名检查(AGENTS.md):`project_stage/project_status/filing_status/bid_result/source_opp_id/scheme_card_id` 均完整词组,无孤立单大写字母,无需 @Column 钉列名。
### 2.3 子表(04 选 B 自建)
- `project_follow`(跟进记录)
- `project_attachment`(附件,biz_type+biz_id 定位,本体走 crm-file)
- 项目动态 timeline = **crm-log 投影,非独立表**(04)
- 项目关联客户:**无 `project_customer` 子表**(grill 拍板走乙)——一项目一客户,落主表 `customer_id` 列(§2.2);`/project/customer/list` 退化为查这一个 + `CustomerCatalogPort` 取详情。未来真有「一项目多客户」需求(如 spin-out 自动转项目带多客户卡)再升子表。
- **阶段专属数据落点(grill#11 拍板,走乙=零阶段子表)**:
- **方案设计 / 商务报价 / 开标 → 主表扩展列**(标量):概算金额 `scheme_budget`、方案版本 `scheme_version`、设计任务状态;报价总价 `quote_total`、报价状态;开标 `bid_result` + 赢/输快照字段。均为场景平台同步的标量结果 + 任务状态,无一对多。
- **招投标准备 → 也进主表扩展列**(投标截止/预算/品牌控制/标底类型/分配比例/投标保证金/投标文件引用/投标主体):**本期投标主体一对一**(产品定案),无需子表。将来真有「一项目多投标主体」再升 `project_bidding` 子表。
- **❗硬边界**:**设备清单 / 报价清单的明细行,CRM 一律不落表**——只存外部场景平台(广联达/斯维尔等)的文件引用(走 crm-file biz 定位)+ 同步下来的汇总标量。实现会话勿建「清单明细子表」。
## 3. 状态机(03 定稿)
**双轴**,对称 opportunity(status+stage)但项目单轴线性、非模板驱动(骨架固定)。
### 轴1 `project_stage`(业务阶段,0~6;**非线性,stage5 赢/输分叉**)
```
0 冲突处理 ← 所有项目起点;冲突解除后→1;含拉群(spin-out,§6);读 conflictPullJson
1 方案设计 设计师认领/方案版本/设备清单/概算
2 商务报价 报价专员认领/报价清单/报价总价
3 招投标准备 招标状态/招标文件/品牌控制/控标策略/投标公司
4 商务跟进 跟进记录/合同与订单
5 赢单/输单 开标信息 + bid_result 标记赢/输(**分叉点**)
6 结项 **仅输单项目进**(输单复盘/竞品/输单原因);赢单不进此节点
```
> **stage5 赢/输分叉(产品定案,原型 a5-1-1-6 L203-206)**:
> - **标记输单** → 进 stage6 结项(异常流程终止)。
> - **标记赢单** → **不进 stage6**,在 stage5 闭环;后续合同/订单/发货/验收/回款正常流程 **本期 spin-out**,做完才进结项。
> → spec 不再把 5→6 当线性下一步。
### 轴2 `project_status`(横切状态)
```
1 进行中 默认活跃态
2 已关闭 任意 stage 可主动关闭(记关闭类型/原因/时间/备注);**不允许阶段回退**
3 已结项 终态:输单进结项落此(赢单本期在 stage5 闭环、不落 3);**不允许阶段回退**
```
> 注:赢单本期 **不** 进已结项(3)——赢单后的正常履约流程 spin-out,本期赢单落 stage5 终点、status 仍 1。只有输单走到 stage6 才 status=3。
### 报备 `filing_status`(独立字段)
```
0 未报备 / 1 报备审核中 / 2 报备通过 / 3 报备驳回
```
报备是外部流程结果,CRM 记字段 + 写项目动态(crm-log)日志条目。
### 关键迁移图(产品定案 20260821,回退口径已钔)
**推进(+1,含 stage3 唯一连跳)**:
```
建项目 → stage=0, status=1, filing_status=0
冲突解除(报备专员) → 0→1
方案确认通过 → 1→2
报价确认 → 2→3
投标完成 → 3→4
【无需招投标】→ 3→5(**唯一连跳**,原型 a5-1-1-4)
跟进推进 → 4→5
【标记输单】→ 5→6(进结项)
【标记赢单】→ stage5 闭环(不进 6,status 仍 1,后续流程 spin-out)
```
**回退(产品定案,弹窗选前面任意节点、非 -1)**:
```
口径:回退弹窗 → 可选**前面任意已经过的节点**(多步回退,不是相邻 -1);只往回不往前。
权限:仅**管理员 + 销售总监**可见可操作(原型两处钔死,普通销售不可点)。
下限:**stage1**(不退回 stage0 冲突处理;冲突页那个回退按钮置灰)。
```
**不允许回退的三种终态(产品钔死)**:
```
① 结项(stage6,status=3)——已归档只读
② 已关闭(status=2)
③ stage5 已标记赢单——赢单锁死(原型「状态锁定,改需管理员权限」)
```
其余:`任意 stage → status=2 已关闭`;`报备审批结果 → filing_status 流转,通过触发相应 stage 推进`。
> **回退与 bid_result 的一致性(实现会话落地)**:stage5 **尚未标记**(bid_result 未定)时可正常回退,回退到 ≤stage4 时 bid_result 保持 null;一旦标记赢单即锁死(不可回退);标记输单已进 stage6 亦不可回退。
## 4. 端点清单(04 定稿,遵 ADR-0017/0024)
**规则**:URL 扁平动作+查询参数传 id/scope;禁 @PathVariable/@PutMapping/@DeleteMapping;写端点 POST、读端点 GET;每端点挂深 tag 一级=`A5 项目管理`(权威树 lanhu-tree-v29.md)。
### 主查询/详情
```
GET /project/list 列表+分屏共用;scope=manage|mine + 分页 + 筛选
GET /project/board 看板聚合;scope=manage|mine;**固定 7 列 stage 0~6 全画** + 每列计数;额外回「冲突待处理」独立计数 badge(=stage0 项目数)
GET /project/detail 详情主档+头部;id
```
- **筛选口径(grill 拍板)**:**项目阶段筛选 = 只筛 `project_stage`,枚举就 7 个**(冲突处理/方案设计/商务报价/招投标准备/商务跟进/赢单输单/结项,走 stageAliasJson 别名)。**原型 L191 的「报备中」是混轴笔误,不照做**——`filing_status` 不是阶段;若需筛报备状态,为独立筛选项,不塔进阶段轴。
- **project 管理**(scope=manage):@DataScope 部门权限
- **我的项目**(scope=mine):owner_user_id=当前用户
- 看板列头别名:stageAliasJson 优先、空回退 stage 字典名(§6);**列头取项目设置别名**(原型 a5-1-1 看板 L221 钔死)。
- **7 列全画(grill 拍板)**:stage0 列除展示外,卡片带「去处理冲突/冲突解除」操作入口(本期检测留桩、解除动作可用→进 stage1);顶部统计卡 7 阶段计数 + 单独「冲突待处理」入口(对齐原型 L172 把冲突当操作入口)。
- 赢单不进 stage6,停 stage5 列;stage6「结项」列**仅输单项目**(§3 分叉)。
### 详情 Tab(每 Tab 独立小端点,对齐 opportunity sub.<tab>)
通用(项目自建子表):
```
GET /project/follow/list 跟进记录
GET /project/attachment/list 附件
GET /project/customer/list 关联客户(跨域 CustomerCatalogPort)
GET /project/timeline/list 项目动态(crm-log 投影)
```
阶段专属:
```
GET /project/scheme 方案设计
GET /project/quote 商务报价
GET /project/bidding 招投标文件
GET /project/bid-open 开标信息(含 bid_result 标记赢/输)
```
写端点(POST):
```
POST /project/create 独立建项目(本期唯一可达建项目入口);客户必填(目录选)+方案卡必填,详§5.1
```
其余写端点(实现会话按动作补全):推进阶段/关闭/标记赢输/报备结果回填/冲突解除等。
### bruno-sync
`bruno-sync.config.json` 加 crm-project sourceRoot + menuBindings(项目管理/我的项目 viewType+scopeKey)。
## 5. 接缝契约(01 定稿)
| 方向 | 契约 | 对手方 | crm-project 角色 |
|---|---|---|---|
| 入站回调(**本期 spin-out 不可达、零调用**) | `OpportunityConvertPort.convert(ConvertToProjectCmd)` | crm-opportunity | crm-project 只是**调用方**;契约产权归 crm-opportunity(声明+实现)。链路:**商机推方案卡=触发** → 系统自动建项目 → 项目回调本口落 E8(2→5)。但推送本期 spin-out(CONTEXT.md L90),故自动建项目不发生、本回调**本期 A5 不会调用**。预留接线。 |
| 出站被调 | `ProjectOwnerSyncPort.syncOwnerByCustomer(customerId, newOwnerId)→int` | crm-customer | **实现者**:替换现 `NoopProjectOwnerSyncPortImpl`(返 0),查 customer_id 命中项目批量刷 owner |
| 共表移交 | `ProjectUserTransferPortImpl extends AbstractOpportunityUserTransferPort` | 用户移交 | 路线甲下需改为查 crm_project;路线乙沿用 opp_status=5 切域(现状) |
| 读配置 | `ProjectRuleQueryPort`(§6) | crm-rule | **消费方** |
**移交 Port 迁移(选甲后的真实工作项,非「改个查询」)**:
现状(已核代码):`AbstractOpportunityUserTransferPort` 是**共表**假设的化身,绑死 `OpportunityMapper`,按 `opp_status` 切域;它有**两个**子类:
- `OpportunityUserTransferPortImpl`(商机域,`statusFilter()=null` 即 `opp_status≠5`
- `ProjectUserTransferPortImpl`(项目域,`statusFilter()=5`)
crm-auth 的 `UserTransferService``domain()` 字符串分发到不同 `UserTransferPort` Bean(多态注册表)。
选甲后必做:
1. **`AbstractOpportunityUserTransferPort` 不废**——商机域子类 `OpportunityUserTransferPortImpl` 仍靠它走 `opp_status≠5` 刷商机移交,继续留 crm-opportunity。
2. **只摘出项目域子类**:`ProjectUserTransferPortImpl` 断开 `extends`(父类绑死 `OpportunityMapper`),重写为 crm-project 模块内一个独立的 `UserTransferPort` 实现:查 `crm_project`、刷 owner 三列(owner_user_id / owner_name_snapshot / owner_dept_id),`domain()="project"` 不变。
3. **删除 crm-opportunity 侧的 `ProjectUserTransferPortImpl`**(类迁到 crm-project,不倒依赖)。
4. **crm-auth 零改动**:Spring 注入所有 `UserTransferPort` Bean,项目域 Bean 从 crm-opportunity 消失、在 crm-project 出现,注册表自动重挂。
**契约注意**:
- **轻重修正(grill 拍板)**:本期 A5 **唯一可达**的建项目入口 = §4 的**独立建项目端点**(跳过商机、A5 侧直接建、**不经过** `OpportunityConvertPort`)。入站回调契约本期零调用,仅预留。
- `ConvertToProjectCmd``schemeCardId 选填` 只服务那条(不可达的)回调链;与 §7/D1「项目关联方案卡必填」**不矛盾**——后者是独立建项目端点的约束,两条入口语义不同,不要混为一谈。
- 商机侧 inbound port 名 `OpportunityConvertPort`(CONTEXT.md L90 已定),**产权归 crm-opportunity(声明+实现)**,A5 仅调用方。
### 5.1 独立建项目端点(本期唯一可达入口,grill 拍板升为一等)
本期因推送 spin-out,自动转项目不可达;唯一真能建出项目的是 A5 自己的 `POST /project/create`(跳过商机)。前置(**4.1 grill 拍板**):
- **客户必填**:从 crm-customer 目录选(走 `CustomerCatalogPort`),**不支持现场新建客户**(现场新建会越界到 crm-customer 建档职责;YAGNI)。
- **方案卡必填**(§7/D1):方案卡「一客户一卡、无孤立方案卡」铁律要求先有客户,故客户是方案卡的前置。
- 载入后:stage=0, status=1, filing_status=0(§3 初始迁移)。
## 6. ProjectRule 消费(05 定稿)
**读取方式**:crm-rule 暴露 inbound port `ProjectRuleQueryPort`(返结构化 DTO),crm-project 依赖注入。纯读,写方=crm-rule 单例行(id=1)。
**Port 契约草案**:
```java
interface ProjectRuleQueryPort {
List<StageAliasDTO> loadStageAlias(); // ① 本期(看板/详情阶段名)
BidConfigDTO loadBidConfig(String bidType); // ⑤ 等级判定本期;分成字段留 DTO 不消费
// 以下 spin-out,签名预留、实现留桩:
List<PoolMemberDTO> loadPoolMembers(); // ② 拉群人员
ConflictPullDTO loadConflictPull(); // ③ 冲突拉群
List<StageRemindDTO> loadStageRemind(); // ④ 阶段提醒
}
```
**五区块消费边界**:
| 区块 | 本期 | 说明 |
|---|---|---|
| ①阶段别名 stageAliasJson | ✅做 | 看板列头别名优先、空回退字典名 |
| ⑤标底认定 bidConfigJson | 🤔部分 | **等级判定(A+~SQ)做**;分成%计算 spin-out(依赖薪酬/绩效) |
| ②拉群 poolMemberJson | ⚠️spin-out | 依赖企微群 |
| ③冲突拉群 conflictPullJson | ⚠️spin-out | 冲突检测 **选甲:stage=0 占位、检测留桩返回无冲突、拉群 spin-out**(§8)。⚠️已建 `conflictPullJson` **只有 5 行拉群+2 触发开关,无「匹配维度选择」字段**(原型 A7-3-4 有、票 10 落表砂掉);spin-out 前需先补此配置 |
| ④阶段提醒 stageRemindJson | ⚠️spin-out | 依赖通知推送 |
ProjectRule 五区块字段结构见 `crm-rule/.../entity/ProjectRule.java` + `ProjectRuleConstants`(7角色/5冲突行/2触发条件/9标底类型 A+~SQ)。
## 7. 方案卡解耦改造(07/D2,跨模块——动 crm-opportunity)
**决策**:项目关联方案卡必填(D1);解耦选 **A 就地改列**(不搬模块、不改表名 `opportunity_scheme_card`)。
**改造清单**:
1. **归属列**:`oppId`(not null) → `ownerType`(tinyint:1=OPPORTUNITY/2=PROJECT) + `ownerId`(bigint not null)。
- 迁移:存量行 `ownerType=OPPORTUNITY, ownerId=旧oppId`
- `ownerType`→`owner_type`✅、`ownerId`→`owner_id`✅ 命名安全。
2. **唯一约束**:`UNIQUE(opp_id,customer_id,delete_key)` → `UNIQUE(owner_type,owner_id,customer_id,delete_key)`
3. **2 个类改动**(真实爆炸半径,非 26 处):
- `OpportunitySchemeCardServiceImpl`:oppId→ownerType+ownerId;客户校验分叉。
- **OPPORTUNITY**:customer_id 必是当前商机下关联客户(查 `opportunity_customer`,现状不变)。
- **PROJECT(grill 拍板,项目客户走乙)**:customer_id 校验 = **等于项目主表 `crm_project.customer_id`**(一项目一客户一卡,无 `project_customer` 子表)。不查全局目录充当关联校验(那会丢「关联客户」语义)。
- `OpportunityViewQueryImpl`:商机列表 join 方案卡加 `ownerType=OPPORTUNITY` 过滤。
4. **未选 B 理由**:A/B 数据改动相同,B 仅多搬模块/改表名纯语义成本、爆炸半径翻倍、动核心聚合风险高;仅 2 owner 属 YAGNI。
## 8. 遗留 / spin-out / CONTEXT 改写待办
### 已 grill 拍板(本轮,已回写各节)
- **§2.1 建模路线 = 甲**(独立 `crm_project` 表)。推翻票 07 共表定案,移交 Port 迁移见 §5。
- **独立建项目客户前置 = 客户必填 + 目录选(`CustomerCatalogPort`)+ 不可现场新建**(§4.1 / §5.1)。
- **项目关联客户存储 = 乙(主表单 `customer_id` 列,无 `project_customer` 子表)**;`/project/customer/list` 退化为查这一个客户 + `CustomerCatalogPort` 取详情(§2.3 / §4 / §7)。
- **本期唯一可达建项目入口 = `POST /project/create`**(自动转项目回调本期 spin-out 不可达,§5/§5.1)。
### 实现会话仍需先 grill 的岔口
- **方案卡冲突检测(stage=0)= 选甲:本期只做占位、检测留桩、拉群 spin-out**(grill 拍板)。依据与约束:
- **冲突定性 = 配置驱动的多维模糊匹配**(非「同客户等值」、非「五维写死」):原型 A7-3-4 原文「匹配维度命中 ≥2 项视为冲突,**可选择维度**包括:项目名称/地点/品牌/联系人/时间戳」+「≥20000方参与同一项目」+「异地报备(销售区域≠项目所在地)」。
- **❗原型↔已建表实现缺口**:已建 `project_rule.conflictPullJson` 结构 = `{rows:[{rowKey,pullRule,required}], autoTriggerMultiParty, autoTriggerRemote}`——**只有拉群 5 行 + 2 触发开关,没有「匹配维度选择」这一层配置字段**(票 10 落表时砂掉了多维匹配)。故本期**连「读哪些维度」的配置源都不存在**。
- **`ProjectRuleConstants` 的 `TRIGGER_MULTI_PARTY`/`TRIGGER_REMOTE_FILING` 是拉群触发开关,不是判定维度**;方案卡唯一约束带 `opp_id`,跨商机同客户多卡合法,故「同客户多卡=冲突」推断错误。
- **本期无「多方报备同一项目」数据源**:唯一入口是 A5 独立建项目(跳过商机、无报备流程),检测无有意义输入。数据源 + 配置源两头都缺 → 选甲是**唯一可行**,非仅优选。
- **选甲落地**:所有项目仍必经 stage=0(产品要求骨架保留);建项目时检测逻辑**留桩返回「无冲突」**;用户手动点「冲突解除」进 stage=1;五维匹配 + 拉群整体 spin-out。
- **spin-out 前置(后补时必先做)**:给 `conflictPullJson`(或新列)补「匹配维度选择 + 命中阈值」配置;`ProjectRuleQueryPort.loadConflictPull()` 返的 `ConflictPullDTO` 应含「启用维度集 + 命中阈值 + 触发条件」,本期签名预留、实现留桩。
### spin-out(Port/契约先留,能力后补)
- 冲突/项目**拉群**(②③,依赖企微群,同群聊详情 Tab)
- 阶段**提醒**(④,依赖通知推送)
- 标底**分成计算**(⑤,依赖薪酬/绩效模块)
- 群聊详情 Tab 端点
### CONTEXT.md 改写留痕(07/D1 推翻既有口径,交付时执行)
- `crm-opportunity/CONTEXT.md` **L90**:「可关联方案卡(选填,归 A5)」→「独立建项目关联方案卡必填」。
- `crm-opportunity/CONTEXT.md` **L118**:「无孤立方案卡」→「方案卡归属 ownerType+ownerId;商机卡仍无孤立、项目卡归项目」。
- 新增 `crm-project/CONTEXT.md`(模块建档,域术语:项目/双轴/阶段/报备/标底/方案卡归属),并更新根 `CONTEXT-MAP.md` 指向。
- 建议记 ADR:方案卡 owner 化解耦 + 项目双轴模型。
### 其他遗留(本轮 grill 后重新分层,未决项)
- **ProjectRuleQueryPort 归 crm-rule inbound 包位置 + DTO 定义**(`StageAliasDTO`/`BidConfigDTO`/`ConflictPullDTO`/`StageRemindDTO`)。
- **`filing_status` 报备状态的写入来源**:外部报备流程结果如何回填(回调?定时同步?)——§3 标「外部流程结果」,来源通道未定。
> 本轮已消化、从遗留清单移除的过时项(留痕):
> - ~~阶段回退口径(参照 opportunity)~~ → 已定案:弹窗多步回退、下限 stage1、三终态锁死(§3,grill#8)。
> - ~~冲突检测触发条件(≥2方/异地)判定~~ → 已定性:配置驱动多维匹配、本期留桩、spin-out 前置补维度配置(§8 上文,grill#7)。
## 9. 落地顺序建议
1. **承重决策已全部 grill 拍板(见 §0 会话表)**;动手前只需先读 §8「实现会话仍需 grill」剩余未决项(报备回填通道 / Port 包位置)。
2. 读对称参照:crm-opportunity(8 controller/双轴/四视图)、crm-lead 的 Controller 形态,不另起风格(AGENTS.md 新模块 checklist)。
3. §7 方案卡解耦改造(跨模块前置,2 个类 + 表迁移)——它是项目建档的地基。
4. crm-project 主表 + 子表(§2)+ 状态机枚举(§3)。
5. 接缝实现(§5):`ProjectOwnerSyncPort` 真实现替换 Noop、`ConvertToProjectCmd` 回调、用户移交 Port 按路线调整。
6. 端点(§4)+ bruno-sync 配置 + 深 tag。
7. ProjectRule 消费 Port(§6):①⑤本期,②③④桩。
8. CONTEXT.md/ADR 留痕(§8)。
9. 全量 BOM 扫描 + `mvn compile`(AGENTS.md 编码纪律)。
---
_本 spec 收敛自 crm-project-wayfinding Map(01–07),并经 grilling 会话(20260821)逐条拷问补拍(10 项承重决策见 §0 会话表)。spin-out 项契约已预留。实现会话据 §9 顺序推进,§8 剩余未决项先 grill。_