where('type', $type)->first(); return [ 'type' => $type, 'auto_enabled' => (int) ($row->auto_enabled ?? 0), 'grant_time' => $row->grant_time ?? '03:00', 'timezone' => self::TIMEZONE, ]; } public static function save(string $type, array $params): array { self::checkType($type); // Validate here so HTTP and other callers share the same write rules. $params = validator($params, [ 'auto_enabled' => ['required', 'boolean'], 'grant_time' => [Rule::requiredIf(fn () => in_array($params['auto_enabled'] ?? null, [1, '1', true], true)), 'nullable', 'date_format:H:i'], ])->validate(); return DB::transaction(function () use ($type, $params) { $values = ['type' => $type, 'auto_enabled' => (bool) $params['auto_enabled'], 'created_at' => now(), 'updated_at' => now()]; $updates = ['auto_enabled', 'updated_at']; if (!empty($params['grant_time'])) { $values['grant_time'] = $params['grant_time']; $updates[] = 'grant_time'; } DB::table('reward_grant_settings')->upsert([$values], ['type'], $updates); return self::get($type); }); } public static function dueAt(?object $setting, CarbonInterface $now): ?CarbonInterface { if (!$setting || (int) $setting->auto_enabled !== 1 || !preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/D', (string) $setting->grant_time)) { return null; } $local = $now->copy()->setTimezone(self::TIMEZONE); [$hour, $minute] = array_map('intval', explode(':', $setting->grant_time)); $scheduled = $local->copy()->setTime($hour, $minute, 0); return $local->greaterThanOrEqualTo($scheduled) ? $scheduled : null; } private static function checkType(string $type): void { if (!in_array($type, self::TYPES, true)) throw new InvalidArgumentException('奖励类型错误'); } }