ThinkPHP 模型 `saveAll` 批量更新时,我因为漏看了第三个参数,把全站商品价格改成了同一个数

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 75 浏览 0 回复

昨晚给商城后台加了个"批量改价"功能,测试环境跑得好好的,上线后运营反馈:怎么所有 SKU 价格变成一样了?

冷汗下来,回滚数据,翻代码。问题就出在 `saveAll` 的调用方式上。

先看我写的错误代码:

// 从 Excel 导入的价格数组
$priceList = [
    ['id' => 101, 'price' => 59.00],
    ['id' => 102, 'price' => 89.00],
    ['id' => 103, 'price' => 129.00],
];

$model = new \app\model\GoodsSku;
$model->saveAll($priceList);

看起来没毛病对吧?数组里每个元素都有主键 `id`,ThinkPHP 应该会识别成更新操作。但问题藏在 `saveAll` 的第三个参数——它默认是 `false`,表示批量新增,而不是更新。

更坑的是,当传入的数据包含主键时,TP 的处理逻辑是:先尝试 `insert`,如果主键冲突了,部分场景下会静默覆盖或者抛出异常取决于你的 DB 配置。但我在 MySQL 里没开严格模式,加上模型设置了 `replaceOnPKConflict` 的惯性思维……实际执行的是批量插入,主键冲突时某些字段被覆写,最终表现为"最后一条数据的价格覆盖了一堆记录"。

运营那边看到的就是:101、102、103 三个 SKU,价格全变成了 129。

正确的写法应该是这样:

// 明确指定第二个参数为 true,表示"更新"而非"插入"
$model = new \app\model\GoodsSku;
$model->saveAll($priceList, true);

// 或者更稳妥,用 Db 类的批量更新,明确控制 where 条件
Db::name('goods_sku')->updateAll($priceList, 'id');

// 再或者,如果你跟我一样老派,直接循环 save 虽然性能差但心里踏实:
foreach ($priceList as $item) {
    $model->find($item['id'])->save($item);
}

这里重点说 `saveAll($data, $replace = false, $getLastInsID = false)` 这个签名。第二个参数 `$replace` 名字太有迷惑性了,我一度以为是"是否替换已有数据"——结果人家意思是是否替换写入(即更新)。`false` = 批量插入,`true` = 批量更新。

命名反直觉 + 默认值为插入 + 主键存在时的模糊处理,三件套凑齐,坑人就稳了。

另外补一个我后来加的防御:批量更新前强制校验主键字段,且限制单次操作数量。现在这段逻辑长这样:

public function batchUpdatePrice(array $list): bool
{
    if (empty($list) || count($list) > 100) {
        throw new \Exception('单次最多更新 100 条');
    }
    
    foreach ($list as $item) {
        if (empty($item['id']) || !is_numeric($item['id'])) {
            throw new \Exception('缺少有效主键');
        }
    }
    
    return (new GoodsSku)->saveAll($list, true);
}

最后吐槽下,TP 文档里 `saveAll` 的示例几乎全是插入场景,更新的用法藏在某个角落。我翻文档花了二十分钟,翻源码确认行为又花了四十分钟——这时间够我写两个接口了。

你们有没有被框架"语义模糊"的参数坑过?欢迎交流,让我知道我不是一个人。

评论0
回复 · 0
还没有回复
微信客服 微信客服