164 lines
6.1 KiB
PHP
164 lines
6.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
use app\adminapi\logic\tcm\DiagnosisAiLogic;
|
|
|
|
final class DiagnosisAiPatientOptionsAuthCacheDouble
|
|
{
|
|
/** @var array<int,string> */
|
|
public static array $uris = [];
|
|
|
|
public function __construct(int $adminId = 0)
|
|
{
|
|
}
|
|
|
|
/** @return array<int,string> */
|
|
public function getAdminUri(): array
|
|
{
|
|
return self::$uris;
|
|
}
|
|
}
|
|
|
|
function patientOptionsExpect(bool $condition, string $message): void
|
|
{
|
|
if (!$condition) {
|
|
fwrite(STDERR, "FAIL: {$message}\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
patientOptionsExpect(
|
|
class_alias(DiagnosisAiPatientOptionsAuthCacheDouble::class, 'app\\common\\cache\\AdminAuthCache'),
|
|
'permission cache double is installed before logic autoload'
|
|
);
|
|
|
|
$reflection = new ReflectionClass(DiagnosisAiLogic::class);
|
|
$source = file_get_contents($reflection->getFileName());
|
|
patientOptionsExpect(is_string($source), 'logic source is readable');
|
|
|
|
$hasPermission = $reflection->getMethod('hasPermission');
|
|
DiagnosisAiPatientOptionsAuthCacheDouble::$uris = ['tcm.diagnosis/aiAssistant'];
|
|
patientOptionsExpect(
|
|
$hasPermission->invoke(null, 7, ['root' => 0], 'tcm.diagnosis/aiassistant') === true,
|
|
'roles with aiAssistant pass the explicit logic permission check'
|
|
);
|
|
DiagnosisAiPatientOptionsAuthCacheDouble::$uris = ['tcm.diagnosis/aiReports'];
|
|
patientOptionsExpect(
|
|
$hasPermission->invoke(null, 7, ['root' => 0], 'tcm.diagnosis/aiassistant') === false,
|
|
'adjacent AI permissions do not grant patient options access'
|
|
);
|
|
patientOptionsExpect(
|
|
$hasPermission->invoke(null, 1, ['root' => 1], 'tcm.diagnosis/aiassistant') === true,
|
|
'root permission compatibility is preserved'
|
|
);
|
|
|
|
$normalize = $reflection->getMethod('normalizePatientOptionsParams');
|
|
$defaults = $normalize->invoke(null, []);
|
|
patientOptionsExpect(
|
|
$defaults === ['keyword' => '', 'page_no' => 1, 'page_size' => 20],
|
|
'pagination defaults are stable'
|
|
);
|
|
$bounded = $normalize->invoke(null, ['keyword' => ' 张三 ', 'page_no' => -8, 'page_size' => 500]);
|
|
patientOptionsExpect(
|
|
$bounded === ['keyword' => '张三', 'page_no' => 1, 'page_size' => 50],
|
|
'keyword is trimmed and pagination is bounded'
|
|
);
|
|
patientOptionsExpect(
|
|
$normalize->invoke(null, ['keyword' => str_repeat('患', 65)]) === null,
|
|
'keywords longer than 64 characters fail validation'
|
|
);
|
|
|
|
$format = $reflection->getMethod('formatPatientOptionRow');
|
|
$dto = $format->invoke(null, [
|
|
'diagnosis_id' => 18,
|
|
'source_patient_id' => 77,
|
|
'patient_name' => '张三',
|
|
'gender' => 1,
|
|
'age' => 42,
|
|
'phone_value' => '13812345678',
|
|
'id_card' => '11010519491231002X',
|
|
'diagnosis_date' => 1787155200,
|
|
'diagnosis_summary' => '气阴两虚',
|
|
'last_visit_at' => '2026-08-18 09:30:00',
|
|
'next_appointment_at' => '2026-08-22 14:00:00',
|
|
]);
|
|
patientOptionsExpect(
|
|
array_keys($dto) === [
|
|
'diagnosis_id',
|
|
'source_patient_id',
|
|
'patient_name',
|
|
'gender',
|
|
'age',
|
|
'phone_masked',
|
|
'diagnosis_date',
|
|
'diagnosis_summary',
|
|
'last_visit_at',
|
|
'next_appointment_at',
|
|
],
|
|
'diagnosis option DTO exposes only the minimal allowlist'
|
|
);
|
|
patientOptionsExpect($dto['phone_masked'] === '138****5678', 'phone is masked');
|
|
patientOptionsExpect($dto['diagnosis_summary'] === '气阴两虚', 'safe diagnosis summary is retained');
|
|
$encodedDto = json_encode($dto, JSON_UNESCAPED_UNICODE);
|
|
patientOptionsExpect(is_string($encodedDto), 'DTO is JSON encodable');
|
|
patientOptionsExpect(!str_contains($encodedDto, '13812345678'), 'plain phone is absent');
|
|
patientOptionsExpect(!str_contains($encodedDto, '11010519491231002X'), 'ID card is absent');
|
|
|
|
$patientOptions = $reflection->getMethod('patientOptions');
|
|
$sourceLines = file($reflection->getFileName());
|
|
$methodSource = is_array($sourceLines) ? implode('', array_slice(
|
|
$sourceLines,
|
|
$patientOptions->getStartLine() - 1,
|
|
$patientOptions->getEndLine() - $patientOptions->getStartLine() + 1
|
|
)) : '';
|
|
patientOptionsExpect(
|
|
str_contains($methodSource, 'self::hasPermission($adminId, $adminInfo, self::PERMISSION_ASSISTANT)'),
|
|
'endpoint explicitly checks the existing AI assistant permission'
|
|
);
|
|
patientOptionsExpect(
|
|
str_contains($methodSource, 'MyPatientLogic::applyScope($query, $adminId, $adminInfo)'),
|
|
'query applies the shared patient data scope'
|
|
);
|
|
patientOptionsExpect(
|
|
str_contains($methodSource, "->where('d.status', 1)")
|
|
&& str_contains($methodSource, "->whereNull('d.delete_time')"),
|
|
'query only exposes enabled, non-deleted diagnoses'
|
|
);
|
|
patientOptionsExpect(
|
|
str_contains($methodSource, 'patient_option_apt.status = 3')
|
|
&& str_contains($methodSource, 'patient_option_apt.status = 1')
|
|
&& str_contains($methodSource, "->order('d.id', 'desc')"),
|
|
'appointment summaries and deterministic diagnosis ordering are present'
|
|
);
|
|
patientOptionsExpect(
|
|
substr_count($methodSource, '->select()') === 1,
|
|
'page data is fetched in one query without row-level lookups'
|
|
);
|
|
|
|
$controller = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/tcm/DiagnosisController.php');
|
|
patientOptionsExpect(is_string($controller), 'controller source is readable');
|
|
patientOptionsExpect(
|
|
str_contains($controller, 'public function aiPatientOptions()')
|
|
&& str_contains($controller, 'DiagnosisAiLogic::patientOptions('),
|
|
'GET controller action delegates to the scoped logic'
|
|
);
|
|
|
|
$migration = file_get_contents(
|
|
dirname(__DIR__) . '/database/migrations/2026_08_20_diagnosis_ai_patient_options_permission.sql'
|
|
);
|
|
patientOptionsExpect(is_string($migration), 'permission migration is readable');
|
|
patientOptionsExpect(
|
|
substr_count($migration, "WHERE `perms` = 'tcm.diagnosis/aiPatientOptions'") >= 2,
|
|
'endpoint permission registration is idempotent and addressable'
|
|
);
|
|
patientOptionsExpect(
|
|
str_contains($migration, "`menu`.`perms` = 'tcm.diagnosis/aiAssistant'")
|
|
&& str_contains($migration, 'INSERT IGNORE INTO `zyt_system_role_menu`'),
|
|
'roles owning aiAssistant inherit the endpoint permission idempotently'
|
|
);
|
|
|
|
echo "Diagnosis AI patient options contract: OK\n";
|