Files
zyt/server/app/adminapi/service/DoctorWebConsultationPolicy.php
2026-10-22 11:02:57 +08:00

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;
}
}