thinkphp 6.x 软删除必须使用 deleted_at 字段名和 datetime/timestamp 类型,显式配置 $deletetime = 'deleted_at',避免 autowritetimestamp 污染,批量操作需用 destroy() 而非 where()->delete()。

ThinkPHP 软删除字段名必须是 deleted_at,否则默认不生效
TP6.x 的 SoftDelete trait 默认只识别 deleted_at 字段,哪怕你数据库里建的是 delete_time 或 is_deleted,只要没显式配置,delete() 方法照样去更新 deleted_at——而这个字段如果不存在或类型不对,就会静默失败或写入 null 失效。
常见错误现象:
- 调用
$user->delete()后查数据库,delete_time字段没变 -
UserModel::withTrashed()->select()查不到已删数据,实际是因为字段名不匹配导致软删根本没触发
正确做法:
- 建表时统一用
ALTER TABLE user ADD deleted_at DATETIME NULL DEFAULT NULL - 模型中显式指定:
protected $deleteTime = 'deleted_at'; - 别依赖“自动推断”,TP6 不支持无配置自定义字段名
deleted_at 字段类型不能是 INT,必须是 DATETIME/TIMESTAMP
ThinkPHP 内部用 DateTime 对象做非空判断(如 whereNotNull('deleted_at')),如果字段类型是 INT 存时间戳,PHP 读出来是整数,框架仍按字符串/对象处理,结果永远不满足 IS NOT NULL 条件——导致软删数据始终被过滤掉,或者恢复查询失效。
使用场景:
- MySQL 中该字段必须设为
DATETIME或TIMESTAMP,且允许为NULL - 迁移文件里不能写
$table->integer('deleted_at'),得用$table->dateTime('deleted_at')->nullable() - 已有表改字段后,务必清空
runtime/cache/,否则模型可能缓存旧结构,查不出软删数据
批量删除时 where()->delete() 是硬删,和软删除无关
很多人写 UserModel::where('status', 0)->delete(),以为启用了软删除就自动转成标记更新——其实这是 Db 查询构造器的原生删法,完全绕过模型逻辑,直接执行 DELETE FROM user WHERE status = 0,软删配置形同虚设。
正确方式取决于你要什么:
- 要软删一批:先取 ID 列表,再用
destroy(),例如UserModel::destroy(UserModel::where('status', 0)->column('id'), true)(第二个参数true强制走软删) - 要真删一批:用
Db::name('user')->where('status', 0)->delete(),但注意它跳过所有模型事件、验证和事务包装 - 千万别混用:在启用了
SoftDelete的模型上调用where()->delete(),既不是软删也不是安全硬删,只是裸删
模型开启软删除后,autoWriteTimestamp 会污染 deleted_at 字段
如果模型同时开了 protected $autoWriteTimestamp = true,又没把 deleted_at 从时间戳字段列表中排除,TP 会在新增/更新时自动给 deleted_at 写入当前时间——刚插入的数据立刻变成“已删除”,后续所有查询都查不到。
关键点:
-
deleted_at不参与自动写入,它只应在delete()调用时由软删逻辑赋值 - 确保
$createTime和$updateTime里不包含deleted_at - 不要在
$type或$dateFormat里强行把deleted_at设为'datetime'并开启自动写入 - 检查
destroy()是否生效:打印 SQL 或查数据库,确认写入的是deleted_at值,而不是NULL或旧时间
最易被忽略的一点:软删除不是开关式功能,它是字段名、字段类型、模型配置、查询拦截四者咬合的机制。任意一环错位,表现都是“删了查不到”或“删了还在”,但问题根源往往不在 delete() 调用本身,而在建表或模型初始化阶段就埋下了。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











