From 73797fcd10cf5076825310d11607cf32511cdc62 Mon Sep 17 00:00:00 2001 From: luoweijian <1329394916@qq.com> Date: Tue, 25 Aug 2026 11:11:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(opportunity):=20=E7=A5=A8=2008=20=E7=8E=B0?= =?UTF-8?q?=E5=9C=BA=E5=8B=98=E5=AF=9F=E5=AD=90=E8=A1=A8=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=EF=BC=88=C2=A77.3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpportunitySiteSurvey 实体 + Mapper 骨架(对齐票 03/09 粒度): - 一商机多条保留历史、无状态机、不绑阶段(自由记录) - 必填 surveyDate/engineerUserId/applyWay/surveySeq,选填 applyNo/surveyDesc - 主要工程师引用 crm-auth SysUser(可跨部门选人,≠操作人),联系方式反查不冗余存 - 附件复用统一附件表 biz_type=SITE_SURVEY(票 09) - 无唯一约束 → BaseEntity 逻辑删,不加 delete_key 测试:OpportunitySiteSurveyMapperTest,crm-opportunity 108→111 全绿。 接线待后续:勘察读写 Service/Controller、crm-dict 申请方式/勘察次数分组 --- .../issues/08-site-survey.md | 13 ++++ crm-opportunity/CONTEXT.md | 2 +- .../domain/entity/OpportunitySiteSurvey.java | 68 +++++++++++++++++++ .../mapper/OpportunitySiteSurveyMapper.java | 9 +++ .../OpportunitySiteSurveyMapperTest.java | 56 +++++++++++++++ 5 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunitySiteSurvey.java create mode 100644 crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapper.java create mode 100644 crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapperTest.java diff --git a/.scratch/opportunity-module/issues/08-site-survey.md b/.scratch/opportunity-module/issues/08-site-survey.md index 4e708de..2dcf150 100644 --- a/.scratch/opportunity-module/issues/08-site-survey.md +++ b/.scratch/opportunity-module/issues/08-site-survey.md @@ -69,3 +69,16 @@ Blocked by: 03 - ✅ 勘察补充说明:**选填**(原型录入表单无 `*`,用户图上确认;P1-3 拍板,不加码必填)。 - ⚠ 「申请方式」选项集(走 `crm-dict` 字典,具体值待产品)。 - ⚠ 原型工程师/日期/单号均为样例,非验收值。 + +--- + +## 落地记录(20260825) + +**结构落地完成**(对齐票 03/09 粒度,§7.3): +- 实体 `OpportunitySiteSurvey`(`@Entity`+`@TableName`+继承 `BaseEntity`)+ `OpportunitySiteSurveyMapper`。 +- 必填:`surveyDate` / `engineerUserId` / `applyWay` / `surveySeq`;选填:`applyNo` / `surveyDesc`(以 grill 底稿为准,PRD §7.3 表中 apply_way/survey_seq 标「—」以底稿必填覆盖)。 +- 无状态机、不绑阶段;无唯一约束→走 BaseEntity 逻辑删 `deleted`,**不加 delete_key**(复用键仅为唯一索引服务)。 +- 附件复用统一附件表(`biz_type=SITE_SURVEY` + `biz_id`=本记录 id,票 09 已落)。 +- 测试:`OpportunitySiteSurveyMapperTest`(表绑定+列映射+逻辑删),crm-opportunity 108→111 全绿。 + +**接线待后续**:勘察读写 Service/Controller(新增/列表/详情,工程师选人反查 SysUser 脱敏展示);crm-dict 新增「申请方式/勘察次数」分组 = 字典种子子票。 diff --git a/crm-opportunity/CONTEXT.md b/crm-opportunity/CONTEXT.md index 7fedd55..4278892 100644 --- a/crm-opportunity/CONTEXT.md +++ b/crm-opportunity/CONTEXT.md @@ -174,7 +174,7 @@ _Avoid_: 浏览(不算有效跟进)、编辑主表字段(不计)、上 ## 待细化(后续票) - 「移交」按钮原型位置、「取消暂缓/重启」独立页存否 —— 见 `issues/01` 的 To-verify,以产品口径为准。 -- ~~商机主表字段~~ **票 03 已落地**(`Opportunity` 实体 + `OpportunityMapper`,三簇 + Amendment 1 + 乐观锁 version);子表(方案卡票 07 已落 / 跟进·附件·工作计划·操作日志票 09 已落 / 现场勘察票 08 / 团队成员待后续)字段归各票。 +- ~~商机主表字段~~ **票 03 已落地**(`Opportunity` 实体 + `OpportunityMapper`,三簇 + Amendment 1 + 乐观锁 version);子表(方案卡票 07 已落 / 跟进·附件·工作计划·操作日志票 09 已落 / 现场勘察票 08 已落 / 团队成员待后续)字段归各票。 - ~~状态机迁移图~~ **票 04 已落地**(`OpportunityTransition` + sealed `OpportunityTransitionCmd` 九边(E1-E9,每带 allowedFromStatuses,无 owner guard)+ `executeRecycle` 批量回收(E10);CAS 两级口径:守卫前置 STATUS_NOT_ALLOWED / 0 行 CAS_FAIL,多起始态边(E6/E9)锚合法起始态集合,E9 另锚 owner;E5 锚点顺推纯函数;`OpportunityStatusHistory` 快照表 + Recorder;`OpportunityConvertPort` 三件套 inbound 端口(幂等含并发复查终态补丁));E9 team 表同步维护待 opportunity_team 结构定稿后接线(javadoc 注明);E7 重启不刷回收锚点为规格空白(PRD §3.2 注记 20260824,待产品拍板)。 - ~~商机公海规则~~ **票 05 已落地**(`OpportunityMaintenanceJob` + 待发通知表):crm-rule 侧实体/版本状态机服务/控制器/菜单种子 + 快照式匹配引擎 `OpportunityPoolRuleMatcher`(见 crm-rule CONTEXT 商机规则子域);本模块侧 `executeRecycle` 演进为 **ids 集合语义**(选择策略归 Job——各部门 recycle_days 不同无法单 cutoff 表达,迁移机制归状态机,按 id+推进中 CAS 复查静默跳过并发变更行)、`OpportunityMaintenanceJob` 单 `@Scheduled`(cron 占位符 `crm.opportunity.maintenance-cron` 默认每日 3 点)**先提醒后回收**串行(ADR-0019 对齐)+ 回收后待发提醒置已失效;`OpportunityPendingNotice` 实体 + 常量(UNIQUE 批次键 = plan_notify_time,幂等口径见 PRD §7.10 回填)。**已知缺口(code-review 20260824 登记)**:① §7.10 失效语义仅落地「回收」来源——领取(E1)/暂缓(E4/E5)/关闭(E6/E9)路径的置失效埋点待通知消费者票统一补(消费者本期 spin-out,误置「待发」无人消费、实际影响为零);② `allow_free_claim`/`allow_manual_pool` 本期仅可配置未生效,消费点随领取/抛公海入口票(票 10/11)接线;③ `invalidatePendingReminds` 按本轮候选全集近似置失效(含 CAS 静默跳过行),已声明由消费者补发时校验商机状态兜底。四视图查询与「我的关注」表(票 10)仍待后续。 - ~~商机子表族(跟进 / 附件 / 工作计划 / 操作日志)~~ **票 09 已落地**(§7.4–7.7 四张表结构 + Mapper 骨架):`OpportunityFollow`(跟进,软删+delete_key,无草稿态,支持删除不回退锚点,本期不做编辑)/ `OpportunityAttachment`(统一附件表,`biz_type`+`biz_id` 定位,`AttachmentBizType` 枚举 OPP/FOLLOW_UP/SITE_SURVEY/SCHEME_CARD,文件本体走 crm-file,软删+delete_key)/ `OpportunityWorkPlan`(工作计划**结构 only**,plan_status 0/1,精确态枚举与联动归 A1X 模块 spin-out,软删+delete_key)/ `OpportunityOplog`(字段级审计,**双维度** `OplogKind`(FIELD_CHANGE/ROW_ADD/ROW_DELETE)+ `OplogLogType`(7 类,SYSTEM_INTERNAL 记库不显),只增不删无 delete_key,带 biz_ref + snapshotJson)。**本票只落表结构 + Mapper + 枚举 + 冒烟单测**(对齐票 03 粒度);**接线待后续**:跟进读写 Service/Controller + 刷 `last_valid_follow_time`(票 05 联动)、附件读写、统一审计切面自动写 oplog(写入点驱动)均未接。crm-dict 新增分组「跟进方式/跟进结果标签/商机资料类型」待字典种子票。 diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunitySiteSurvey.java b/crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunitySiteSurvey.java new file mode 100644 index 0000000..084bcd0 --- /dev/null +++ b/crm-opportunity/src/main/java/com/crm/opportunity/domain/entity/OpportunitySiteSurvey.java @@ -0,0 +1,68 @@ +package com.crm.opportunity.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.crm.base.domain.entity.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Comment; + +import java.time.LocalDate; + +/** + * 现场勘察子表(票 08 §7.3 / 原型 A3-1-1-1-4 列表、A3-1-1-2-10 新增弹窗) + * + *

记录商机某次到客户现场勘察的**结果单据**(现场条件/客户需求/安装环境/工程师建议 + 附件)。 + * 一个商机可多条勘察记录(多次勘察保留历史不覆盖,新增多条区分);**无状态机**(填完即完整记录, + * 异于方案卡);**不绑阶段节点**(自由记录,不联动轴 2)。

+ * + *

主要工程师从 {@code crm-auth} 组织架构选({@code engineer_user_id} 引用 SysUser,非自由文本, + * 可跨部门查询选人)——工程师是被选中的执行人,≠ 操作人(当前登录销售/管理员);列表「联系方式」 + * 由 engineer_user_id 反查 SysUser 电话脱敏展示,不冗余存。附件走统一附件表 + * {@code opportunity_attachment}({@code biz_type=SITE_SURVEY},{@code biz_id}=本记录 id,票 09)。

+ * + *

删除留痕走 BaseEntity 逻辑删({@code deleted})——无唯一约束故不需 delete_key 复用键 + * (对齐子表族约定:delete_key 仅为唯一索引复用服务)。

+ */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("opportunity_site_survey") +@Entity +@Table(name = "opportunity_site_survey", + indexes = { + @Index(name = "idx_oss_opp", columnList = "opp_id"), + @Index(name = "idx_oss_engineer", columnList = "engineer_user_id") + }) +public class OpportunitySiteSurvey extends BaseEntity { + + @Comment("所属商机ID(opportunity.id)") + @Column(columnDefinition = "bigint not null comment '所属商机ID'") + private Long oppId; + + @Comment("现场勘察日期(必填)") + @Column(columnDefinition = "date not null comment '现场勘察日期'") + private LocalDate surveyDate; + + @Comment("主要工程师用户ID(必填,引用 crm-auth SysUser,可跨部门选人;列表反查姓名/部门/脱敏电话)") + @Column(columnDefinition = "bigint not null comment '主要工程师用户ID'") + private Long engineerUserId; + + @Comment("申请方式字典 code(工单申请/线下申请…,crm-dict「申请方式」分组)") + @Column(columnDefinition = "varchar(32) not null comment '申请方式字典code'") + private String applyWay; + + @Comment("勘察次数字典 code(第一次/第二次/第三次/三次以上四档,crm-dict「勘察次数」分组,非自增,P1-2)") + @Column(columnDefinition = "varchar(32) not null comment '勘察次数字典code'") + private String surveySeq; + + @Comment("申请单号(选填,人工录入,外部勘察申请流程未对接、线下拿到后补录)") + @Column(columnDefinition = "varchar(64) comment '申请单号'") + private String applyNo; + + @Comment("勘察补充说明(选填,长文本:现场环境/客户需求/安装条件/设备数量/特殊要求/工程师建议)") + @Column(columnDefinition = "text comment '勘察补充说明'") + private String surveyDesc; +} diff --git a/crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapper.java b/crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapper.java new file mode 100644 index 0000000..050dad4 --- /dev/null +++ b/crm-opportunity/src/main/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapper.java @@ -0,0 +1,9 @@ +package com.crm.opportunity.mapper; + +import com.crm.base.mapper.CrmBaseMapper; +import com.crm.opportunity.domain.entity.OpportunitySiteSurvey; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OpportunitySiteSurveyMapper extends CrmBaseMapper { +} diff --git a/crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapperTest.java b/crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapperTest.java new file mode 100644 index 0000000..2d65ff1 --- /dev/null +++ b/crm-opportunity/src/test/java/com/crm/opportunity/mapper/OpportunitySiteSurveyMapperTest.java @@ -0,0 +1,56 @@ +package com.crm.opportunity.mapper; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.crm.base.mapper.CrmBaseMapper; +import com.crm.opportunity.domain.entity.OpportunitySiteSurvey; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 现场勘察 Mapper 骨架单测(票 08 §7.3)。 + *

验证 MyBatis-Plus 表绑定 + 驼峰列映射;无唯一约束故走 BaseEntity 逻辑删(deleted), + * 无 delete_key 复用键。

+ */ +@DisplayName("现场勘察 Mapper 骨架 OpportunitySiteSurveyMapper(票 08)") +class OpportunitySiteSurveyMapperTest { + + @BeforeAll + static void initTableInfo() { + MapperBuilderAssistant assistant = + new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, OpportunitySiteSurvey.class); + } + + @Test + @DisplayName("继承 CrmBaseMapper + @Mapper 注解(可被扫描注册)") + void mapperWellFormed() { + assertThat(CrmBaseMapper.class).isAssignableFrom(OpportunitySiteSurveyMapper.class); + assertThat(OpportunitySiteSurveyMapper.class.getAnnotation(Mapper.class)).isNotNull(); + } + + @Test + @DisplayName("绑定 opportunity_site_survey,列映射 engineer_user_id↔engineerUserId / survey_seq↔surveySeq") + void tableInfo_resolvesSiteSurvey() { + TableInfo info = TableInfoHelper.getTableInfo(OpportunitySiteSurvey.class); + assertThat(info.getTableName()).isEqualTo("opportunity_site_survey"); + assertThat(info.getFieldList()).anyMatch(f -> + "engineer_user_id".equals(f.getColumn()) && "engineerUserId".equals(f.getProperty())); + assertThat(info.getFieldList()).anyMatch(f -> + "survey_seq".equals(f.getColumn()) && "surveySeq".equals(f.getProperty())); + } + + @Test + @DisplayName("逻辑删:BaseEntity deleted 生效,无 delete_key(无唯一约束不需复用键)") + void logicDelete_noDeleteKey() { + TableInfo info = TableInfoHelper.getTableInfo(OpportunitySiteSurvey.class); + assertThat(info.getLogicDeleteFieldInfo()).isNotNull(); + assertThat(info.getFieldList()).noneMatch(f -> "delete_key".equals(f.getColumn())); + } +}