CSV 字符串被当等值匹配,仪表盘四个接口同时静默返空
某天上线了一个数据仪表盘,产品打开后发现:趋势图、分布图、明细表、导出——全部空白。但页面顶部的"汇总概览"卡片显示正常。后台没有任何报错,所有接口都返回 HTTP 200,只是 data: []、total: 0。
第一反应是数据问题,翻了半天数据库,发现数据完好无损。
现象
- 同一时间范围内,汇总接口
/overview返回正确的总金额和单量 - 其余四个接口(
/list、/trend、/distribution、/export)全部返回空 - 单值请求
docType=NS能正常出数据,多值请求docType=NS,SR,PR,PX必空 - 没有 5xx,没有异常日志,纯静默失败
根因
开启慢日志后,看到了这条 SQL:
SELECT * FROM purchase_records
WHERE del_flag = 0
AND doc_type = 'NS,SR,PR,PX'
AND doc_date BETWEEN '2026-01-01' AND '2026-03-31'
-- Rows: 0根因是共用查询构造器里的一行代码:
// ThinkPHP — 共用的 buildPurchaseQuery()
$query->where('doc_type', $params['docType']);where('col', $value) 在 ThinkPHP / Laravel Eloquent 里生成的是等值匹配 col = ?。前端传来的是逗号分隔的字符串 "NS,SR,PR,PX",整个字符串被绑定为单个参数——而表里任何一行的 doc_type 都不会等于 "NS,SR,PR,PX" 这个字面量,于是永远返回空集。
更糟的是,这个构造器被四个接口共享,一行 bug 同时把四个数据通道全部拦截。
坑:where(col, csvString) 不是 whereIn
$query->where('doc_type', 'NS,SR,PR,PX') 生成的 SQL 是:
WHERE doc_type = 'NS,SR,PR,PX'不是:
WHERE doc_type IN ('NS', 'SR', 'PR', 'PX')HTTP 返回 200、无异常、单值 smoke test 通过——三重掩护让这个 bug 能在生产藏几周不被发现。
正解
场景 A:列表 / 明细 / 导出——用 whereIn 过滤子集
// ThinkPHP
$docTypes = array_values(array_filter(
explode(',', (string)($params['docType'] ?? ''))
));
if (!empty($docTypes)) {
$query->whereIn('doc_type', $docTypes);
}// Knex / Node
const docTypes = (params.docType || '').split(',').map(s => s.trim()).filter(Boolean);
if (docTypes.length) query.whereIn('doc_type', docTypes);<!-- MyBatis -->
<if test="docTypeList != null and docTypeList.size() > 0">
AND doc_type IN
<foreach collection="docTypeList" item="t" open="(" separator="," close=")">
#{t}
</foreach>
</if>场景 B:分布图——不要用用户传入的 CSV 去过滤
分布接口的语义是"按类型汇总",如果用用户传来的过滤条件去 WHERE,你正在过滤的恰好是要分组的维度,结果必然全空。应该用枚举硬白名单,或完全不过滤:
// 分布接口:用枚举硬限,不用用户 CSV
$query->whereIn('doc_type', ['NS', 'SR', 'PR', 'PX'])
->field('doc_type, COUNT(*) AS cnt, SUM(amount) AS amt')
->group('doc_type');共用构造器防御性重构
让构造器从一开始就接受数组,字符串输入自动拆解,从根源堵住这类 bug:
private function normalizeMultiValue($v): array {
if (is_array($v)) return array_values(array_filter($v, fn($x) => $x !== ''));
if (is_string($v) && $v !== '') {
return array_values(array_filter(explode(',', $v), fn($x) => trim($x) !== ''));
}
return [];
}正解:区分接口语义,分别用 whereIn 或跳过过滤
- 列表 / 明细 / 导出:explode CSV →
whereIn - 分布 / 聚合:不用用户传来的分类字段过滤,用枚举硬白名单 GROUP BY
- 构造器本身:接受 array 并封装
normalizeMultiValue,让调用方没机会传 CSV 字符串
排查捷径
- 开启查询日志(ThinkPHP:
Db::getLastSql()),直接看生成的 SQL - 看到
WHERE col = 'a,b,c'就确认是这个坑 - 对比:
SELECT doc_type, COUNT(*) FROM t GROUP BY doc_type的总行数 vs 接口total
别只修一个接口——共用构造器意味着 bug 同域扩散,修完所有消费者之后,还要把构造器本身防御化,否则下一个新接口接进来就会重蹈覆辙。
一句话外卖
多值过滤参数进后端时,先看生成的 SQL 是
= ?还是IN (?),前者只匹配字面量,不会自动拆 CSV。