getTimestamp(); } public static function cutoffAt(?array $configuration = null): string { return self::schedule($configuration)['cutoff']->format(DATE_ATOM); } public static function message(?array $configuration = null): string { return '网页端面诊已于' . self::schedule($configuration)['cutoff']->format('Y年n月j日H:i:s') . '(北京时间)停用,请医生使用桌面应用进行面诊。'; } public static function publicPolicy(?int $now = null, ?array $configuration = null): array { $schedule = self::schedule($configuration); return [ 'cutoff_at' => $schedule['cutoff']->format(DATE_ATOM), 'approaching_at' => $schedule['approaching']->format(DATE_ATOM), 'urgent_at' => $schedule['urgent']->format(DATE_ATOM), 'server_time' => $now ?? time(), ]; } public static function blocksRequest( array $adminInfo, string $controller, string $action, array $headers, ?int $now = null, ?array $configuration = null ): bool { if (strtolower($controller) !== 'tcm.diagnosis' || !in_array(strtolower($action), [ 'getcallsignature', 'startcall', 'getdoctorsignature', 'getpatientsignature', ], true) || (int) ($adminInfo['root'] ?? 0) === 1) { return false; } // 来自登录令牌缓存的角色,不能取请求参数;非 root 且含角色 1 即医生。 $roles = array_map('intval', (array) ($adminInfo['role_id'] ?? [])); if (!in_array(1, $roles, true)) { return false; } $headers = array_change_key_case($headers, CASE_LOWER); // 浏览器信号优先,防止浏览器误携带桌面标识后被放行。 foreach (['origin', 'referer', 'sec-fetch-mode', 'sec-fetch-site', 'sec-fetch-dest'] as $name) { if (trim((string) ($headers[$name] ?? '')) !== '') { return ($now ?? time()) >= self::cutoffTimestamp($configuration); } } // 两端历史上都使用 terminal=1,不能据终端编号误停旧版医生工作站。 // 新版工作站使用明确 UA,旧版保留 HTTPX 默认 UA 的兼容性。 $userAgent = trim((string) ($headers['user-agent'] ?? '')); if (preg_match('/^(?:ZytDoctorWorkstation|python-httpx)\/\d+(?:\.\d+){1,3}$/i', $userAgent) === 1) { return false; } return ($now ?? time()) >= self::cutoffTimestamp($configuration); } /** 每次读取固定配置文件,绕过 ThinkPHP 配置缓存及长驻进程中的静态日期缓存。 */ private static function configuration(): array { $path = dirname(__DIR__, 3) . '/config/doctor_web_consultation.php'; if (!is_file($path) || !is_readable($path)) { throw new \UnexpectedValueException('doctor_web_consultation 配置文件不存在或不可读'); } // 即使 PHP 设置了 opcache.validate_timestamps=0,也让本次读取使用新内容。 if (function_exists('opcache_invalidate')) { opcache_invalidate($path, true); } $configuration = require $path; if (!is_array($configuration)) { throw new \UnexpectedValueException('doctor_web_consultation 配置必须返回数组'); } return $configuration; } /** @return array */ private static function schedule(?array $configuration): array { $configuration = $configuration ?? self::configuration(); $value = $configuration['cutoff_at'] ?? null; $timezone = new \DateTimeZone('Asia/Shanghai'); $cutoff = is_string($value) && preg_match('/\A[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\z/', $value) === 1 ? \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $value, $timezone) : false; $errors = \DateTimeImmutable::getLastErrors(); if ($cutoff === false || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) || $cutoff->format('Y-m-d H:i:s') !== $value || (int) $cutoff->format('Y') < 1) { throw new \InvalidArgumentException('doctor_web_consultation.cutoff_at 必须是有效北京时间,格式为 YYYY-MM-DD HH:MM:SS'); } $hours = []; foreach (['approaching_hours', 'urgent_hours'] as $key) { $hours[$key] = $configuration[$key] ?? null; if (!is_int($hours[$key]) || $hours[$key] < 0 || $hours[$key] > intdiv(PHP_INT_MAX, 3600)) { throw new \InvalidArgumentException('doctor_web_consultation.' . $key . ' 必须是有效非负整数小时数'); } } if ($hours['approaching_hours'] < $hours['urgent_hours']) { throw new \InvalidArgumentException('doctor_web_consultation.approaching_hours 必须大于或等于 urgent_hours'); } $schedule = ['cutoff' => $cutoff]; foreach (['approaching', 'urgent'] as $phase) { $seconds = $hours[$phase . '_hours'] * 3600; if ($cutoff->getTimestamp() < PHP_INT_MIN + $seconds) { throw new \InvalidArgumentException('doctor_web_consultation.' . $phase . '_hours 超出有效日期范围'); } $date = $cutoff->setTimestamp($cutoff->getTimestamp() - $seconds); if ((int) $date->format('Y') < 1) { throw new \InvalidArgumentException('doctor_web_consultation.' . $phase . '_hours 超出有效日期范围'); } $schedule[$phase] = $date; } return $schedule; } }