@@ -20,6 +20,8 @@ return [
|
||||
app\adminapi\http\middleware\LoginMiddleware::class,
|
||||
// 权限认证
|
||||
app\adminapi\http\middleware\AuthMiddleware::class,
|
||||
// 医生网页面诊到期限制(须在登录及权限校验之后执行)
|
||||
app\adminapi\http\middleware\DoctorWebConsultationMiddleware::class,
|
||||
// 演示模式 - 禁止提交数据
|
||||
app\adminapi\http\middleware\CheckDemoMiddleware::class,
|
||||
// 演示模式 - 不返回敏感数据
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace app\adminapi\controller;
|
||||
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\adminapi\logic\ConfigLogic;
|
||||
use app\adminapi\service\DoctorWebConsultationPolicy;
|
||||
|
||||
/**
|
||||
* 配置控制器
|
||||
@@ -24,7 +25,13 @@ use app\adminapi\logic\ConfigLogic;
|
||||
*/
|
||||
class ConfigController extends BaseAdminController
|
||||
{
|
||||
public array $notNeedLogin = ['getConfig', 'dict'];
|
||||
public array $notNeedLogin = ['getConfig', 'dict', 'doctorWebConsultation'];
|
||||
|
||||
/** 医生网页面诊迁移时间,只读且免登录,供已打开的网页同步配置。 */
|
||||
public function doctorWebConsultation()
|
||||
{
|
||||
return $this->data(DoctorWebConsultationPolicy::publicPolicy());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -58,4 +65,4 @@ class ConfigController extends BaseAdminController
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,13 @@ use app\adminapi\validate\setting\DesktopWorkstationValidate;
|
||||
*/
|
||||
class DesktopWorkstationController extends BaseAdminController
|
||||
{
|
||||
public array $notNeedLogin = ['check'];
|
||||
public array $notNeedLogin = ['check', 'downloads'];
|
||||
|
||||
/** @notes 提供已发布的各平台安装包,医生无需系统设置权限即可下载 */
|
||||
public function downloads()
|
||||
{
|
||||
return $this->data(DesktopWorkstationLogic::downloads());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 读取升级策略与安装包
|
||||
|
||||
@@ -429,13 +429,17 @@ class DiagnosisController extends BaseAdminController
|
||||
*/
|
||||
public function getCallRecords()
|
||||
{
|
||||
$params = $this->request->get();
|
||||
|
||||
if (empty($params['diagnosis_id'])) {
|
||||
return $this->fail('诊单ID不能为空');
|
||||
$rawDiagnosisId = $this->request->get('diagnosis_id', 0);
|
||||
$diagnosisId = is_int($rawDiagnosisId) || is_string($rawDiagnosisId)
|
||||
? filter_var($rawDiagnosisId, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]])
|
||||
: false;
|
||||
if ($diagnosisId === false) {
|
||||
return $this->fail('诊单ID无效');
|
||||
}
|
||||
|
||||
$result = DiagnosisLogic::getCallRecords($params);
|
||||
if (!DiagnosisLogic::canViewReadonlyDiagnosis($diagnosisId, (int) $this->adminId, $this->adminInfo)) {
|
||||
return $this->fail(DiagnosisLogic::getError() ?: '诊单不存在或无权访问');
|
||||
}
|
||||
$result = DiagnosisLogic::getCallRecords(['diagnosis_id' => $diagnosisId]);
|
||||
return $this->data($result);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,16 @@ class AuthMiddleware
|
||||
// 当前访问路径
|
||||
$accessUri = strtolower($request->controller() . '/' . $request->action());
|
||||
|
||||
// 面诊回放与文字共用读取权限;未登记该接口时也不能走历史放行分支。
|
||||
if ($accessUri === 'tcm.diagnosis/getcallrecords') {
|
||||
$uris = $this->formatUrl($adminAuthCache->getAdminUri() ?? []);
|
||||
if (in_array('tcm.diagnosis/huifang', $uris, true)
|
||||
|| in_array('tcm.diagnosis/getcallrecords', $uris, true)) {
|
||||
return $next($request); // 控制器还会校验当前诊单的数据域。
|
||||
}
|
||||
return JsonService::fail('权限不足,无法查看面诊记录');
|
||||
}
|
||||
|
||||
// This group must never use the legacy unregistered-route bypass.
|
||||
if (str_starts_with($accessUri, 'tcm.followupaudio/')) {
|
||||
$allowed = ['capabilities', 'uploadsession', 'uploadchunk', 'uploadcomplete',
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\http\middleware;
|
||||
|
||||
use app\adminapi\service\DoctorWebConsultationPolicy;
|
||||
use app\common\service\JsonService;
|
||||
|
||||
final class DoctorWebConsultationMiddleware
|
||||
{
|
||||
public function __construct(private ?array $configuration = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle($request, \Closure $next)
|
||||
{
|
||||
if (DoctorWebConsultationPolicy::blocksRequest(
|
||||
(array) ($request->adminInfo ?? []),
|
||||
(string) $request->controller(),
|
||||
(string) $request->action(),
|
||||
(array) $request->header(),
|
||||
null,
|
||||
$this->configuration
|
||||
)) {
|
||||
return JsonService::fail(DoctorWebConsultationPolicy::message($this->configuration), [
|
||||
'reason' => 'doctor_web_consultation_expired',
|
||||
'cutoff_at' => DoctorWebConsultationPolicy::cutoffAt($this->configuration),
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace app\adminapi\lists\order;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\adminapi\service\order\PaymentPrescriptionOrderService;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\lists\Traits\HasDataScopeFilter;
|
||||
use app\common\model\Order;
|
||||
@@ -218,12 +219,20 @@ class OrderLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
|
||||
$query = Order::where($where);
|
||||
$this->applyDataScopeByOwner($query, 'creator_id');
|
||||
return $query
|
||||
$rows = $query
|
||||
->with(['patient', 'creator'])
|
||||
->order(['create_time' => 'desc'])
|
||||
->limit($this->limitOffset, $this->pageSize)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$prescriptionOrders = PaymentPrescriptionOrderService::forPaymentIds(array_column($rows, 'id'));
|
||||
foreach ($rows as &$row) {
|
||||
$row['prescription_orders'] = $prescriptionOrders[(int) $row['id']] ?? [];
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
namespace app\adminapi\logic\auth;
|
||||
|
||||
use app\adminapi\logic\LoginLogic;
|
||||
use app\adminapi\service\DoctorWebConsultationPolicy;
|
||||
use app\common\cache\AdminAuthCache;
|
||||
use app\common\enum\YesNoEnum;
|
||||
use app\common\logic\BaseLogic;
|
||||
@@ -318,6 +319,7 @@ class AdminLogic extends BaseLogic
|
||||
|
||||
$authRoleIds = AdminRole::where('admin_id', $params['id'])->column('role_id');
|
||||
$admin['role_ids'] = array_values(array_map('intval', $authRoleIds));
|
||||
$admin['web_consultation_policy'] = DoctorWebConsultationPolicy::publicPolicy();
|
||||
|
||||
$admin['need_bind_work_wechat'] = LoginLogic::adminMustBindWorkWechat([
|
||||
'root' => (int) ($admin['root'] ?? 0),
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\logic\order;
|
||||
|
||||
use app\adminapi\service\order\PaymentPrescriptionOrderService;
|
||||
use app\common\model\Order;
|
||||
use app\common\model\OrderDetail;
|
||||
use app\common\model\auth\Admin;
|
||||
@@ -776,7 +777,11 @@ class OrderLogic
|
||||
return null;
|
||||
}
|
||||
|
||||
return $order->toArray();
|
||||
$detail = $order->toArray();
|
||||
$prescriptionOrders = PaymentPrescriptionOrderService::forPaymentIds([$id]);
|
||||
$detail['prescription_orders'] = $prescriptionOrders[$id] ?? [];
|
||||
|
||||
return $detail;
|
||||
}
|
||||
|
||||
private static $error = '';
|
||||
|
||||
@@ -37,6 +37,36 @@ class DesktopWorkstationLogic extends BaseLogic
|
||||
public static function getConfig(): array
|
||||
{
|
||||
return self::present(self::loadStored());
|
||||
}
|
||||
|
||||
/** 医生公告使用的安装包清单,与自动升级检测开关独立。 */
|
||||
public static function downloads(): array
|
||||
{
|
||||
return self::downloadCatalog(self::getConfig());
|
||||
}
|
||||
|
||||
public static function downloadCatalog(array $config): array
|
||||
{
|
||||
$labels = [
|
||||
'windows_x64' => 'Windows 64位',
|
||||
'macos_arm64' => 'macOS(Apple芯片)',
|
||||
'macos_x64' => 'macOS(Intel芯片)',
|
||||
];
|
||||
$packages = [];
|
||||
foreach (self::PLATFORMS as $platform) {
|
||||
$package = self::plainPackage((array) ($config['packages'][$platform] ?? []), $platform);
|
||||
$url = parse_url($package['url']);
|
||||
if (!is_array($url) || !in_array(strtolower($url['scheme'] ?? ''), ['https', 'http'], true)
|
||||
|| empty($url['host']) || isset($url['user']) || isset($url['pass'])
|
||||
|| !preg_match('/^[a-f0-9]{64}$/', $package['sha256'])) {
|
||||
continue;
|
||||
}
|
||||
$packages[] = ['platform' => $platform, 'label' => $labels[$platform]] + $package;
|
||||
}
|
||||
return [
|
||||
'version' => self::normalizeVersion((string) ($config['latest_version'] ?? '')),
|
||||
'packages' => $packages,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\service\order;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 支付单反查处方业务订单。调用方先按原有权限和分页取得支付单,再批量补充关联。
|
||||
*/
|
||||
class PaymentPrescriptionOrderService
|
||||
{
|
||||
/**
|
||||
* @param int[] $paymentIds
|
||||
* @return array<int, list<array{id: int, order_no: string}>>
|
||||
*/
|
||||
public static function forPaymentIds(array $paymentIds): array
|
||||
{
|
||||
$paymentIds = array_values(array_unique(array_filter(
|
||||
array_map('intval', $paymentIds),
|
||||
static fn (int $id): bool => $id > 0
|
||||
)));
|
||||
if ($paymentIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = array_fill_keys($paymentIds, []);
|
||||
// 历史 linked_pay_order_id 已由迁移写入关联表;不能用旧字段恢复已移除或已拆分的关联。
|
||||
// 不按履约/支付状态筛选,保留仍有关联的取消、退款订单历史。
|
||||
$rows = Db::name('tcm_prescription_order_pay_order')->alias('link')
|
||||
->join('tcm_prescription_order po', 'po.id = link.prescription_order_id')
|
||||
->whereIn('link.pay_order_id', $paymentIds)
|
||||
->whereNull('po.delete_time')
|
||||
->field(['link.pay_order_id', 'po.id', 'po.order_no'])
|
||||
->order('po.id', 'asc')
|
||||
->select()
|
||||
->toArray();
|
||||
foreach ($rows as $row) {
|
||||
$result[(int) $row['pay_order_id']][(int) $row['id']] = [
|
||||
'id' => (int) $row['id'],
|
||||
'order_no' => (string) $row['order_no'],
|
||||
];
|
||||
}
|
||||
foreach ($result as &$orders) {
|
||||
$orders = array_values($orders);
|
||||
}
|
||||
unset($orders);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user