ThinkPHP软删除需字段deleted_at(DATETIME/TIMESTAMP、可空)、模型启用useSoftDelete=true且避开autoWriteTimestamp冲突、查询时默认过滤已删数据,三者必须严格对齐。

ThinkPHP 的软删除不是开个开关就能用的,它依赖模型配置、数据库字段、查询逻辑三者严格对齐,漏掉任意一环都会导致数据“删了又出来”或者“根本查不到”。
软删除字段必须是 deleted_at 且类型为 DATETIME 或 TIMESTAMP
ThinkPHP(6.x)默认只识别 deleted_at 字段名,不支持自定义字段名(除非重写 SoftDelete trait 中的 $deleteTime 属性)。MySQL 中该字段不能设为 NOT NULL,否则插入时没值会报错;也不能用 INT 存时间戳——框架内部用的是 PHP 的 DateTime 对象比对,INT 类型会导致 whereNotNull('deleted_at') 永远不生效。
- 建表语句里写:
ALTER TABLE user ADD deleted_at DATETIME NULL DEFAULT NULL - 迁移文件中用:
$table->softDeletes()(Laravel 风格写法在 TP6 不可用,别抄错) - 如果已有表,改字段后务必清空
runtime/cache/,否则模型可能缓存旧结构,查不出软删除数据
use SoftDelete 必须显式引入并启用,且不能和 autoWriteTimestamp 冲突
TP6 的软删除不是靠配置驱动,而是靠 trait 注入行为。只在模型里写 use \think\model\concern\SoftDelete; 不够,还必须在模型属性中声明:useSoftDelete => true。更关键的是:如果模型同时开启了自动时间戳(protected $autoWriteTimestamp = true),而你又没给 deleted_at 单独配置写入时机,TP 会把它当成普通时间字段,在新增时就写入当前时间,导致刚插进去的数据立刻“被删”。
- 正确写法:
use think\model\concern\SoftDelete; class User extends Model { use SoftDelete; protected $useSoftDelete = true; protected $autoWriteTimestamp = true; protected $createTime = 'created_at'; protected $updateTime = 'updated_at'; // 不要在这里配 deleted_at!让它保持 null,删除时才由 delete() 方法写入 } - 错误操作:在
$type或$dateFormat里强行把deleted_at设为'datetime'并开启自动写入 —— 这会让新增/更新都污染该字段
查询时默认不包含软删除数据,恢复/强制查询需手动干预
开启软删除后,所有 User::find()、User::select()、User::where(...)->select() 默认自动追加 WHERE deleted_at IS NULL。这不是 SQL 优化,是框架层拦截,所以即使你手写原生查询(Db::table('user')->select())也不会受影响——它压根不走模型逻辑。
立即学习“PHP免费学习笔记(深入)”;
- 查包括已删除的:用
User::withTrashed()->select() - 只查已删除的:用
User::onlyTrashed()->select() - 硬删(绕过软删):用
User::destroy($id, true)或(new User())->force(true)->delete() - 注意:关联查询(如
with('posts'))不会自动继承主模型的软删状态,posts表仍按自身模型规则过滤,得单独在关联定义里加->withTrashed()
软删除真正难的不是怎么删,而是怎么让「删」和「查」在业务各处保持语义一致——比如后台列表要显示已删除项,API 接口却必须过滤,这时候靠全局配置扛不住,得在每个调用点想清楚该不该带 withTrashed。字段名、模型配置、查询意图,三者只要有一个没对齐,数据就会“消失得莫名其妙”或者“删都删不干净”。



















