21 KiB
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<Void>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;groupValueInteger 配对archive_status:1 有效 / 2 已归档;customer_stage:1 潜在 / 2 重潜 / 3 已成交(三态只前进,CAS)oplog.action11 种: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 形态。