需用闭包替代内置_unique规则,在db查询中显式添加软删除条件(如where('delete_time', 'is null')),因验证器不感知模型的软删除全局作用域。

软删除后 _unique 验证仍报重复怎么办
ThinkPHP 的 _unique 验证规则默认不感知软删除字段(如 delete_time),查数据库时会把已“删掉”的记录也纳入比对,导致新建同名记录时误判为重复。
根本原因是验证器底层调用的 Db::name()->where()->count() 没自动排除软删除数据。你得手动干预查询条件。
- 在验证规则里用闭包写完整查询逻辑,而不是依赖内置
_unique - 确保闭包中显式加上
['delete_time', '=', null]或['delete_time', 'is null'] - 注意:如果模型开启了全局作用域(
use SoftDelete),验证器不走模型,所以全局作用域无效
用闭包替代 _unique 实现带软删除的唯一校验
直接在验证规则数组里把 'field|name' => 'unique:table,column' 换成闭包,控制查询行为。
示例(验证用户手机号唯一,且忽略已软删除的):
return [
'mobile|手机号' => [
'require',
function($value, $data) {
$count = \think\facade\Db::name('user')
->where('mobile', $value)
->where('delete_time', 'is null')
->count();
return $count === 0;
}
]
];
- 闭包参数
$data是整个提交数据,可用于关联字段判断(比如“同一部门下岗位名唯一”) - 别用
where('delete_time', null)—— MySQL 中= NULL永远为 false,必须用is null - 如果表用的是时间戳软删除(非 null 值而是 0 或默认时间),条件要改成
['delete_time', '=', 0]或对应值
模型场景下用 withTrashed() 反向处理更危险
有人想“既然软删除被过滤了,那我主动拉出所有记录再筛选”,于是用 UserModel::withTrashed()->where(...)->count()。这反而容易出错。
-
withTrashed()是模型方法,验证器默认不加载模型,硬调可能触发未初始化异常 - 即使能跑通,也会查出大量无意义的已删除记录,性能差,且和业务语义冲突(你本意是“只看有效数据”)
- 更隐蔽的问题:如果软删除字段类型是
datetime,但数据库存的是'0000-00-00 00:00:00',is null判定失效,得统一用['delete_time', 'eq', null]或具体值判断
验证器复用时如何动态传表名和软删除字段
多个模型都要做类似校验(比如 category、tag),别每个都写死表名和字段。把软删除条件抽成配置项更稳妥。
示例:封装一个可复用的闭包生成器
function uniqueWithSoftDelete($table, $column, $softDeleteField = 'delete_time') {
return function($value, $data) use ($table, $column, $softDeleteField) {
return \think\facade\Db::name($table)
->where($column, $value)
->where($softDeleteField, 'is null')
->count() === 0;
};
}
// 使用
'mobile' => [uniqueWithSoftDelete('user', 'mobile')]
- 闭包里用
use绑定变量,避免外部污染 - 软删除字段名不一定是
delete_time,有些项目叫is_deleted或status,传参覆盖更安全 - 这种写法绕过了验证器的字符串规则解析,所以不能和
require|unique:user,mobile混用,必须全用闭包
SoftDelete trait,也不管你的全局查询范围写了啥。所有过滤逻辑,得自己一行行写进闭包里。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











