|
|
@@ -0,0 +1,466 @@
|
|
|
+# 报销域日期列头双三角排序 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:** 在有日期列的报销相关列表上,为 `datetime` 列头增加与 CRM 相同的双三角整库排序(视图合并页为拉全量后的客户端排序)。
|
|
|
+
|
|
|
+**Architecture:** 扩展 `ColumnHeaderFilter`:存在 `crmColumnSort` inject 且 `type==='datetime'` 时渲染双三角。页面接入已有 `useColumnSort`。后端复用 `resolve_datetime_order_by`;`applyTime` 映射 `create_time`。视图 mine/todo/all 已 `fetchAllPagedRecords` 合并双边数据,改 `sortMergedRows` 按当前 sort 状态排。
|
|
|
+
|
|
|
+**Tech Stack:** Vue 3、`ColumnHeaderFilter`、`useColumnSort`、FastAPI、SQLAlchemy、`resolve_datetime_order_by`
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-09-08-bx-date-column-sort-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 操作 | 文件 | 职责 |
|
|
|
+|------|------|------|
|
|
|
+| Modify | `web-admin/src/components/business/column-header-filter/index.vue` | datetime + inject 时显示双三角 |
|
|
|
+| Modify | `backend/app/services/bx/base.py` | `get_paged` 支持可选 `order_by` |
|
|
|
+| Create | `backend/app/services/bx/list_sort.py` | 各实体白名单封装(调用 crm `resolve_datetime_order_by`) |
|
|
|
+| Create | `backend/tests/test_bx_list_sort.py` | 白名单映射单测 |
|
|
|
+| Modify | `backend/app/api/bx/advance.py` + `services/bx/advance.py` | 透传 sort → order |
|
|
|
+| Modify | `backend/app/api/bx/reimburse.py` + `services/bx/reimburse.py` | 同上 |
|
|
|
+| Modify | `backend/app/api/bx/payment.py` + 付款 list 路径 | `applyTime`/`payTime` |
|
|
|
+| Modify | `backend/app/api/bx/invoice.py` + `services/bx/invoice.py` | 我的发票/流水 `createTime` |
|
|
|
+| Modify | `backend/app/api/bx/export_common.py` | `BxApplyListExportBody` 显式 `sortField`/`sortOrder`(可选,extra=allow 已可透传) |
|
|
|
+| Modify | 各 `views/bx/**/index.vue`(见 Task 6–10) | provide + merge query + 藏列 |
|
|
|
+
|
|
|
+**不做:** expense-item、workflow、invoice-wallet/admin;非日期列。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: `ColumnHeaderFilter` 双三角
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/business/column-header-filter/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 改模板根结构**
|
|
|
+
|
|
|
+当前根节点是单个 `el-popover`。改为外包一层,在 popover **之前**插入三角(与 CRM 列头「标题 | 三角 | 漏斗」一致;标题仍由页面 label 提供):
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <span class="col-header-filter-root">
|
|
|
+ <span
|
|
|
+ v-if="showSort"
|
|
|
+ class="col-header-sort-pair"
|
|
|
+ @click.stop
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="col-header-sort-btn"
|
|
|
+ :class="{ on: sortApi!.isSortActive(prop, 'asc') }"
|
|
|
+ title="升序"
|
|
|
+ aria-label="升序"
|
|
|
+ @click="sortApi!.setSort(prop, 'asc')"
|
|
|
+ >▲</button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="col-header-sort-btn"
|
|
|
+ :class="{ on: sortApi!.isSortActive(prop, 'desc') }"
|
|
|
+ title="降序"
|
|
|
+ aria-label="降序"
|
|
|
+ @click="sortApi!.setSort(prop, 'desc')"
|
|
|
+ >▼</button>
|
|
|
+ </span>
|
|
|
+ <el-popover ...现有内容不变...>
|
|
|
+ ...
|
|
|
+ </el-popover>
|
|
|
+ </span>
|
|
|
+</template>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: script 增加 inject**
|
|
|
+
|
|
|
+```ts
|
|
|
+import { computed, inject, ref, watch } from 'vue'
|
|
|
+// ...
|
|
|
+
|
|
|
+const sortApi = inject<{
|
|
|
+ setSort: (prop: string, order: 'asc' | 'desc') => void
|
|
|
+ isSortActive: (prop: string, order?: 'asc' | 'desc') => boolean
|
|
|
+} | null>('crmColumnSort', null)
|
|
|
+
|
|
|
+const showSort = computed(() => Boolean(sortApi && props.type === 'datetime'))
|
|
|
+```
|
|
|
+
|
|
|
+样式 `.col-header-sort-pair` 已在同文件全局 style 中(CRM Task 6 已加),无需重复。可补:
|
|
|
+
|
|
|
+```scss
|
|
|
+.col-header-filter-root {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 确认无 provide 时不显示三角;CRM 的 `CrmColFilterHeader` 仍自带三角(两套并存可接受)**
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/business/column-header-filter/index.vue
|
|
|
+git commit -m "feat(web-admin): show dual carets on ColumnHeaderFilter datetime columns."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: BX 排序白名单 helper + `get_paged` 可选 order
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/app/services/bx/list_sort.py`
|
|
|
+- Create: `backend/tests/test_bx_list_sort.py`
|
|
|
+- Modify: `backend/app/services/bx/base.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试**
|
|
|
+
|
|
|
+```python
|
|
|
+# backend/tests/test_bx_list_sort.py
|
|
|
+from app.models.bx_models import BxAdvanceApply, BxReimburseApply, BxInvoice, BxInvoiceLedger
|
|
|
+from app.services.bx.list_sort import (
|
|
|
+ order_advance,
|
|
|
+ order_reimburse,
|
|
|
+ order_invoice,
|
|
|
+ order_ledger,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def test_advance_apply_time_asc():
|
|
|
+ expr = order_advance("applyTime", "asc")
|
|
|
+ assert "create_time" in str(expr).lower()
|
|
|
+ assert "asc" in str(expr).lower()
|
|
|
+
|
|
|
+
|
|
|
+def test_advance_issue_time():
|
|
|
+ expr = order_advance("issueTime", "desc")
|
|
|
+ assert "issue_time" in str(expr).lower()
|
|
|
+
|
|
|
+
|
|
|
+def test_advance_invalid_falls_back():
|
|
|
+ default = BxAdvanceApply.create_time.desc()
|
|
|
+ assert order_advance("hack", "asc", default=default) is default
|
|
|
+
|
|
|
+
|
|
|
+def test_reimburse_pay_time():
|
|
|
+ expr = order_reimburse("payTime", "desc")
|
|
|
+ assert "pay_time" in str(expr).lower()
|
|
|
+
|
|
|
+
|
|
|
+def test_invoice_create_time():
|
|
|
+ expr = order_invoice("createTime", "asc")
|
|
|
+ assert "create_time" in str(expr).lower()
|
|
|
+```
|
|
|
+
|
|
|
+(若模型 import 路径不同,以仓库 `app.models` 实际模块为准。)
|
|
|
+
|
|
|
+- [ ] **Step 2: pytest 应 FAIL**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && PYTHONPATH=. pytest tests/test_bx_list_sort.py -v
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现 `list_sort.py`**
|
|
|
+
|
|
|
+```python
|
|
|
+# backend/app/services/bx/list_sort.py
|
|
|
+from __future__ import annotations
|
|
|
+from typing import Any, Optional
|
|
|
+
|
|
|
+from app.models.bx_models import (
|
|
|
+ BxAdvanceApply,
|
|
|
+ BxInvoice,
|
|
|
+ BxInvoiceLedger,
|
|
|
+ BxReimburseApply,
|
|
|
+)
|
|
|
+from app.services.crm.list_sort import resolve_datetime_order_by
|
|
|
+
|
|
|
+ADVANCE_WHITELIST = {
|
|
|
+ "applyTime": BxAdvanceApply.create_time,
|
|
|
+ "issueTime": BxAdvanceApply.issue_time,
|
|
|
+}
|
|
|
+REIMBURSE_WHITELIST = {
|
|
|
+ "applyTime": BxReimburseApply.create_time,
|
|
|
+ "payTime": BxReimburseApply.pay_time,
|
|
|
+}
|
|
|
+INVOICE_WHITELIST = {"createTime": BxInvoice.create_time}
|
|
|
+LEDGER_WHITELIST = {"createTime": BxInvoiceLedger.create_time}
|
|
|
+
|
|
|
+
|
|
|
+def order_advance(sort_field: Optional[str], sort_order: Optional[str], default=None):
|
|
|
+ if default is None:
|
|
|
+ default = BxAdvanceApply.create_time.desc()
|
|
|
+ return resolve_datetime_order_by(sort_field, sort_order, ADVANCE_WHITELIST, default)
|
|
|
+
|
|
|
+
|
|
|
+def order_reimburse(sort_field: Optional[str], sort_order: Optional[str], default=None):
|
|
|
+ if default is None:
|
|
|
+ default = BxReimburseApply.create_time.desc()
|
|
|
+ return resolve_datetime_order_by(sort_field, sort_order, REIMBURSE_WHITELIST, default)
|
|
|
+
|
|
|
+
|
|
|
+def order_invoice(sort_field: Optional[str], sort_order: Optional[str], default=None):
|
|
|
+ if default is None:
|
|
|
+ default = BxInvoice.create_time.desc()
|
|
|
+ return resolve_datetime_order_by(sort_field, sort_order, INVOICE_WHITELIST, default)
|
|
|
+
|
|
|
+
|
|
|
+def order_ledger(sort_field: Optional[str], sort_order: Optional[str], default=None):
|
|
|
+ if default is None:
|
|
|
+ default = BxInvoiceLedger.create_time.desc()
|
|
|
+ return resolve_datetime_order_by(sort_field, sort_order, LEDGER_WHITELIST, default)
|
|
|
+
|
|
|
+
|
|
|
+def parse_sort_params(params: dict) -> tuple:
|
|
|
+ """从 request/body dict 取 sortField/sortOrder(兼容 snake)。"""
|
|
|
+ sf = params.get("sortField") or params.get("sort_field")
|
|
|
+ so = params.get("sortOrder") or params.get("sort_order")
|
|
|
+ return (str(sf).strip() if sf else None), (str(so).strip() if so else None)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: `BxCrudService.get_paged` 增加可选 `order_by=None`**
|
|
|
+
|
|
|
+在现有默认 `order_by(sort_order, create_time)` 分支前:
|
|
|
+
|
|
|
+```python
|
|
|
+order_by: Optional[Any] = None,
|
|
|
+# ...
|
|
|
+if order_by is not None:
|
|
|
+ query = query.order_by(order_by)
|
|
|
+elif hasattr(model, "sort_order"):
|
|
|
+ ...
|
|
|
+else:
|
|
|
+ query = query.order_by(model.create_time.desc())
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: 测试通过并 Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/bx/list_sort.py backend/tests/test_bx_list_sort.py backend/app/services/bx/base.py
|
|
|
+git commit -m "feat(bx): add datetime list sort helpers and get_paged order_by."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 备用金 API/Service 接排序
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/bx/advance.py`
|
|
|
+- Modify: `backend/app/services/bx/advance.py`
|
|
|
+- Modify: `backend/app/api/bx/export_common.py`(可选显式字段)
|
|
|
+
|
|
|
+- [ ] **Step 1: `BxApplyListExportBody` 增加**
|
|
|
+
|
|
|
+```python
|
|
|
+sort_field: Optional[str] = Field(default=None, alias="sortField")
|
|
|
+sort_order: Optional[str] = Field(default=None, alias="sortOrder")
|
|
|
+```
|
|
|
+
|
|
|
+(`extra=allow` 已有则 GET query 的 `sortField` 进 `_merge_query` 即可;显式字段便于导出 body。)
|
|
|
+
|
|
|
+- [ ] **Step 2: 所有走 `AdvanceService.get_paged` 的 list/export**
|
|
|
+
|
|
|
+在调用前:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.services.bx.list_sort import order_advance, parse_sort_params
|
|
|
+
|
|
|
+sf, so = parse_sort_params(params)
|
|
|
+order = order_advance(sf, so)
|
|
|
+rows, total = AdvanceService.get_paged(..., order_by=order)
|
|
|
+```
|
|
|
+
|
|
|
+确认 `AdvanceService.get_paged` 把 `order_by` 传给 `BxCrudService.get_paged`(若当前是薄封装,补参数)。
|
|
|
+
|
|
|
+- [ ] **Step 3: `pending_approval` / `approved_by_me`**
|
|
|
+
|
|
|
+将硬编码 `.order_by(BxAdvanceApply.create_time.desc())` 改为 `order_by(order_advance(sf, so))`;`sf/so` 从该接口的 `params`/`_merge_query` 读取。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): support datetime column sort on advance lists."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 报销 API/Service 接排序
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/bx/reimburse.py`
|
|
|
+- Modify: `backend/app/services/bx/reimburse.py`
|
|
|
+
|
|
|
+镜像 Task 3:`get_paged`、`pending_approval`、`approved_by_me` 及对应 export 使用 `order_reimburse`。白名单含 `applyTime`、`payTime`。
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): support datetime column sort on reimburse lists."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 支付发放冲抵接排序
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/bx/payment.py`(及实际 list/export 调用的 service)
|
|
|
+
|
|
|
+待处理/已处理列表对 `BxReimburseApply` 排序时用 `order_reimburse`(`applyTime`/`payTime`)。冲抵记录子表(`BxReimburseOffset.create_time`)**本批不改**(非主列表漏斗列)。
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): support datetime column sort on payment lists."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 我的发票夹后端
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/bx/invoice.py`
|
|
|
+- Modify: `backend/app/services/bx/invoice.py`
|
|
|
+
|
|
|
+- `list_invoices`:经 `get_paged` 传入 `order_by=order_invoice(sf, so)`;API `_param_get` / merge 取出 sort。
|
|
|
+- `list_ledgers`:将 `order_by(BxInvoiceLedger.create_time.desc())` 换为 `order_ledger(sf, so)`。
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): support datetime column sort on my invoice wallet lists."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 接线备用金三页前端
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- `web-admin/src/views/bx/advance/apply/index.vue`
|
|
|
+- `web-admin/src/views/bx/advance/approve/index.vue`
|
|
|
+- `web-admin/src/views/bx/advance/issue/index.vue`
|
|
|
+
|
|
|
+每页:
|
|
|
+
|
|
|
+```ts
|
|
|
+import { useColumnSort } from '@/composables/useColumnSort'
|
|
|
+
|
|
|
+const {
|
|
|
+ setSort, clearSort, isSortActive, sortField, sortOrder,
|
|
|
+ toQueryParams: toSortQueryParams,
|
|
|
+ watchVisibleColumns: watchSortVisibleColumns
|
|
|
+} = useColumnSort({ onChange: () => { /* 现有 reset page + load */ } })
|
|
|
+
|
|
|
+provide('crmColumnSort', { setSort, clearSort, isSortActive, sortField, sortOrder })
|
|
|
+```
|
|
|
+
|
|
|
+列表/导出参数合并 `...toSortQueryParams()`;`watchSortVisibleColumns(visibleFilterProps)`。
|
|
|
+
|
|
|
+审批/发放若双 Tab 共用一套 filters,共用一套 sort 即可。
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): wire date column sort on advance apply/approve/issue pages."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 8: 接线报销申请/审批前端
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- `web-admin/src/views/bx/reimburse/apply/index.vue`
|
|
|
+- `web-admin/src/views/bx/reimburse/approve/index.vue`
|
|
|
+
|
|
|
+同 Task 7 模式。
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): wire date column sort on reimburse apply/approve pages."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 9: 接线支付页前端
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- `web-admin/src/views/bx/payment/index.vue`
|
|
|
+
|
|
|
+同模式;`payTime` 仅已处理 Tab 有漏斗,藏列时 `watchSortVisibleColumns` 清排序。
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): wire date column sort on payment page."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 10: 接线我的发票夹前端
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- `web-admin/src/views/bx/invoice-wallet/mine/index.vue`
|
|
|
+
|
|
|
+发票表与流水表各有 `useServerColumnFilters`。推荐:
|
|
|
+
|
|
|
+- 两套 `useColumnSort`(`invoiceSort` / `ledgerSort`)
|
|
|
+- `provide` 时按当前 Tab 提供,或给 `ColumnHeaderFilter` 增加可选 `injectKey`(若改组件成本高:切换 Tab 时 `provide` 无法动态——改用 **同一套 sort**,切 Tab 时 `clearSort`,更简单)
|
|
|
+
|
|
|
+**选定更简单:共用一套 `useColumnSort`,切 Tab 时 `clearSort()`,请求分别带 `toSortQueryParams()`。**
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): wire date column sort on my invoice wallet page."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 11: 视图三页(合并列表 · 客户端排序)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- `web-admin/src/views/bx/view/mine/index.vue`
|
|
|
+- `web-admin/src/views/bx/view/todo/index.vue`
|
|
|
+- `web-admin/src/views/bx/view/all/index.vue`
|
|
|
+
|
|
|
+现状:`sortMergedRows` 固定 `applyTime` 倒序。这些页拉全量后合并,**以客户端排序为准**:
|
|
|
+
|
|
|
+```ts
|
|
|
+function sortMergedRows(rows: any[]) {
|
|
|
+ const field = sortField.value || 'applyTime'
|
|
|
+ const order = sortOrder.value || 'desc'
|
|
|
+ return [...rows].sort((a, b) => {
|
|
|
+ const av = String(a[field] || '')
|
|
|
+ const bv = String(b[field] || '')
|
|
|
+ if (!av && !bv) return 0
|
|
|
+ if (!av) return order === 'asc' ? -1 : 1 // 空值:升序前、降序后
|
|
|
+ if (!bv) return order === 'asc' ? 1 : -1
|
|
|
+ const cmp = av.localeCompare(bv)
|
|
|
+ return order === 'asc' ? cmp : -cmp
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+仍 `provide('crmColumnSort')` 驱动列头三角;`onChange` → `loadData()`。
|
|
|
+可选:请求时仍带 `sortField` 给单边 API(非必须,合并后会再排)。
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "feat(bx): wire date column sort on bx view mine/todo/all pages."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 12: 回归与图谱
|
|
|
+
|
|
|
+- [ ] **Step 1:** `PYTHONPATH=. pytest tests/test_bx_list_sort.py tests/test_crm_list_sort.py -v`
|
|
|
+- [ ] **Step 2:** `pnpm dlx vitest run src/composables/useColumnSort.test.ts`
|
|
|
+- [ ] **Step 3:** 手工:备用金申请 `applyTime`;发放 `issueTime`;报销申请;支付 `payTime`;我的发票夹;我的申请单合并列表;费用项目无三角。
|
|
|
+- [ ] **Step 4:** `./scripts/update-graph.sh all` 后仅提交图谱变更:
|
|
|
+
|
|
|
+```bash
|
|
|
+git commit -m "chore: update graphify after BX date column sort."
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec 覆盖自检
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| ColumnHeaderFilter 扩展 | 1 |
|
|
|
+| applyTime→create_time 白名单 | 2–5 |
|
|
|
+| 备用金/报销/支付/发票夹 | 3–6, 7–10 |
|
|
|
+| 视图合并页 | 11(客户端排序,符合现网拉全量架构) |
|
|
|
+| 跳过费用项目/工作流/发票夹管理 | 文件结构「不做」 |
|
|
|
+| 空值升降序 | 2 helper + 11 sortMergedRows |
|
|
|
+| 与 CRM 同 inject key | 1, 7–11 |
|
|
|
+
|
|
|
+无 TBD;视图页明确为客户端排序,避免与「整库服务端」表述冲突——spec §7 已提示 union/合并风险,本计划按现网 `fetchAllPagedRecords` 落地。
|