docs: add implementation plan for plan deliveryTimes column

Break the approved design into TDD tasks across filters, schema,
export, and the four plan-family list pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-12 21:05:41 +08:00
1 parent cb57a71d2d
commit de7d5194ad
1 file changed
+735
@@ -0,0 +1,735 @@
# 运输计划列表「送货时间」列 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在运输计划族四页列表「计划日期」后展示去重升序的送货段日期(`deliveryTimes`),并支持列头区间过滤(任一送货段命中)。
**Architecture:** 在 `build_plan_column_filters` 增加对 `pl_plan_delivery.delivery_time` 的 EXISTS 过滤;列表装配时从已加载送货段聚合 `deliveryTimes: string[]`;四页 Vue 增加合并列 + 列头漏斗;计划维度导出在「计划日期」后增加「送货时间」(` / ` 拼接)。不使用、不改写 `pl_plan.delivery_time`。
**Tech Stack:** FastAPI · SQLAlchemy · Pydantic v2 · Vue 3 · Element Plus · pytest
**Spec:** `docs/superpowers/specs/2026-09-12-pl-plan-delivery-times-column-design.md`
**提交约定:** 默认每个 Task 末尾 commit;若用户要求暂不提交,可攒到全部完成后再提交。
---
## 文件地图
| 职责 | 路径 |
|------|------|
| 过滤 EXISTS | Modify `backend/app/services/pl/plan_list_filters.py` |
| 过滤单测 | Modify `backend/tests/test_pl_plan_list_filters.py` |
| 聚合 helper + attach | Modify `backend/app/services/pl_plan_stop_service.py` |
| 聚合单测 | Create `backend/tests/test_pl_delivery_times_aggregate.py` |
| Schema | Modify `backend/app/schemas/pl/plan.py`(`PlanWithProducts`、`PlanContractGapRead`) |
| List/Export/Gaps API | Modify `backend/app/api/pl/plan.py` |
| Gaps service 过滤+字段 | Modify `backend/app/services/pl_plan_contract_gap.py` |
| FE Plan 类型 | Modify `web-admin/src/api/pl/index.ts` |
| FE 列定义 | Modify 四页 `columnFilterDefs.ts` |
| FE 四页列表 | Modify `plan` / `plan-archive` / `plan-cert` / `plan-contract` 的 `index.vue` |
**不做:** DB 迁移、小程序、归档段次展开导出加聚合列、改顶部日期 pills。
---
### Task 1: 过滤 EXISTS + 单测
**Files:**
- Modify: `backend/app/services/pl/plan_list_filters.py`
- Modify: `backend/tests/test_pl_plan_list_filters.py`
- [ ] **Step 1: 写失败单测**
在 `backend/tests/test_pl_plan_list_filters.py` 末尾追加:
```python
def test_delivery_time_range_exists_requires_db():
filters = build_plan_column_filters(
db=None,
tenant_no="shanxia2.0",
delivery_time_from=date(2026, 9, 12),
delivery_time_to=date(2026, 9, 14),
)
assert filters == []
def test_delivery_time_range_exists_sql(monkeypatch):
class _FakeQuery:
def __init__(self):
self.filters = []
def filter(self, *args):
self.filters.extend(args)
return self
def exists(self):
return ("EXISTS", tuple(self.filters))
class _FakeDb:
def query(self, *_args, **_kwargs):
return _FakeQuery()
filters = build_plan_column_filters(
db=_FakeDb(),
tenant_no="shanxia2.0",
delivery_time_from=date(2026, 9, 12),
delivery_time_to="2026-09-14",
)
assert len(filters) == 1
kind, clauses = filters[0]
assert kind == "EXISTS"
compiled = " ".join(_compile(c) for c in clauses)
assert "pl_plan_delivery" in compiled or "delivery_time" in compiled
assert ">=" in compiled
assert "<=" in compiled
```
说明:若实现选用 SQLAlchemy `exists()` 表达式而非假 query 元组,将 `test_delivery_time_range_exists_sql` 改为对真实 `exists` clause 做 `_compile`,断言含 `exists`、`delivery_time`、`>=`、`<=`、`del_flag`(与现有 product_name exists 测法一致即可)。**以能编译出 EXISTS 子查询且不引用 `pl_plan.delivery_time` 为准。**
- [ ] **Step 2: 跑测确认失败**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py::test_delivery_time_range_exists_requires_db tests/test_pl_plan_list_filters.py::test_delivery_time_range_exists_sql -v
```
Expected: FAIL(未知参数或断言失败)
- [ ] **Step 3: 实现过滤**
在 `plan_list_filters.py`:
1. Import `PlPlanDelivery`:
```python
from app.models.pl_models import PlPlan, PlPlanProduct, PlPlanDelivery
```
2. 增加 helper(推荐真实 exists,便于编译断言):
```python
def plan_delivery_time_exists(
db: Session,
tenant_no: str,
*,
d_from: Optional[date] = None,
d_to: Optional[date] = None,
):
"""任一未删除送货段的送货日历日落在 [from, to] 内。"""
q = db.query(PlPlanDelivery.id).filter(
PlPlanDelivery.plan_id == PlPlan.id,
PlPlanDelivery.tenant_no == tenant_no,
PlPlanDelivery.del_flag == 0,
PlPlanDelivery.delivery_time.isnot(None),
)
if d_from is not None:
q = q.filter(func.date(PlPlanDelivery.delivery_time) >= d_from)
if d_to is not None:
q = q.filter(func.date(PlPlanDelivery.delivery_time) <= d_to)
return q.exists()
```
3. `build_plan_column_filters` 增加参数并在 `plan_date` 过滤后追加:
```python
delivery_time_from: DateLike = None,
delivery_time_to: DateLike = None,
```
```python
dt_from = _parse_date(delivery_time_from)
dt_to = _parse_date(delivery_time_to)
if (dt_from is not None or dt_to is not None) and db is not None:
filters.append(
plan_delivery_time_exists(
db, tenant_no, d_from=dt_from, d_to=dt_to
)
)
```
若采用真实 `exists()`,把 Step 1 的 fake-db 测试改成:
```python
def test_delivery_time_range_exists_sql():
from unittest.mock import MagicMock
db = MagicMock()
q = MagicMock()
db.query.return_value = q
q.filter.return_value = q
q.exists.return_value = "DELIVERY_TIME_EXISTS"
filters = build_plan_column_filters(
db=db,
tenant_no="shanxia2.0",
delivery_time_from=date(2026, 9, 12),
delivery_time_to="2026-09-14",
)
assert filters == ["DELIVERY_TIME_EXISTS"]
assert db.query.called
# filter 调用中应包含 date 比较;至少确认 exists 被调用
assert q.exists.called
```
- [ ] **Step 4: 跑测通过**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py -v
```
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add backend/app/services/pl/plan_list_filters.py backend/tests/test_pl_plan_list_filters.py
git commit -m "$(cat <<'EOF'
feat(pl): filter plans by delivery-stop date range
Add EXISTS on pl_plan_delivery.delivery_time for list column filters.
EOF
)"
```
---
### Task 2: `deliveryTimes` 聚合 helper + 单测
**Files:**
- Modify: `backend/app/services/pl_plan_stop_service.py`
- Create: `backend/tests/test_pl_delivery_times_aggregate.py`
- [ ] **Step 1: 写失败单测**
创建 `backend/tests/test_pl_delivery_times_aggregate.py`:
```python
from datetime import date, datetime
from app.services.pl_plan_stop_service import collect_unique_delivery_date_strings
class _D:
def __init__(self, delivery_time):
self.delivery_time = delivery_time
def test_collect_unique_sorted_yyyy_mm_dd():
rows = [
_D(datetime(2026, 9, 14, 10, 0, 0)),
_D(datetime(2026, 9, 12, 8, 0, 0)),
_D(date(2026, 9, 12)),
_D(None),
_D(""),
_D("2026-09-13"),
]
assert collect_unique_delivery_date_strings(rows) == [
"2026-09-12",
"2026-09-13",
"2026-09-14",
]
def test_collect_empty():
assert collect_unique_delivery_date_strings([]) == []
assert collect_unique_delivery_date_strings([_D(None)]) == []
```
- [ ] **Step 2: 跑测确认失败**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_delivery_times_aggregate.py -v
```
Expected: FAIL(ImportError / 函数不存在)
- [ ] **Step 3: 实现 helper**
在 `pl_plan_stop_service.py` 合适位置(`attach_plan_stops_to_item` 附近)增加:
```python
def collect_unique_delivery_date_strings(deliveries) -> list[str]:
"""送货段 delivery_time → 去重升序 YYYY-MM-DD。"""
days: set[date] = set()
for row in deliveries or []:
raw = getattr(row, "delivery_time", None)
if raw is None or raw == "":
continue
if isinstance(raw, datetime):
days.add(raw.date())
elif isinstance(raw, date):
days.add(raw)
else:
text = str(raw).strip()
if not text:
continue
try:
days.add(datetime.strptime(text[:10], "%Y-%m-%d").date())
except ValueError:
continue
return [d.isoformat() for d in sorted(days)]
```
确保文件顶部已有 `date` / `datetime` import(已有则勿重复)。
- [ ] **Step 4: 跑测通过**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_delivery_times_aggregate.py -v
```
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add backend/app/services/pl_plan_stop_service.py backend/tests/test_pl_delivery_times_aggregate.py
git commit -m "$(cat <<'EOF'
feat(pl): aggregate unique delivery dates from stop rows
Provide YYYY-MM-DD dedupe helper for plan list deliveryTimes.
EOF
)"
```
---
### Task 3: Schema + attach 写入 `deliveryTimes`
**Files:**
- Modify: `backend/app/schemas/pl/plan.py`
- Modify: `backend/app/services/pl_plan_stop_service.py`(`attach_plan_stops_to_item`)
- Modify: `backend/app/services/pl_plan_contract_gap.py`(`_build_gap_item` 或等价构造函数)
- [ ] **Step 1: Schema 增加字段**
在 `PlanWithProducts`(约 `delivery_customer_summary` 后)增加:
```python
delivery_times: list[str] = Field(
default_factory=list,
alias="deliveryTimes",
description="送货段送货时间去重升序 YYYY-MM-DD(列表展示)",
)
```
在 `PlanContractGapRead` 增加同样字段(`deliveries` 字段附近):
```python
delivery_times: list[str] = Field(
default_factory=list,
alias="deliveryTimes",
description="送货段送货时间去重升序 YYYY-MM-DD",
)
```
- [ ] **Step 2: attach 时写入**
`attach_plan_stops_to_item` 末尾:
```python
plan_item.delivery_times = collect_unique_delivery_date_strings(delivery_reads)
```
(若 Pydantic model 用 alias,赋值用 `delivery_times` 属性名。)
- [ ] **Step 3: 合同缺口项写入**
在构造 `PlanContractGapRead(...)` 处增加:
```python
deliveryTimes=collect_unique_delivery_date_strings(deliveries),
```
或 `delivery_times=...`(与该处其它字段传参风格一致)。从 `pl_plan_stop_service` import `collect_unique_delivery_date_strings`。
- [ ] **Step 4: 快速冒烟**
```bash
cd backend && source venv/bin/activate && python -c "
from app.schemas.pl.plan import PlanWithProducts, PlanContractGapRead
from app.services.pl_plan_stop_service import collect_unique_delivery_date_strings, attach_plan_stops_to_item
print('ok', collect_unique_delivery_date_strings([]))
"
```
Expected: 打印 `ok []`,无 ImportError
- [ ] **Step 5: Commit**
```bash
git add backend/app/schemas/pl/plan.py backend/app/services/pl_plan_stop_service.py backend/app/services/pl_plan_contract_gap.py
git commit -m "$(cat <<'EOF'
feat(pl): expose deliveryTimes on plan list and contract-gap schemas
Populate from delivery stops during attach/gap row build.
EOF
)"
```
---
### Task 4: API 透传过滤参数 + 计划导出列
**Files:**
- Modify: `backend/app/api/pl/plan.py`
- Modify: `backend/app/services/pl_plan_contract_gap.py`(`list_plan_contract_gaps` 签名与过滤)
- [ ] **Step 1: `_filters_from_params` 透传**
```python
delivery_time_from=_param_get(params, "deliveryTimeFrom", "delivery_time_from"),
delivery_time_to=_param_get(params, "deliveryTimeTo", "delivery_time_to"),
```
- [ ] **Step 2: `list_plans` Query + `_merge_query`**
在 `plan_date_to` Query 旁增加:
```python
delivery_time_from: Optional[date] = Query(
default=None, alias="deliveryTimeFrom", description="送货时间开始"
),
delivery_time_to: Optional[date] = Query(
default=None, alias="deliveryTimeTo", description="送货时间结束"
),
```
并在 `_merge_query(...)` 调用处传入 `deliveryTimeFrom=delivery_time_from`、`deliveryTimeTo=delivery_time_to`。
证书列表若走同一 `list_plans`,无需另改。
- [ ] **Step 3: Export body + 计划维度导出填充**
`PlanExportBody` 增加(可依赖 `extra="allow"`,但显式字段更清晰):
```python
delivery_time_from: Optional[date] = Field(default=None, alias="deliveryTimeFrom")
delivery_time_to: Optional[date] = Field(default=None, alias="deliveryTimeTo")
```
`PLAN_EXPORT_COLUMNS` 在计划日期后插入:
```python
ExportColumn(title="计划日期", field="planDate"),
ExportColumn(title="送货时间", field="deliveryTimes"),
```
非 archive 导出循环中,在 `model_validate(plan)` 后批量加载 stops(页内已有 `load_plan_stops_grouped` 用法):
```python
plan_ids = [p.id for p in plans]
stops_by_plan = load_plan_stops_grouped(db, tenant_no, plan_ids)
items = []
for plan in plans:
data = PlanWithProducts.model_validate(plan).model_dump(by_alias=True)
if data.get("planDate") is not None:
data["planDate"] = str(data["planDate"])[:10]
_, delivery_reads = stops_by_plan.get(plan.id, (None, []))
times = collect_unique_delivery_date_strings(delivery_reads)
data["deliveryTimes"] = " / ".join(times) if times else ""
if data.get("erpEntered") is not None:
data["erpEntered"] = "是" if int(data["erpEntered"]) else "否"
items.append(data)
```
从 `pl_plan_stop_service` import `collect_unique_delivery_date_strings`(若尚未导入)。**不要**改 `PLAN_ARCHIVE_EXPORT_COLUMNS`(段次展开已有「段次时间」)。
- [ ] **Step 4: 合同缺口 list/export 透传**
`list_contract_gaps` / `export_contract_gaps`:Query/Body + 调用 `list_plan_contract_gaps(..., delivery_time_from=..., delivery_time_to=...)`。
在 `list_plan_contract_gaps` 内,于 `plan_date` 过滤附近增加:
```python
from app.services.pl.plan_list_filters import plan_delivery_time_exists, _parse_date
dt_from = _parse_date(delivery_time_from)
dt_to = _parse_date(delivery_time_to)
if dt_from is not None or dt_to is not None:
query = query.filter(
plan_delivery_time_exists(db, tenant_no, d_from=dt_from, d_to=dt_to)
)
```
(若 `_parse_date` 不想导出,可在 gap 服务内复用 `build_plan_column_filters` 只取 delivery 条件,或把 `_parse_date` 升为模块公开函数;优先直接调用 `build_plan_column_filters` 把返回的 filters `query.filter(*filters)`,但注意不要重复已有 plan_no 等条件——**仅追加 delivery 相关 EXISTS**:调用 `plan_delivery_time_exists` 最干净。)
`CONTRACT_GAP_EXPORT_COLUMNS` 在计划日期后加「送货时间」;export 行把 `deliveryTimes` list 转 `" / ".join(...)`。
- [ ] **Step 5: Commit**
```bash
git add backend/app/api/pl/plan.py backend/app/services/pl_plan_contract_gap.py
git commit -m "$(cat <<'EOF'
feat(pl): wire deliveryTimeFrom/To through plan list and export
Contract-gap list/export share the same stop-date EXISTS filter.
EOF
)"
```
---
### Task 5: 前端类型 + 四页 `columnFilterDefs`
**Files:**
- Modify: `web-admin/src/api/pl/index.ts`
- Modify: `web-admin/src/views/pl/plan/columnFilterDefs.ts`
- Modify: `web-admin/src/views/pl/plan-archive/columnFilterDefs.ts`
- Modify: `web-admin/src/views/pl/plan-cert/columnFilterDefs.ts`
- Modify: `web-admin/src/views/pl/plan-contract/columnFilterDefs.ts`
- [ ] **Step 1: Plan / Gap 类型**
在 `Plan` interface(`planDate` 附近)增加:
```typescript
deliveryTimes?: string[]
```
若合同缺口有独立 interface(搜 `PlanContractGap` / gap 列表类型),同样加 `deliveryTimes?: string[]`。
- [ ] **Step 2: 四页 columnFilterDefs**
在各文件的 `planDate` 定义后插入:
```typescript
{
prop: 'deliveryTimes',
label: '送货时间',
type: 'datetime',
queryKeyFrom: 'deliveryTimeFrom',
queryKeyTo: 'deliveryTimeTo'
},
```
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/api/pl/index.ts \
web-admin/src/views/pl/plan/columnFilterDefs.ts \
web-admin/src/views/pl/plan-archive/columnFilterDefs.ts \
web-admin/src/views/pl/plan-cert/columnFilterDefs.ts \
web-admin/src/views/pl/plan-contract/columnFilterDefs.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): add deliveryTimes column filter defs for plan lists
Align query keys with deliveryTimeFrom/To server filters.
EOF
)"
```
---
### Task 6: 主列表 `plan/index.vue` UI
**Files:**
- Modify: `web-admin/src/views/pl/plan/index.vue`
- [ ] **Step 1: 合并列与列设置**
`PLAN_MERGE_COLUMN_PROPS` 在 `'planDate'` 后插入 `'deliveryTimes'`。
列设置数组 `{ prop: 'planDate', label: '计划日期' }` 后插入:
```typescript
{ prop: 'deliveryTimes', label: '送货时间' },
```
- [ ] **Step 2: 表格列(计划日期列后)**
```vue
<el-table-column
v-if="isColumnVisible('deliveryTimes')"
prop="deliveryTimes"
label="送货时间"
min-width="130"
align="center"
>
<template #header>
<div class="col-header-with-filter">
<span class="col-header-with-filter__label">送货时间</span>
<ColumnHeaderFilter
prop="deliveryTimes"
label="送货时间"
type="datetime"
:is-active="isFilterActive('deliveryTimes')"
:model-value="filters.deliveryTimes || null"
@update:model-value="(v) => setFilterState('deliveryTimes', v)"
/>
</div>
</template>
<template #default="{ row }">
<div
v-if="(row.plan.deliveryTimes || []).length"
class="pl-delivery-times-cell"
>
<div
v-for="(t, i) in row.plan.deliveryTimes"
:key="`${row.plan.id}-dt-${i}`"
>
{{ t }}
</div>
</div>
<span v-else class="text-muted">—</span>
</template>
</el-table-column>
```
若项目无 `.text-muted`,用现有 muted 类或内联 `color: var(--mint-text-muted)`。
- [ ] **Step 3: `getDisplayStatCellValue`**
```typescript
if (prop === 'deliveryTimes') {
return (plan.deliveryTimes || []).join('\n') || '—'
}
```
(合并键用字符串化结果,保证同计划多行一致。)
- [ ] **Step 4: 样式**
scoped 中:
```scss
.pl-delivery-times-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
line-height: 1.35;
}
```
- [ ] **Step 5: 确认 pills 只清 planDate**
核对 `handleDateShortcut` 仅 `delete filters.planDate`,**不要**删除 `filters.deliveryTimes`。
- [ ] **Step 6: Commit**
```bash
git add web-admin/src/views/pl/plan/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): show deliveryTimes column on transport plan list
Multi-line YYYY-MM-DD with header range filter after planDate.
EOF
)"
```
---
### Task 7: 归档 / 证书列表 UI(与主列表同构)
**Files:**
- Modify: `web-admin/src/views/pl/plan-archive/index.vue`
- Modify: `web-admin/src/views/pl/plan-cert/index.vue`
- [ ] **Step 1: 两页均做与 Task 6 相同改动**
- `PLAN_MERGE_COLUMN_PROPS` 插入 `deliveryTimes`
- 列设置插入
- `el-table-column`(数据源仍为 `row.plan.deliveryTimes`)
- `getDisplayStatCellValue`(或等价)处理 `deliveryTimes`
- 同样 cell 样式类
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/views/pl/plan-archive/index.vue web-admin/src/views/pl/plan-cert/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add deliveryTimes column on plan archive and cert lists
Match main plan list display and column-header filter behavior.
EOF
)"
```
---
### Task 8: 合同完善列表 UI + 导出字段
**Files:**
- Modify: `web-admin/src/views/pl/plan-contract/index.vue`
- [ ] **Step 1: 合并与列**
`PLAN_MERGE_COLUMN_PROPS`:`['planDate', 'deliveryTimes', 'planType', 'followStatus']`(或在 `planDate` 后插入)。
表格在计划日期后增加列;单元格取 `asGapItem(row).deliveryTimes`(与该页 `planDate` 取值方式一致)。
列头过滤 defs 已在 Task 5;确保 `useServerColumnFilters` 使用的 defs 含 `deliveryTimes`,且 buildParams 会带上 `deliveryTimeFrom/To`。
- [ ] **Step 2: 查看弹窗(可选一致性)**
若补充信息 KV 有「计划日期」,在其后加「送货时间」多行或 ` / ` 拼接;无则跳过(YAGNI)。
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/views/pl/plan-contract/index.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): add deliveryTimes column on plan contract-gap list
Filter and display deduped delivery dates beside planDate.
EOF
)"
```
---
### Task 9: 回归验证 + 图谱更新
- [ ] **Step 1: 后端测试**
```bash
cd backend && source venv/bin/activate && pytest tests/test_pl_plan_list_filters.py tests/test_pl_delivery_times_aggregate.py -v
```
Expected: PASS
- [ ] **Step 2: 手测清单(对照 spec §4)**
1. `/#/pl/plan`:计划日期后有送货时间;多段同日去重;多日多行 `YYYY-MM-DD`
2. 列头送货时间区间:任一命中;顶部 pills 仍只动计划日期,不清送货时间过滤
3. 归档 / 证书 / 合同完善行为一致
4. 计划列表导出有「送货时间」,多值 ` / ` 分隔
5. 无送货时间显示 `—`,送货时间过滤不误命中
- [ ] **Step 3: 更新知识图谱**
```bash
./scripts/update-graph.sh backend
./scripts/update-graph.sh frontend
```
或按仓库约定:`graphify update .`(若脚本封装不同,以 `AGENTS.md` 为准)。
- [ ] **Step 4: Commit 图谱(若有 diff)**
```bash
git add backend/graphify-out web-admin/graphify-out graphify-out 2>/dev/null || true
git status -sb
# 仅当有图谱文件变更时:
git commit -m "$(cat <<'EOF'
chore: update graphify after deliveryTimes list column
EOF
)"
```
---
## Self-Review(对照 spec)
| Spec 项 | Task |
|---------|------|
| EXISTS 任一命中过滤 | Task 1、4 |
| `deliveryTimes[]` 去重升序 YYYY-MM-DD | Task 2、3 |
| 不用 `pl_plan.delivery_time` | Task 1/2 数据源仅 delivery 表/段 |
| 四页 UI + 合并列 + 列头漏斗 | Task 5–8 |
| pills 只筛计划日期 | Task 6 Step 5 |
| 计划维度导出 + 段次导出不加聚合列 | Task 4 |
| 合同缺口同源 | Task 3、4、8 |
| 无 DB 迁移 | 全计划无 DDL |
无 TBD 占位;命名统一 `deliveryTimes` / `deliveryTimeFrom` / `deliveryTimeTo` / `collect_unique_delivery_date_strings`。