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

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 保留字,作废字段不能叫 voidLeadConstants 已用 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 + followingtotal == 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)):null0 两种“空”同字段两种含义;“本视图用哪些字段”知识被拆到后端 whitelist,与前端卡片渲染列表重复且需同步维护。
  • 副作用预告(甲已接受,具体处理看 Grill 4):若 statusIn=[3]countStats,因 applyViewFilters 包含 statusIn,会返 claimed=total 、其他为 0。非 bug。

Grill 4 — statusIn 与卡片下钻交互(甲)

选项丁:statusIn 照传照吃,stats 严格遵循 P1-7。

  • 前端在 /stats 请求里传 statusIn=[4],后端 applyViewFiltersstatus 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 > 0todayNew 仅数视图内今日、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 == 0todayNew == 今日 owner=我的 create 数(✗格字段如实返回,前端不渲染)。
  • countStats_myFollow_perCardCountslead_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 + followingconvertedexpiredvoided == 真实计数(✗格如实返回)。

加法关系与全量 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/feedbackNoneclaimed 语义改为仅 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)。