背景:拉黑功能引发的数据泄露
公司客服系统需要实现消息拉黑功能:当用户拉黑对方后,被拉黑方发送的消息不展示在拉黑方的聊天记录中。原始查询逻辑如下:
$query = ChatMessage::with(['send'])
->whereRaw('(? = send_id AND ? = accept_id) OR (? = send_id AND ? = accept_id)',[$sendId, $acceptId, $acceptId, $sendId]);
return $query->orderByDesc('id')->paginate(20);
新增拉黑功能时,我"理所当然"地添加了过滤条件:
return ChatMessage::with(['send'])
->where('is_black', 0) // 或 ->where('is_black', '0')
->where(function ($q) use ($sendId, $acceptId) {
$q->where([
'send_id' => $sendId,
'accept_id' => $acceptId
])->orWhere([
'send_id' => $acceptId,
'accept_id' => $sendId
]);
})
->orderByDesc('id')
->paginate(20);
事故现场:
✅ 开发&前端自测通过 ❌ 测试发现拉黑消息仍出现在聊天记录
排查过程:
- 检查代码逻辑未发现问题
- 日志输出实际 SQL 语句:
WHERE is_black = 0
AND (send_id = A AND accept_id = B)
OR (send_id = B AND accept_id = A)
发现了问题所在,原来的代码中,OR 条件被放在了 AND 条件的后面,导致查询逻辑被错误拆分。
一、问题本质:SQL 的隐式优先级陷阱
错误逻辑解析
/* 预期逻辑 */
WHERE (is_black=0)
AND (
(A→B消息) OR (B→A消息)
)
/* 实际执行 */
WHERE (is_black=0 AND A→B消息)
OR (B→A消息) -- 此部分绕过拉黑检查!
预期效果: ✅ 获取用户A→B或B→A的消息 ✅ 过滤所有 is_black=1 的消息
实际效果: ❌ 拉黑消息(is_black=1)仍然出现在结果中
二、解决方案:用闭包强制逻辑分组
方案1:基础闭包分组(推荐)
->where('is_black', 0)
->where(function ($q) use ($sendId, $acceptId) {
$q->where([
['send_id', $sendId],
['accept_id', $acceptId]
])
->orWhere([
['send_id', $acceptId],
['accept_id', $sendId]
]);
})
方案2:多层闭包(更直观)
->where('is_black', 0)
->where(function ($q) use ($sendId, $acceptId) {
$q->where(function ($q1) use ($sendId, $acceptId) {
$q1->where('send_id', $sendId)
->where('accept_id', $acceptId);
})
->orWhere(function ($q2) use ($sendId, $acceptId) {
$q2->where('send_id', $acceptId)
->where('accept_id', $sendId);
});
})
方案3:参数绑定安全写法
->where('is_black', 0)
->where(function ($q) use ($sendId, $acceptId) {
$q->whereRaw('(? = send_id AND ? = accept_id)', [$sendId, $acceptId])
->orWhereRaw('(? = send_id AND ? = accept_id)', [$acceptId, $sendId]);
})
三、技术原理:闭包如何拯救查询
修正后的 SQL 结构
WHERE is_black = 0
AND ( -- 闭包生成的安全隔离层
(send_id = 'A' AND accept_id = 'B')
OR
(send_id = 'B' AND accept_id = 'A')
)
闭包的三大作用
- 逻辑隔离:创建独立的查询上下文
- 优先级控制:通过括号显式定义条件执行顺序
- 防污染:避免链式调用导致的意外关联
四、工程师的自我修养
必守规则
1. OR 必闭包:只要出现 `orWhere()` 立即包裹闭包
2. 调试先行:复杂查询必用 `DB::getQueryLog()` 验证
3. 字段验型:数字/字符串类型需与数据库严格一致
调试模板:
// 开启日志
DB::enableQueryLog();
// 执行查询
$messages = ChatMessage::...->paginate(20);
// 打印SQL(重要!)
logger('Final SQL', [
'sql' => DB::getQueryLog()[0]['query'],
'bindings' => DB::getQueryLog()[0]['bindings']
]);
五、总结:敬畏每一行代码
血泪教训:
"这个功能很简单" ➜ "代码不可能有问题" ➜ "测试怎么可能不通过" 是工程师翻车的经典三部曲。
核心认知:
ORM 不是 SQL 的简单封装,而是需要深度理解的抽象层。 当混合使用 AND/OR 时,闭包分组是保障逻辑正确的唯一路径。
终极建议: 在团队规范中强制要求:所有包含 orWhere() 的查询,必须用闭包包裹整个 OR 条件组。这能避免 90% 的条件逻辑错误。