136 lines
6.0 KiB
PHP
136 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\adminapi\service;
|
|
|
|
/** 医生网页面诊迁移。客户端识别用于迁移兼容,原有登录和数据权限仍须独立校验。 */
|
|
final class DoctorWebConsultationPolicy
|
|
{
|
|
public static function cutoffTimestamp(?array $configuration = null): int
|
|
{
|
|
return self::schedule($configuration)['cutoff']->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<string, \DateTimeImmutable> */
|
|
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;
|
|
}
|
|
}
|