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.
12 KiB
12 KiB
LeadStatsDTO 字段形态与前端返回契约
Type: grilling Status: resolved Blocked by: 02
事实基础(session 03 核实)
- 现状
LeadStatsDTO9 字段:total / waiting / claimed / converted / todayNew / undistributed / feedbackValid / feedbackInvalid / feedbackNone。 claimed字段名与语义已错位:LeadViewQueryImplline 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 的最终字段与接口返回契约:
- 新增字段:现有 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)」。命名要不歧义。
- feedback 三字段:新需求四视图卡片清单里已不含反馈相关卡片,这三字段是否废弃 / 保留兼容?→ 可能 graduate 出一个「删字段是否破坏其他调用方」的调查。
- 返回契约:维持「接口全量返回全部计数、前端按视图渲染」,还是按 viewType 只算/只返回该视图需要的字段?(全量返回更简单,但若 #01 让不同视图口径分叉,同名字段在不同视图含义不同就危险。)
- 定稿后列出
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(失效)中文易混。
- 方案 B(
Grill 2 — feedback 三字段去留(甲)
选项甲:直接删除 feedbackValid / feedbackInvalid / feedbackNone。
- DTO 三个字段删。
LeadViewQueryImpl#countStats里那段按feedback_statusGROUP 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,与前端卡片渲染列表重复且需同步维护。
- 选项乙(按 viewType 裁剪):DTO shape 变 viewType-dependent,前端反序列化心智负担升,后端多 4 条代码路径;真能省的只有
- 副作用预告(甲已接受,具体处理看 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 需落地:
LeadStatsDTO:删feedbackValid/feedbackInvalid/feedbackNone;claimed语义改为仅 status=3;新增following/expired/voided/sellerHolding;重写 javadoc(四视图卡片映射 = ticket 02 表)。LeadViewQueryImpl#countStats:删 feedback GROUP BY 那段 SQL;status 分组拆出 3/4 独立字段 +sellerHolding=3+4+voided=7+expired=6;今日改为 JVMLocalDate.now()范围条件(弃DATE(create_time)=CURRENT_DATE);statusIn不擦除。LeadViewQueryImplTest:按上面 10 个用例重写countStats_*。- 前端:管理视图「销售持有」卡从读
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)。