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.

118 lines
5.4 KiB

2 weeks ago
<!-- wayfinder:spec -->
# 「今日新增」卡片下钻的列表联动入参(方案 B:语义布尔参数)
Type: spec
Status: resolved
Related: 02(口径映射表)、03(DTO 与返回契约)
## 背景 / 缺口
改版后「线索公海」和「线索管理」都有 **今日新增** 卡片。stats 侧口径已实现
(`LeadViewQueryImpl#countStats` line 119-130):
```
applyViewFilters(视图 + 公共筛选) + create_time ∈ [今日00:00, 次日00:00) // 不看 status
```
其中「今日」= JVM 时区的 `LocalDate.now()`,用**半开区间** `ge(create_time, todayStart).lt(create_time, tomorrowStart)`
**刻意不用** `DATE(create_time)=CURRENT_DATE`(对齐 JVM 时区、走索引)。
**缺口**:列表接口 `/api/lead/page` 的入参 `LeadPageParam`(含继承的 `BaseParam`
**没有任何创建时间范围字段**,可用下钻字段只有 `statusIn` + 若干等值筛选 + `keyword`
所以前端点「今日新增」卡片时,**无参数可把 `/page` 也过滤成"今天创建的"**——
其余按 status 的卡片都能靠 `statusIn` 联动,唯独今日新增不能。
## 决策:方案 B —— 加一个语义布尔参数 `todayNewOnly`
对比方案 A(加通用 `createTimeBegin/End`)后选 B,理由:
- **前后端用同一个 `LocalDate.now()`**:时间窗由**后端**按 `LocalDate.now()` 展开,
前端只传 `todayNewOnly=true`。彻底消除"前端算的今天"与"后端算的今天"跨零点 /
跨时区不一致的风险(方案 A 让前端传绝对时间戳,跨零点点击就可能错开一天)。
- **口径单一可信源**:stats 与 page 的今日窗口用**同一段展开逻辑**(下沉到
`applyViewFilters`),不会两处各写一遍再漂移。严守 P1-7「统计口径 = 列表口径」。
- 代价:只能查"今天",不能查任意区间。当前需求只有"今日新增"这一个时间卡片,
通用区间是 YAGNI,需要时再加 A。
## 实现要点
### 1. `LeadPageParam` 新增字段
`crm-lead/.../domain/param/LeadPageParam.java`
```java
@Schema(description = "仅看今日新增(按后端 JVM 时区 LocalDate.now() 展开 create_time ∈ [今日00:00,次日00:00);用于「今日新增」卡片下钻)")
private Boolean todayNewOnly;
```
- 类型 `Boolean`(可空)。`null` / `false` 都视为不过滤,只有 `true` 才追加时间窗。
### 2. 时间窗展开下沉到 `applyViewFilters`
`LeadViewQueryImpl#applyViewFilters`,在公共筛选段追加一条(与现有 `statusIn`
条件并列,条件式追加):
```java
if (Boolean.TRUE.equals(param.getTodayNewOnly())) {
LocalDate today = LocalDate.now();
wrapper.ge(Lead::getCreateTime, today.atStartOfDay())
.lt(Lead::getCreateTime, today.plusDays(1).atStartOfDay());
}
```
- 下沉到 `applyViewFilters` 后,`pageLeads` 自动生效;且 `countStats` 里的
status 分组查询 / total 也会随 `todayNewOnly=true` 收窄(前端下钻时通常只调 `/page`
但 stats 复用同一套 WHERE 意味着口径天然一致,无副作用)。
### 3. `countStats` 里的今日新增块可选收敛(非必须)
现有 line 119-130 独立算 todayNew 的那段,其时间窗逻辑与第 2 步**完全相同**。
可提取一个私有 helper(如 `todayCreateTimeRange()` 返回 `[start,end)`)给两处复用,
避免同一段 `LocalDate.now()` 展开写两遍。**此为整洁性收敛,不改口径**,可在实现时顺手做。
## 前端联动契约(交付给前端)
点击「今日新增」卡片时,在当前视图的 `/page` 请求上追加 `todayNewOnly:true`
```json
// 线索公海 今日新增
{ "viewType": "PUBLIC_POOL", "todayNewOnly": true, "current": 1, "size": 10 }
// 线索管理 今日新增
{ "viewType": "MANAGE", "todayNewOnly": true, "current": 1, "size": 10 }
```
- 「今日新增」**不看 status**,故下钻时 **不传 `statusIn`**(传了会叠加收窄,与卡片口径不符)。
- 其它筛选(poolId / channelCode / keyword…)可与 `todayNewOnly` 叠加,语义 = "今天创建的且满足其它筛选",符合列表页保留筛选条件的直觉。
- 其余按 status 的卡片仍走 `statusIn`,与本 ticket 无关(对照表见下)。
### 卡片 → 列表下钻传参对照
| 卡片 | 传参 |
|------|------|
| 待领取 | `statusIn:[2]` |
| 已领取 | `statusIn:[3]` |
| 跟进中 | `statusIn:[4]` |
| 销售持有 | `statusIn:[3,4]` |
| 已转商机 | `statusIn:[5]` |
| 过期失效 | `statusIn:[6]` |
| 线索作废 | `statusIn:[7]` |
| 未分发 | `statusIn:[1]` |
| **今日新增** | **`todayNewOnly:true`(不传 statusIn)** |
## 测试断言清单(不写实现,只列口径)
- `pageLeads_todayNewOnly_仅返回今日create_time记录`:造 2 条今天 + 1 条昨天,
`todayNewOnly=true` 只返回 2 条;`todayNewOnly=null` 返回全部。
- `pageLeads_todayNewOnly_不受status影响`:今天创建但 status 各异(1/3/7)均命中。
- `pageLeads_todayNewOnly_叠加其它筛选`:`todayNewOnly=true` + `channelCode=X`
只返回今天且渠道=X。
- `pageLeads_todayNewOnly_与stats一致`:同一 param 下
`pageLeads(todayNewOnly=true).total == countStats(...).todayNew`(口径一致性回归)。
- 边界:`create_time` 恰为今日 00:00:00.000 命中;次日 00:00:00.000 不命中(半开区间)。
## Out of scope
- 通用创建时间区间查询(方案 A 的 `createTimeBegin/End`)—— 需要任意区间时再开。
- 其它时间维度卡片(暂无需求)。