# spec-flows:客户模块 E2E 执行全集(票 01 交付) > **依据优先级**:v29 原型(`.scratch/lanhu-latest/v29-customer-pages/a4-*.txt`,20260903 重拉 41 页)> spec 决策 D1–D29(`.scratch/customer-module/map.md`)> 票 12 十页静态对齐结论(HANDOFF §3.7.2)> Controller 源码实核(9 Controller 55 端点逐一核对,20260903)。 > **接口纪律**:全部 flat 实路径引自 Controller 源码,禁止按 RESTful 推断。分页 = `current`/`size`(BaseParam);写端点除标注外均 **form 绑定**(无 @RequestBody);`Result` data=null 非失败。 > **两簇结构**:核心域簇(票 04)+ 重流程簇(票 05)。每条流程:原型页码 / 操作序列 / 接口 / 前置 / 断言要点。 > **判定口径**:✅ 通过 / ⚠ 行为与原型不符 / ❌ 缺失或阻断;缺陷 D-xx 递增;已知缺口(API-SUMMARY §5)预登记不算新缺陷。 --- ## 0. 端点总账(源码实核,55 = 10+3+9+8+7+5+6+6+1) | Controller | 前缀 | 数 | 端点 | |---|---|---|---| | CustomerController | `/api/customer` | 10 | POST `` / PUT `/{id}` / GET `/{id}` / POST `/page` / POST `/search` / POST `/quick-create` / GET `/{customerId}/contacts` / GET `/check-name` / GET `/check-credit-code` / GET `/company-lookup` | | CustomerWorkspaceController | `/api/customer/workspace` | 3 | POST `/{workspace}/page` / POST `/{workspace}/board/summary` / POST `/{workspace}/board/cards`(workspace ∈ mine/overview/pool) | | CustomerOwnershipController | `/api/customer` | 9 | POST `/claim` `/claim-batch` `/assign` `/assign-batch` `/release-pool` `/release-pool-batch` `/archive` `/archive-batch` `/restore`(id/ids/userId 走 @RequestParam) | | CustomerDetailController | `/api/customer` | 8 | GET `/{id}/detail-head` / GET `/{id}/members` / POST `/{id}/members` / DELETE `/{id}/members/{memberUserId}` / GET `/{id}/opportunities` / GET `/{id}/oplog/page` / POST `/{id}/follow` / GET `/{id}/follow/page` | | CustomerContactController | `/api/customer/contact` | 7 | POST `` / PUT `/{id}` / DELETE `/{id}` / GET `/{id}` / GET `/page` / GET `/search` / GET `/check-phone` | | CustomerFocusController | `/api/customer` | 5 | POST `/{id}/focus` / `/{id}/unfocus` / `/{id}/star` / `/{id}/unstar` / `/focus-batch` | | CustomerTransferController | `/api/customer/transfer` | 6 | GET `/preview` / POST `/initiate` / GET `/{id}` / GET `/page` / POST `/{id}/assign` / GET `/{id}/assignable` | | CustomerImportController | `/api/customer/import` | 6 | POST `/upload` / POST `/{taskId}/confirm` / GET `/{taskId}` / GET `/{taskId}/failures` / GET `/page` / GET `/template`(模板=信封例外二进制流 RFC 5987) | | CustomerPingController | `/api/customer` | 1 | GET `/ping` | 跨模块(分组归属票 06 定):crm-rule `GET/PUT /api/rule/customer/reminder`(**PUT 唯一 @RequestBody JSON 端点**);crm-preference `GET /api/preference/view/list?scopeKey=`、`POST /api/preference/view/save`、`POST /api/preference/view/delete`、`POST /api/preference/view/set-default`、`GET /api/preference/get`、`POST /api/preference/save`(scope_key ∈ `customer.overview`/`customer.mine`/`customer.pool`)。 **底稿笔误订正**:API-SUMMARY §2.1 看板分组维度写 `stage/customerStar/relationStar`——源码白名单实为 **stage/star/relation**(`CustomerWorkspacePageParam.groupColumn` javadoc + `CustomerWorkspaceQuery` 双处实锤;越界静默忽略)。票 06 生成文档按源码值域。 ## 0.1 关键值域(实测断言用) - `viewType`:ASSIGNED / COLLABORATING / FOLLOW_UP_DUE / FOCUSED / RECENT(空=基础集;RECENT 不接,票 04 只验证不炸) - `groupColumn`:stage / star / relation;`groupValue` Integer 配对 - `archive_status`:1 有效 / 2 已归档;`customer_stage`:1 潜在 / 2 重潜 / 3 已成交(三态只前进,CAS) - `oplog.action` 11 种:CREATE/UPDATE/FOLLOW/ARCHIVE/RESTORE/TRANSFER/CLAIM/POOL/STAGE_CHANGE/MEMBER_ADD/MEMBER_REMOVE - 导入 `importMode`:APPEND_ONLY / UPDATE_ONLY / UPSERT;任务状态 0 DRAFT→1 RUNNING→2 DONE/3 FAILED - 交割 `reason`:resign / transfer_post / region_adjust(67011 必填) - 错误码:67001–67016(CustomerConstants)+ 64023(提醒规则校验,crm-rule)+ 66001(商机侧) - viewType/savedViewId 互斥语义:savedViewId 非空时叠加检索条件、不改 @DataScope 数据范围 --- # 第一簇:核心域(票 04 消费) ## F-01 三 workspace 列表(列表/卡片/分屏三视图) - **原型**:A4-1-1 客户公海(列表视图)/ A4-2-1 客户总览(列表/卡片/分屏)/ A4-3-1 我的客户(列表/卡片/分屏) - **操作序列**:选 tab(我的客户/客户总览/客户公海)→ 检索(类型/行业/关键字)→ 排序(公海按进入公海时间升/降,grill Q19)→ 翻页 → 切卡片/看板视图 - **接口**:`POST /api/customer/workspace/{pool|overview|mine}/page`(CustomerWorkspacePageParam:viewType/customerType/industryCode/customerStarLevel/relationStarLevel/customerStage/archiveStatus/provinceCode + current/size) - **前置**:seed 三态客户就位(有主跨部门/公海/已归档) - **断言**:分页 `{content,total}`;pool 行 owner 为空且 `enterPoolTime` 有值可排序;overview 走 DataScope(管理员可见全部);mine=负责人或协同人;列字段齐(客户类型/行业/星级/协议等级/VIP 等级/关联业务计数 opportunityCount 真算 projectCount 恒 0 显 --,不符①修复项);卡片视图同数据源 - **原型细节**:公海列 = 序号/客户名称/类型/行业/阶段/客户星级/关系星级/战略协议等级/已成交VIP等级/进入公海时间/操作;行操作=详情·关注|取消关注·领取|分配(领导视角分配、普通销售领取,双视角按钮差异) ## F-02 看板三分组 - **原型**:A4-2-1 / A4-3-1 卡片视图 + 看板分组(潜在/重潜/已成交 三列注记) - **接口**:`POST /api/customer/workspace/{w}/board/summary` → `[{groupValue,count}]`;`POST .../board/cards`(groupColumn+groupValue+分页) - **断言**:stage/star/relation 三种 groupColumn 均可;仅返回有数据分组(空列前端补骨架);summary 总数与 page total 自洽;同 WHERE 口径(summary+cards+page 数字一致) ## F-03 saved-view 自定义检索 CRUD 与应用 - **原型**:A4-1-1 / A4-3-1 左栏(常用检索/自定义检索/最近访问/我关注的客户 + 编辑检索/重命名/删除 + 检索条件且逻辑 + 排序条件升/降 + 保存) - **接口**:`GET /api/preference/view/list?scopeKey=customer.mine` → `POST /api/preference/view/save` → `POST /api/preference/view/set-default` → `POST /api/preference/view/delete`;应用= workspace/page 带 `savedViewId` - **断言**:三 scope_key 各建一视图互不影响;savedViewId 生效后列表过滤叠加且 DataScope 不变;删除后应用报视图不存在不炸;RECENT 视图不接(传 FOLLOW_UP_DUE 验证内置视图即可) - **注**:内置视图 = 我负责的(ASSIGNED)/我协同的(COLLABORATING)/待跟进的(FOLLOW_UP_DUE)/我关注的(FOCUSED) ## F-04 新增客户 + 三层查重 - **原型**:A4-3-3-1 新增客户信息(基本信息/联系人子表/关联信息工商组/跟进人员/协同人/人脉关系单位/备注/附件) - **操作序列**:+新增客户 → 输入名称(L1 停输防抖相似提示 GET check-name)→ 输入信用代码(L1 失焦 GET check-credit-code)→ 提交: - 名称相似 → 弹「系统自动查询CRM相似客户」卡(命中行含编号/地区/类型/负责人)→「返回修改」或「仍要创建」(confirmSimilar=true 重发) - 信用代码已存在 → 硬拦弹窗(仅返回修改,无仍要创建) - **接口**:`GET /api/customer/check-name?name=&excludeId=` / `GET /api/customer/check-credit-code?creditCode=` / `POST /api/customer`(CustomerCreateDTO form;返回 CustomerSaveResultDTO{needConfirm,similarHits[{customerId,customerName,ownerName,customerStage,archiveStatus,masked}]} 或落库) - **前置**:库内已有带信用代码客户 + 名称相近客户 - **断言**:相似命中 masked 语义(无权显示「存在匹配记录」);67003 硬拦;confirmSimilar=true 落库成功;ownerUserId 空 → 进公海记 enter_pool_time;ownerUserId 有值 → 我的客户;客户编号 KH+yyyyMMdd+4 位;阶段默认潜在;oplog CREATE - **范围外注**:OCR 快捷新增(a4-3-3-4)/企查查联想(company-lookup stub)/附件(file 模块不深测)/人脉关系单位——D2/D5 划出,只测 check-credit-code 在回填场景 ## F-05 编辑客户 - **原型**:A4-3-3-2 编辑客户信息 - **接口**:`GET /api/customer/{id}`(全字段回显+字典名)→ `PUT /api/customer/{id}`(CustomerUpdateDTO form,CAS version 必传) - **断言**:回显字段齐(类型/行业两级/省市区三级/星级/工商组/父子客户/总裁班组);version 不带 → 67005;归属字段不可改;改信用代码撞他人 → 67003;查重排除自身(同名不改也过得去);oplog UPDATE 字段级旧→新(D13 叙事「把 X 从 旧 修改为 新」) ## F-06 联系人 CRUD + 全局搜索 + 电话软提示 - **原型**:A4-5-1 联系人列表 / A4-5-2 新增 / A4-5-3 编辑 / A4-5-5 详情 / A4-4-2 详情联系人页签(子表:姓名/职务/电话/是否关键联系人/是否内线/礼品备注/来源) - **接口**:`POST /api/customer/contact` / `PUT /api/customer/contact/{id}` / `DELETE /api/customer/contact/{id}` / `GET /api/customer/contact/{id}` / `GET /api/customer/contact/page` / `GET /api/customer/contact/search?keyword=&limit=` / `GET /api/customer/contact/check-phone?phone=&excludeId=` - **断言**:create 所属客户必填(67008 对不可见客户);电话手机号格式校验;同客户内电话表单级校验;跨客户 check-phone 只提示不阻断(D26 软提示,受查重设置开关控制);search 三维模糊(姓名/电话)+带公司名(商机侧硬依赖);软删后不出现在列表 - **联系人导入**(a4-5-4):划出(REVIEW-HANDOFF 定稿),仅记录 ## F-07 客户详情公共头部 + 8 页签 - **原型**:A4-4-1 客户详情(客户信息/已归档/已被合并三变体)+ a4-4-2..8 - **接口**:`GET /api/customer/{id}/detail-head` - **断言**:头部 11 项(阶段只读条三态/星级/协议VIP快照/负责人/最近跟进摘要/待跟进提醒/关联业务计数/客户档案状态/归档按钮/标记重点客户/返回);**汇总卡 5 字段恒 null 前端显 "--"**(wonProjectAmount/planEstimateAmount/ongoingOpportunityAmount/contractAmount/paidAmount——不符④拍板占位);关联业务计数 opportunityCount 真、projectCount 显 --;67002 客户不存在 - **变体**:已归档客户详情可开(档案状态=已归档);已合并态 V1 无合并功能不测 ## F-08 跟进写入 + 跟进页签 - **原型**:A4-4-3 跟进记录页签(时间倒序+日期分组「6月28日 周日」+ follow_way 过滤 电话/拜访 + 搜索) - **接口**:`POST /api/customer/{id}/follow`(FollowCreateDTO form:followWay/followContent/nextFollowTime)/ `GET /api/customer/{id}/follow/page`(FollowPageParam:followWay/keyword/startTime/endTime) - **断言**:append-only(提交后无改删口);content 空 → 67009;nextFollowTime 早于本次 → 67009;写库后 `last_valid_follow_time` 刷新(D25 锚点,DB 验);detail-head 最近跟进摘要联动;oplog FOLLOW - **待跟进提醒**:nextFollowTime 未来 → 头部「X 待跟进」样例(seed 造) ## F-09 关注 / 重点标记(一行两态) - **原型**:A4-1-1 行操作(关注/取消关注)+ A4-4-1 头部「标记重点客户」 - **接口**:`POST /api/customer/{id}/focus` / `/{id}/unfocus` / `/{id}/star` / `/{id}/unstar` / `/focus-batch?ids=` - **断言**:focus 幂等(重复成功不炸);star 未关注自动建行 starred=1;unstar 保留行 starred=0;unfocus 物理删行连带清标记;FOCUSED 视图(viewType=FOCUSED)过滤生效;不写 oplog;DB 验 customer_focus 行态(每步新连接 autocommit=True,坑㉕) ## F-10 团队成员 - **原型**:A4-4-7 团队成员页签(+添加成员 → 搜索多选「已选择 N 位成员」→ 确认添加;行操作删除+二次确认;负责人无删除按钮;角色列 负责人/协同人) - **接口**:`GET /api/customer/{id}/members`(负责人 ROLE_OWNER 在前)/ `POST /api/customer/{id}/members`(MemberAddDTO form memberUserIds[])/ `DELETE /api/customer/{id}/members/{memberUserId}` - **断言**:两阶段校验整批拒绝(名单空/含负责人/重复加入/用户停用 → 67016 无半写);批内幂等去重;移除软删 + delete_key 复用键(移除后可重加);oplog MEMBER_ADD/MEMBER_REMOVE;姓名快照 ## F-11 归属动作(领取/分配/抛公海/归档/恢复) - **原型**:A4-1-1 公海行操作+批量+确认弹窗+并发领取失败 Toast;A4-3-1 更多菜单(归档);A4-4-1 头部归档 - **接口**:`POST /api/customer/claim?id=` / `claim-batch?ids=` / `assign?id=&userId=` / `assign-batch?ids=&userId=` / `release-pool?id=` / `release-pool-batch?ids=` / `archive?id=` / `archive-batch?ids=` / `restore?id=` - **断言**: - 领取:公海→当前用户;D25 锚点为空时赋当下(DB 验);CAS 并发(两用户抢领→后者业务失败,Toast「已被他人领取」);批量逐条独立事务 BatchResult{successes,failures[]} - 分配:67012(越权/本人/当前负责人);目标启用在职校验;不校验进行中商机(issues-03 §E) - 抛公海:有进行中商机 → 67007;清 owner+协同人(部门锚点保留);软删 team_member;oplog POOL - 归档:仅销售负责人;有进行中商机 → 67006;批量归档整批预检(任一失败整批不执行,原型 A4-3-1 §3.6);归档=删除语义 D19(默认列表排除,archiveStatus=2 可见);oplog ARCHIVE - 恢复:仅销售负责人;oplog RESTORE - **已知缺口注**:客户导出按钮(A4-1-1 气泡)全规格无票 → 范围外记录 ## F-12 商机侧硬依赖三端点(跨模块回归) - **原型**:商机模块 A3 侧(添加关联客户弹窗 a3-1-1-2-4 / 商机详情客户信息页签) - **接口**:`POST /api/customer/quick-create`(CustomerQuickCreateDTO form)/ `POST /api/customer/search`(CustomerSearchParam form)/ `GET /api/customer/{customerId}/contacts` - **断言**:quick-create 最小集默认 owner=当前用户/is_child=否/isBizNegotiated=否;**已知缺口:QuickCreateDTO 无 confirmSimilar**(命中相似返回 needConfirm 无法仍要创建——预登记不算新缺陷);search 三维模糊+类型精确+DataScope;contacts 按客户带出 # 第二簇:重流程(票 05 消费) ## F-13 客户交割(两段式) - **原型**:A4-7-1 发起交接(客户预览勾选「已选择2个 - 共80个」+备注+确认交接)/ A4-7-2 客户分配 / A4-7-3 交接记录 / A4-7-4 详情 - **操作序列**:进入客户交割 tab → 发起客户交接 → 预览名下客户(GET preview)→ 勾选 + 选交接原因 + 接收总监(resign 自动推导唯一接收总监 D22;transfer_post 校验范围)→ 确认交接(整批原子)→ 切客户分配 tab → 逐条/批量分配(GET {id}/assignable 选销售)→ 全部分配完自动已完成(D23)→ 交接记录可见 - **接口**:`GET /api/customer/transfer/preview` / `POST /api/customer/transfer/initiate`(TransferInitiateDTO form:reason 必填 67011)/ `GET /api/customer/transfer/{id}` / `GET /api/customer/transfer/page` / `POST /api/customer/transfer/{id}/assign?customerIds=&assignUserId=` / `GET /api/customer/transfer/{id}/assignable` - **断言**: - reason 缺失 → 67011;存在未完成交接单再发起 → 67010 防重入;无可交接客户 → 67011 - 整批原子(任一失败全回滚不生成交接单);交接编号 JG+yyyyMMdd+4 位 - **商机 owner 联动**:seed 商机关联样例验证 OpportunityOwnerSyncPort 三列真源同步;负责人不存在 → 66001 回滚口径 - 分配 67012(明细已分配/越权);逐条独立事务 BatchResult 行级部分成功;assignedCount==totalCount 自动置位 - oplog TRANSFER(operator=系统,detail 含交接编号);writeSystem - 项目 owner 同步 Noop seam 无副作用 - **原型细节注**:部分勾选=整批简化口径(issues-08 §D 定稿,非不符);交接单状态 0→1 ## F-14 客户导入 - **原型**:A4-3-2 客户导入(导入方式三选一/触发校验规则勾选/上传 Excel xls xlsx ≤20M/下载导入模板/数据重复时 覆盖导入|不导入) - **操作序列**:下载模板 → 按模板造 Excel → 选导入方式上传(DRAFT 预校验返回预计数+明细)→ 确认执行 → 轮询结果(总数/成功/失败/疑似重复)→ 失败明细查看 → 任务记录分页 - **接口**:`GET /api/customer/import/template`(二进制流,RFC 5987)/ `POST /api/customer/import/upload`(multipart file + importMode)/ `POST /api/customer/import/{taskId}/confirm` / `GET /api/customer/import/{taskId}` / `GET /api/customer/import/{taskId}/failures` / `GET /api/customer/import/page` - **断言**: - 模板下载文件名中文正确(RFC 5987)、双 sheet 纯表头 - 预检 67013(非 xls/xlsx / >20MB / 模板版本错 / >5 万行);DRAFT 落库;**MinIO 通路 admin/Itc@123456** - 三方式行为:APPEND_ONLY 只新增 / UPDATE_ONLY 只更新 / UPSERT 新增或更新;匹配顺序=客户编号→信用代码(**不按名称**);文件内多行同客户默认「重复时不导入」(D24) - 行级部分成功;失败明细 ImportFailDTO 纯展示(sheet/行号/原因);状态机 0→1→2/3,重复确认 → 67014 - Excel 用 Python(openpyxl)构造,EasyExcel 兼容 - **U-01 挂号**:原型页同时有「导入方式三选一」+「数据重复时覆盖/不导入」两组控件,实现收敛为单参数 importMode——执行时按实现实测记录映射行为,映射语义与原型若明显偏离则 ⚠/D-xx 登记(判定以实现契约为准,不视为缺陷除非行为自相矛盾) ## F-15 提醒规则族(a7-3-3-1) - **原型**:A7-3-3-1 超期未跟进提醒(总开关/首次触发时间 N 天/二次提醒间隔时间 N 天/独立开关/保存) - **接口**:`GET /api/rule/customer/reminder` / `PUT /api/rule/customer/reminder`(**唯一 JSON body 端点**) - **断言**:单例 GET 回显;PUT 全量覆盖;64023(天数>0);开关缺省=0;保存后再 GET 一致 - **Job 实测(U-02 挂号,票 05 定触发方式)**:造过期 last_valid_follow_time 样例 → 触发 Job → customer_pending_notice 落表(FOLLOW_OVERDUE,状态 0 待发);锚点=成为有主时刻(D25,成为有主时无跟进记录才从锚点起算);二次提醒间隔(k=1 到期补发);公海/已归档不参与;**Job 只落待发不直接发**(平台通知 spin-out) - **造数依据**:seed F-08 造过期样例(坑⑯:锚点时长选仅 k=0 到期档) ## F-16 查重设置消费侧(a7-3-3-2) - **原型**:A7-3-3-2 客户查重设置(客户名称 模糊匹配+相似度 70/80/90/自定义%;统一社会信用代码 精确匹配;联系电话 精确匹配;宽松/标准/严格/自定义预设;「开启后,客户名称或联系电话命中时提示疑似重复;统一社会信用代码重复校验始终生效」) - **现状**:**设置页无对外端点**(已知缺口 §5.2,单例由服务端种子化+运维改库);宽松/标准/严格预设档位定稿未纳入(范围外) - **实测路径**:DB 改 customer_dedup_rule 单例(每步新连接)→ 验证消费侧行为: - 信用代码硬拦**始终生效**(开关关也拦 → 67003) - 名称相似阈值 70/80/90 从配置读(check-name 命中边界样例) - 电话软提示可单独关闭(D26:check-phone 关后返回空) - ngram FULLTEXT 相似判定在远程 MySQL 实测(H2 无 FULLTEXT,H2 已覆盖的深度逻辑只复验关键路径) --- ## U-xx 不确定点登记(graduate 或执行时拍板) | # | 不确定点 | 处置 | |---|---|---| | U-01 | 导入页「导入方式三选一」+「数据重复时覆盖/不导入」双控件 vs 实现单参数 importMode 的映射口径 | 票 05 执行时按实现实测记录;明显自相矛盾才 ⚠/D-xx | | U-02 | 提醒 Job 手动触发方式(手动/测试端点/直调) | 票 05 执行时定(map Not-yet 预挂号;参照商机 Job 测试先例) | | U-03 | 公海「分配」按钮的角色可见性(领导视角 vs 普通销售视角) | E2E 用管理员统一视角只验端点行为;角色矩阵不实测(RBAC fail-open 收口范围) | | U-04 | 看板 groupColumn 白名单 stage/star/relation 与原型三分组列名(客户阶段/客户星级/关系星级)对应 | 已实锤(源码 javadoc 双处),无歧义,销号 | ## 静态对齐已核项(票 12 十页,实测不再重复质疑) 公海列表(不符①关联业务计数列已修)/ 总览我的客户(身份筛选+看板+视图偏好)/ 详情 8 页签(不符③④已修)/ 操作日志(不符⑤action 筛选已修,值域 11 种)/ 交割(整批原子+D22/D23+owner 联动)/ 导入(三方式+两段式+版本 67013)/ 规则两页(D21/D26+硬拦始终生效)/ 归属弹窗(67006/67007/67012+CAS+D25)/ 新增客户(全字段+三层查重+不符②成员写端点已修)。 实测关注**动态行为与静态契约的偏差**:并发、事务边界、副作用落库、错误码触达、JSON 形态。