返回博客

前端筛了,导出却全量——服务端导出的筛选条件漂移陷阱

在后台管理系统里,用户勾选了某个多选筛选项,表格立刻只显示匹配的几行,点"导出 Excel",下载下来一看——全量数据,刚才的筛选好像从未存在过。

没有任何报错,接口状态码 200,文件正常下载。但行数从 12 跳到了 487。用户第一反应:「这是数据 bug!」开发排查了半天接口,发现后端逻辑没问题——因为真正的问题在前端,而且只有一行。

现象

某个报表页面有日期、门店、等级三个筛选条件。前两个早就有,等级是本次新加的多选下拉框。

  • 勾选等级 A、门店 X → 表格更新,只显示 12 条记录 ✅
  • 点击「导出 Excel」→ 下载的文件有 487 行,包含了所有等级 ❌

根因

一张报表页面通常存在两条独立的数据通路

  1. 列表路径:前端把筛选条件拼成参数 → 调 getList 接口 → 后端 SQL 加 WHERE → 返回 JSON → 表格渲染。
  2. 导出路径:点击导出按钮 → 调 exportXlsx 接口 → 后端查 DB 生成文件 → 返回文件 URL → 浏览器下载。

两条路径打的是同一张表,但各自维护独立的参数对象。开发在新增等级筛选时,只把 level_code 加进了 fetchList 的 payload,导出按钮的调用忘了跟上:

// 典型漂移写法
methods: {
  fetchList() {
    api.getList({
      month:      this.month,
      store_code: this.storeFilter.join(','),
      level_code: this.levelFilter.join(','),  // ← 新增,已透传
    })
  },
  handleExport() {
    api.exportXlsx({
      month:      this.month,
      store_code: this.storeFilter.join(','),
      // level_code 漏了 ← 就这一行
    })
  },
}

后端导出接口的 SQL WHERE 也没有等级过滤分支,于是返回全量行。

这是静默漂移——无报错、无警告,用户只能通过肉眼对比行数才能发现,很多时候直接流入了线上业务决策。

坑:两条数据通路,新增筛选只修了一路

列表接口和导出接口分别维护各自的参数列表,是代码级定时炸弹。每次新增一个筛选维度,都必须同时更新两处;但因为导出路径没有即时视觉反馈,开发极容易只改了列表路径就提测通过,导出漂移完全不被单测覆盖。

正解

前端:共享同一份 payload 对象

// 提取公共 payload 构建函数
buildFilterPayload() {
  return {
    month:      this.month,
    store_code: this.storeFilter.join(','),
    level_code: this.levelFilter.join(','),
    date_range: this.dateRange,
    keyword:    this.keyword,
  }
},
 
fetchList() {
  api.getList(this.buildFilterPayload())
},
 
handleExport() {
  api.exportXlsx(this.buildFilterPayload())  // 同一来源
},

后续再加任何筛选字段,只需改 buildFilterPayload,两条路径自动同步。

后端:导出接口镜像列表接口的 WHERE 逻辑

// 导出控制器中,同样接收并应用 level_code
$levelCodes = $request->param('level_code/a', []);
if (!empty($levelCodes)) {
    $placeholders = implode(',', array_fill(0, count($levelCodes), '?'));
    $sql .= ' AND b.level_code IN (' . $placeholders . ')';
    $bindings = array_merge($bindings, $levelCodes);
}

不要为导出接口另写一套过滤逻辑——从列表接口的 WHERE 构建函数中提取公共方法复用。

数据量参考

| 数据量 | 推荐方案 | 漂移风险 | |--------|---------|----------| | < 5,000 行 | 前端直接用 xlsx-js-style 导出已渲染数组 | 零风险(数据源即筛选后数组) | | 5,000–50,000 行 | 服务端导出,必须透传所有筛选参数 | 高,必须强制检查 | | > 50,000 行 | 服务端异步任务 + 完成通知 | 高,同上 |

数据量允许时,优先选前端导出——直接把表格的数据数组传给 SheetJS,根本不存在两条路径,从架构上消除这个坑。

正解:一份 payload 函数,两条路径共用

把所有筛选条件聚合到 buildFilterPayload() 这一个函数,fetchListhandleExport 都调它。新增筛选字段时只改一处,无需记住还要更新导出路径。

一句话外卖

只要页面上有「导出」按钮,且导出是服务端生成的,就要把 fetchList 和 handleExport 的参数列表视为同一份合同——任何筛选字段缺席导出路径,都是一个等待被发现的数据漂移 bug。