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.
 
 
 
 
 

26 KiB

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.)

通用(项目自建子表):

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()=nullopp_status≠5
  • ProjectUserTransferPortImpl(项目域,statusFilter()=5

crm-auth 的 UserTransferServicedomain() 字符串分发到不同 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)。入站回调契约本期零调用,仅预留。
  • ConvertToProjectCmdschemeCardId 选填 只服务那条(不可达的)回调链;与 §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 契约草案

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
    • ownerTypeowner_typeownerIdowner_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 落表时砂掉了多维匹配)。故本期连「读哪些维度」的配置源都不存在
    • ProjectRuleConstantsTRIGGER_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。