
本文详解在 laravel 中如何安全、准确地查询记录是否处于指定时间范围内,重点指出直接使用数据库 `time` 类型字段进行跨日或时区敏感场景下的严重缺陷,并推荐采用 `datetime` 或 `timestamp` 字段配合 `wheretime` 与逻辑组合的正确实践。
在 Laravel 应用中,常需判断当前时刻是否落在某条记录定义的时间窗口内(例如:用户设置的短信通知时段 start_time 到 end_time)。但如原始代码所示,若数据库字段类型为 time(仅存储 H:i:s),将面临根本性设计缺陷:
- ❌ time 类型无法表达日期上下文,无法处理跨零点场景(如 22:00:00 → 03:00:00);
- ❌ 时区转换极易出错(Asia/Karachi 与数据库服务器时区不一致时,whereTime() 的比较结果不可靠);
- ❌ whereTime('start_time', '>=', $time) 和 whereTime('end_time', '逻辑错误:这实际要求 $time 同时 ≥ 开始且 ≤ 结束——看似合理,但若时间范围跨日(如 23:00–02:00),该条件永远为假。
✅ 正确方案:改用 datetime 类型存储带日期的时间点,并将“每日生效时段”建模为相对周期逻辑。
✅ 推荐迁移方案(兼顾语义与健壮性)
首先,更新数据库结构(强烈建议弃用 time 字段):
// 修改迁移文件(新增 datetime 字段,保留兼容性可先添加再逐步迁移)
Schema::table('settings', function (Blueprint $table) {
$table->datetime('start_datetime')->nullable()->after('start_time');
$table->datetime('end_datetime')->nullable()->after('end_time');
// 可选:后续删除旧 time 字段
// $table->dropColumn(['start_time', 'end_time']);
});接着,在模型中定义访问器,将“每日固定时段”动态映射为当天的 datetime 范围:
// app/Models/Setting.php
class Setting extends Model
{
protected $casts = [
'start_datetime' => 'datetime',
'end_datetime' => 'datetime',
];
// 获取今日生效的 datetime 范围(自动适配当前日期)
public function getTodayTimeRangeAttribute(): array
{
$now = now('Asia/Karachi'); // 显式指定业务时区
$today = $now->toDateString();
$start = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$today} {$this->start_time}", 'Asia/Karachi');
$end = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$today} {$this->end_time}", 'Asia/Karachi');
// 处理跨日逻辑:若 end_time < start_time,视为次日
if ($end->lessThan($start)) {
$end = $end->addDay();
}
return [$start, $end];
}
}✅ 查询逻辑重构(安全、可读、可维护)
use Illuminate\Support\Facades\DB;
$nowInKarachi = now('Asia/Karachi');
$currentTime = $nowInKarachi->format('H:i:s');
$users_with_crypto_only_sms_alert = User::whereNotNull('device_key')
->where('subscription_package', 'LIKE', '%Morpheus%')
->where('role', 'user')
->where('sms_alerts', 1)
->where('is_email', 0)
->where('is_blocked', 0)
->where('mobile_no', '!=', '')
->whereHas('setting', function ($q) use ($currentTime) {
$q->where(function ($sub) use ($currentTime) {
// 场景1:正常区间(start <= end)
$sub->whereRaw('? BETWEEN start_time AND end_time', [$currentTime])
->orWhere(function ($inner) use ($currentTime) {
// 场景2:跨日区间(end < start),如 23:00–02:00
$inner->where('start_time', '>', 'end_time')
->whereRaw("(? >= start_time OR ? <= end_time)", [$currentTime, $currentTime]);
});
});
})
->pluck('device_key')
->all();? 关键说明: 使用 whereRaw 避免 ORM 对 time 类型的隐式转换风险; BETWEEN 自动处理 start_time
⚠️ 重要注意事项
- 绝不依赖数据库本地时区:始终在 PHP 层显式指定业务时区(如 now('Asia/Karachi')),并在数据库连接配置中设置 timezone 为 +05:00 或保持 UTC 并在应用层转换;
- 避免 whereTime 连用 >= 和 :这是常见逻辑陷阱,应改用 BETWEEN 或 whereRaw 明确表达区间关系;
-
生产环境务必索引:为 start_time 和 end_time 字段添加复合索引提升查询性能:
$table->index(['start_time', 'end_time']);
通过以上重构,您将获得一个时区安全、跨日鲁棒、易于测试且符合 Laravel 最佳实践的时间范围查询方案。记住:数据类型的合理性,永远是业务逻辑正确性的第一道防线。










