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.
141 lines
12 KiB
141 lines
12 KiB
|
2 weeks ago
|
<!-- wayfinder:grilling -->
|
||
|
|
# LeadStatsDTO 字段形态与前端返回契约
|
||
|
|
|
||
|
|
Type: grilling
|
||
|
|
Status: resolved
|
||
|
|
Blocked by: 02
|
||
|
|
|
||
|
|
## 事实基础(session 03 核实)
|
||
|
|
|
||
|
|
- 现状 `LeadStatsDTO` 9 字段:`total / waiting / claimed / converted / todayNew / undistributed / feedbackValid / feedbackInvalid / feedbackNone`。
|
||
|
|
- **`claimed` 字段名与语义已错位**:`LeadViewQueryImpl` line 104-106 把 status ∈ {3,4} 都累加进 `claimed`,实际语义 = 「销售持有」,不是「已领取(3)」。DTO javadoc 里管理视图卡片列表也是按 3+4 语义在用 `claimed`。
|
||
|
|
- 全仓 grep:`LeadStatsDTO` 及其字段**仅在 crm-lead 内部**(controller / service / query / test)引用,无前端仓库、无跨模块 Java 调用方。改名 / 改语义爆炸半径 = 本模块内 + 前端接入方。
|
||
|
|
- `void` 是 Java 保留字,作废字段不能叫 `void`。`LeadConstants` 已用 `STATUS_VOID = 7`。
|
||
|
|
|
||
|
|
## Question
|
||
|
|
|
||
|
|
原问题:
|
||
|
|
|
||
|
|
口径表定稿后,定 `LeadStatsDTO` 的最终字段与接口返回契约:
|
||
|
|
|
||
|
|
1. **新增字段**:现有 total/waiting/claimed/converted/todayNew/undistributed/feedbackValid/feedbackInvalid/feedbackNone。新需求要拆出:
|
||
|
|
- `following`(跟进中 status=4)
|
||
|
|
- `voidCount`(线索作废 status=7,`void` 是 Java 关键字,需换名)
|
||
|
|
- `expired`(过期失效 status=6)
|
||
|
|
- `claimed` 保留为「已领取 status=3」还是继续 =3+4?管理视图的「销售持有」= 3+4。需要一个字段名对应「销售持有」(如 `held` 或复用 `claimed`)+ 一个「已领取(仅3)」。命名要不歧义。
|
||
|
|
2. **feedback 三字段**:新需求四视图卡片清单里已不含反馈相关卡片,这三字段是否废弃 / 保留兼容?→ 可能 graduate 出一个「删字段是否破坏其他调用方」的调查。
|
||
|
|
3. **返回契约**:维持「接口全量返回全部计数、前端按视图渲染」,还是按 viewType 只算/只返回该视图需要的字段?(全量返回更简单,但若 #01 让不同视图口径分叉,同名字段在不同视图含义不同就危险。)
|
||
|
|
4. 定稿后列出 `LeadViewQueryImplTest#countStats_*` 需要新增/改写的用例清单(不写实现,只列口径断言)。
|
||
|
|
|
||
|
|
## Answer
|
||
|
|
|
||
|
|
### Grill 1 — DTO 字段命名(甲)
|
||
|
|
|
||
|
|
**方案 A + `voided`**。
|
||
|
|
|
||
|
|
- `claimed` **收窄为仅 status=3**(“已领取”);新增 `sellerHolding` 承担 status IN (3,4)(“销售持有”)。
|
||
|
|
- 新增 `following`(status=4)、`expired`(status=6)、`voided`(status=7,与 `LeadConstants.STATUS_VOID` 命名呼应)。
|
||
|
|
- **加法关系(可作测试断言)**:`sellerHolding == claimed + following`;`total == undistributed + waiting + claimed + following + converted + expired + voided`(同一视图下、无动态筛选、7 态无交并时成立)。
|
||
|
|
- **升级注意事项**:`claimed` 名字不变但**语义改了**(3+4 → 仅 3)。管理视图前端原本读 `claimed` 渲染「销售持有」卡的,**必须同步改为读 `sellerHolding`**。DTO javadoc 也要改。
|
||
|
|
- 未采纳的方案:
|
||
|
|
- 方案 B(`claimed` 改名 `sellerHolding` + 新增 `claimedOnly`):「已领取」中文卡对应 `claimedOnly` 反直觉。
|
||
|
|
- `abolished` / `invalidated`:前者行政色彩强;后者与 `expired`(失效)中文易混。
|
||
|
|
|
||
|
|
### Grill 2 — feedback 三字段去留(甲)
|
||
|
|
|
||
|
|
**选项甲:直接删除 `feedbackValid` / `feedbackInvalid` / `feedbackNone`。**
|
||
|
|
|
||
|
|
- DTO 三个字段删。
|
||
|
|
- `LeadViewQueryImpl#countStats` 里那段按 `feedback_status` GROUP BY 的 SQL(现实现 line 112-127)一并删,省一次数据库往返。
|
||
|
|
- **契约破坏性变更**:前端若在读这三字段,翻新后会读不到;因本次改版本就需前端接新字段(`following`/`expired`/`voided`/`sellerHolding`),删三个已废字段不额外增加联调成本。
|
||
|
|
- 未采纳:
|
||
|
|
- 选项乙(保留不动):遗留 3 个死字段 + 每次 stats 多打一次 SQL,未来清理时机不明。
|
||
|
|
- 选项丙(保留字段、永返 0 + `@Deprecated`):前端若读会看到 0,比字段不存在更隐蔽。
|
||
|
|
|
||
|
|
### Grill 3 — 全量返回 vs 按 viewType 裁剪(甲)
|
||
|
|
|
||
|
|
**选项甲:全量返回,维持现状**。
|
||
|
|
|
||
|
|
- 后端 `countStats(param)` **不看 `viewType` 决定字段集**,`viewType` 仅进 `applyViewFilters`。一次 `GROUP BY status` 拿到全部 status 分组计数,`sellerHolding` = 内存里 `claimed + following` 相加,`todayNew` 单独一次 SQL。
|
||
|
|
- **DTO shape 是常量 10 字段**(Grill 1 定稿后):`total / undistributed / waiting / claimed / following / converted / expired / voided / sellerHolding / todayNew`。四视图同 shape。
|
||
|
|
- **payload 语义噪音可接受**:事实卡片渲染知识在前端(参看 ticket 02 映射表);后端返本视图用不上的字段(如公海的 `voided=0`)不构成误导。
|
||
|
|
- **未采纳**:
|
||
|
|
- 选项乙(按 viewType 裁剪):DTO shape 变 viewType-dependent,前端反序列化心智负担升,后端多 4 条代码路径;真能省的只有 `todayNew` 一次 SQL(我/我的关注 不需要)—代价不划算。
|
||
|
|
- 选项丙(`Integer` + `@JsonInclude(NON_NULL)`):`null` 与 `0` 两种“空”同字段两种含义;“本视图用哪些字段”知识被拆到后端 whitelist,与前端卡片渲染列表重复且需同步维护。
|
||
|
|
- **副作用预告**(甲已接受,具体处理看 Grill 4):若 `statusIn=[3]` 进 `countStats`,因 `applyViewFilters` 包含 `statusIn`,会返 `claimed=total 、其他为 0`。非 bug。
|
||
|
|
|
||
|
|
### Grill 4 — `statusIn` 与卡片下钻交互(甲)
|
||
|
|
|
||
|
|
**选项丁:`statusIn` 照传照吃,stats 严格遵循 P1-7。**
|
||
|
|
|
||
|
|
- 前端在 `/stats` 请求里传 `statusIn=[4]`,后端 `applyViewFilters` 叠 `status IN (4)`,返 `total=12 / following=12 / 其他 status 字段=0`。**这是正确行为,不是 bug**。
|
||
|
|
- “卡片跟随所有筛选(含 `statusIn`)”是**领域刻意**。“点一张卡其他卡归零”的产品观感由**前端渲染决策**处理(下钻页要不要重拉 stats、要不要隐藏其他卡),**后端 spec 不管**。
|
||
|
|
- **P1-7 硬约束一寸不让**:`/stats` 与 `/page` 完全同口径,`applyViewFilters` 零改动,`countStats` 不对 `param` 做任何字段擦除(不写 `param.setStatusIn(null)`)。
|
||
|
|
- **未采纳**:
|
||
|
|
- 选项甲(前端约定剔除):靠前端守约定;多前端接入方时可重现副作用。
|
||
|
|
- 选项乙(后端主动忽略):会在 stats/page 之间首次做口径分叉,破 P1-7。
|
||
|
|
- 选项丙(双接口):无必要复杂度。
|
||
|
|
|
||
|
|
### Grill 5 — 测试断言矩阵(甲按推荐)
|
||
|
|
|
||
|
|
`LeadViewQueryImplTest#countStats_*` 现有 3 个用例(`countStats_groupsByStatus` / `countStats_todayNewCountsTodayOnly` / `countStats_usesGroupByAggregation`)**全部重写**。下列为新断言清单(只列口径,不写实现)。
|
||
|
|
|
||
|
|
**两条类型决策(甲拍板):**
|
||
|
|
- `@DataScope` **合并为 1 个用例**(拦截器三档行为有独立测试覆盖,stats 侧不重复)。
|
||
|
|
- **需求✗格字段 = 甲(如实计算)**:全量返回 = 后端如实算 10 字段,前端按视图挑字段渲染;后端不知视图勾选表,每字段独立按公式算(与 Grill 3 一致)。
|
||
|
|
|
||
|
|
#### 每视图口径正确性(4 用例)
|
||
|
|
|
||
|
|
- **`countStats_publicPool_perCardCounts`**:造跨 status 1-7 + 今日/昨日 create。断言 `total == 视图内 count(*)`(仅 status IN 2,3,4)、`waiting > 0`、`todayNew` 仅数视图内今日、`claimed/following/converted/expired/voided/undistributed/sellerHolding == 0`(公海过滤排除)。
|
||
|
|
- **`countStats_myLead_perCardCounts`**:owner=我的 lead 覆盖 status ∈ {3,4,5,6,7} + owner=别人对照组。断言 `total == owner=我 count(*)`、`claimed/following/converted/expired/voided` 各对应计数、`waiting == 0`、`todayNew == 今日 owner=我的 create 数`(✗格字段如实返回,前端不渲染)。
|
||
|
|
- **`countStats_myFollow_perCardCounts`**:`lead_follow` 插 (userId=我, leadId) 若干,被关注 lead 覆盖 status ∈ {2,3,4,5,6,7},其中 status=7/6 的 owner=**别人**。断言 `total == follow 表内 count(*)`、各 status 计数、**`voided > 0` 且这些 voided 的 owner ≠ 我**(钉死“我的关注不叠 owner”)。
|
||
|
|
- **`countStats_manage_perCardCounts`**:造覆盖全 7 态。无 DataScope 场景断言 `total == 全表 count(*)`、`undistributed`(1)、`waiting`(2)、`sellerHolding == claimed + following`、`converted`、`expired`、`voided == 真实计数`(✗格如实返回)。
|
||
|
|
|
||
|
|
#### 加法关系与全量 shape(1 用例)
|
||
|
|
|
||
|
|
- **`countStats_sellerHoldingEqualsClaimedPlusFollowing`**:选 MANAGE(覆盖最全),断言 `stats.sellerHolding == stats.claimed + stats.following`。同一 wrapper 同一时刻算出,严格相等。
|
||
|
|
|
||
|
|
#### 今日新增口径(2 用例)
|
||
|
|
|
||
|
|
- **`countStats_todayNew_usesJvmLocalDateRange`**:造昨日 23:59:59 / 今日 00:00:00 / 今日中午 / 今日 23:59:59 / 明日 00:00:00 各一条。断言 `todayNew == 3`(今日三条),排除昨日末与明日初。验证边界 `[todayStart, tomorrowStart)`。时钟注入方式(Clock/固定时间戳)交实现 session,03 只写断言。
|
||
|
|
- **`countStats_todayNew_ignoresCurrentStatus`**:造今日 create 且 status=7 一条。断言 `todayNew` 计入。验证“只看 create_time 不看当前 status”。
|
||
|
|
|
||
|
|
#### `statusIn` 照传照吃(1 用例)
|
||
|
|
|
||
|
|
- **`countStats_respectsStatusInFilter`**:任一视图,param 带 `statusIn=[4]`,造 status=3/4/5 各若干。断言 `total == following == status=4 计数`,其余(含 `sellerHolding`,因 status=3 被 `statusIn` 排除)== 0。钉死 P1-7 同口径、`statusIn` 生效。
|
||
|
|
|
||
|
|
#### `@DataScope`(1 用例,合并)
|
||
|
|
|
||
|
|
- **`countStats_respectsDataScope`**:mock DataScopeInterceptor 注入 DEPT(或任一非 ALL_VISIBLE 档),MANAGE 视图断言数字 = 本部门可见 count(≠ 全表 count)。仅表明 stats 共享同一 wrapper、吃到 DataScope 天花板。三档完整覆盖由拦截器自己的单测负责。
|
||
|
|
|
||
|
|
### 交付下游(实现 session)
|
||
|
|
|
||
|
|
spec 已完整。实现 session 需落地:
|
||
|
|
1. `LeadStatsDTO`:删 `feedbackValid/feedbackInvalid/feedbackNone`;`claimed` 语义改为仅 status=3;新增 `following/expired/voided/sellerHolding`;重写 javadoc(四视图卡片映射 = ticket 02 表)。
|
||
|
|
2. `LeadViewQueryImpl#countStats`:删 feedback GROUP BY 那段 SQL;status 分组拆出 3/4 独立字段 + `sellerHolding=3+4` + `voided=7` + `expired=6`;今日改为 JVM `LocalDate.now()` 范围条件(弃 `DATE(create_time)=CURRENT_DATE`);`statusIn` 不擦除。
|
||
|
|
3. `LeadViewQueryImplTest`:按上面 10 个用例重写 `countStats_*`。
|
||
|
|
4. 前端:管理视图「销售持有」卡从读 `claimed` 改读 `sellerHolding`;接新字段;下钻交互自行决定是否重拉 stats / 隐藏其他卡。
|
||
|
|
|
||
|
|
### 最终 DTO 字段快照(10 字段)
|
||
|
|
|
||
|
|
| 字段 | 口径 | 备注 |
|
||
|
|
|---|---|---|
|
||
|
|
| `total` | 视图内 `count(*)` | 四视图均有 |
|
||
|
|
| `undistributed` | status=1 | 仅管理卡 |
|
||
|
|
| `waiting` | status=2 | |
|
||
|
|
| `claimed` | status=3 | **语义改了**:从 3+4 收窄为仅 3 |
|
||
|
|
| `following` | status=4 | 新增 |
|
||
|
|
| `converted` | status=5 | |
|
||
|
|
| `expired` | status=6 | 新增 |
|
||
|
|
| `voided` | status=7 | 新增(`void` 是 Java 保留字) |
|
||
|
|
| `sellerHolding` | status IN (3,4) | 新增,= `claimed + following` |
|
||
|
|
| `todayNew` | `create_time ∈ [todayStart, tomorrowStart)` | JVM 时区,不看当前 status |
|
||
|
|
|
||
|
|
删除:`feedbackValid` / `feedbackInvalid` / `feedbackNone`(及其 feedback GROUP BY SQL)。
|
||
|
|
|
||
|
|
### 明确不属于本 ticket
|
||
|
|
|
||
|
|
- 后端实现改动(新起 session,清单见上“交付下游”)。
|
||
|
|
- 前端卡片渲染 / 下钻是否重拉 stats 的 UI 决策。
|
||
|
|
- 状态机字段取舍(ticket 04)。
|