Files
zyt/server/tests/DiagnosisAssistantNameTest.php
2026-11-06 12:01:31 +08:00

262 lines
11 KiB
PHP

<?php
declare(strict_types=1);
// Exercise the real lists/count methods with synthetic in-memory rows only.
require dirname(__DIR__) . '/vendor/autoload.php';
final class AssistantNameQuery
{
public static array $tables = [];
public static array $adminReads = [];
private array $conditions = [];
private ?array $fields = null;
private int $offset = 0;
private ?int $length = null;
public function __construct(private string $table) {}
public function where($field, $operator = null, $value = null): self
{
if (is_array($field)) {
foreach ($field as $condition) {
$this->where(...$condition);
}
return $this;
}
if (func_num_args() === 2) {
$value = $operator;
$operator = '=';
}
if ($operator === 'in') {
return $this->whereIn($field, $value);
}
if ($operator !== '=') {
throw new RuntimeException('Unexpected fixture operator: ' . $operator);
}
$this->conditions[] = [$field, '=', $value];
return $this;
}
public function whereIn(string $field, array $values): self
{
$this->conditions[] = [$field, 'in', $values];
return $this;
}
public function whereNull(string $field): self
{
$this->conditions[] = [$field, 'null', null];
return $this;
}
public function whereRaw(string $sql): self
{
if (!in_array($sql, ['0 = 1', '(assistant_id IS NULL OR assistant_id = 0)'], true)) {
throw new RuntimeException('Unexpected fixture SQL: ' . $sql);
}
$this->conditions[] = ['', 'raw', $sql];
return $this;
}
public function with(array $relations): self { return $this; }
public function append(array $fields): self { return $this; }
public function orderRaw(string $order): self { return $this; }
public function order(string $field, string $direction): self { return $this; }
public function group(string $field): self { return $this; }
public function getTableFields(): array { return []; }
public function field($fields): self
{
$this->fields = is_array($fields) ? $fields : array_map('trim', explode(',', $fields));
return $this;
}
public function limit(int $offset, int $length): self
{
$this->offset = $offset;
$this->length = $length;
return $this;
}
private function rows(): array
{
$rows = array_values(array_filter(self::$tables[$this->table] ?? [], function (array $row): bool {
foreach ($this->conditions as [$field, $operator, $value]) {
$actual = $row[$field] ?? null;
if (($operator === '=' && $actual != $value)
|| ($operator === 'in' && !in_array($actual, $value))
|| ($operator === 'null' && $actual !== null)
|| ($operator === 'raw' && ($value === '0 = 1' || ($row['assistant_id'] ?? 0) != 0))) {
return false;
}
}
return true;
}));
return array_slice($rows, $this->offset, $this->length);
}
public function select(): think\Collection
{
if ($this->table === 'admin') {
throw new RuntimeException('Assistant enrichment must not read full admin records');
}
$rows = $this->rows();
if ($this->fields !== null) {
$rows = array_map(fn (array $row): array => array_intersect_key($row, array_flip($this->fields)), $rows);
}
return new think\Collection($rows);
}
public function column(string $field, string $key = ''): array
{
if ($this->table === 'admin') {
self::$adminReads[] = ['conditions' => $this->conditions, 'field' => $field, 'key' => $key];
}
return array_column($this->rows(), $field, $key !== '' ? $key : null);
}
public function count(): int { return count($this->rows()); }
}
class AssistantNameModel
{
protected const TABLE = 'empty';
public function getTable(): string { return static::TABLE; }
public static function where(...$args): AssistantNameQuery
{
return (new AssistantNameQuery(static::TABLE))->where(...$args);
}
public static function whereIn(...$args): AssistantNameQuery
{
return (new AssistantNameQuery(static::TABLE))->whereIn(...$args);
}
}
final class AssistantNameDiagnosis extends AssistantNameModel { protected const TABLE = 'tcm_diagnosis'; }
final class AssistantNameAdmin extends AssistantNameModel { protected const TABLE = 'admin'; }
final class AssistantNameRoles extends AssistantNameModel { protected const TABLE = 'admin_role'; }
final class AssistantNameAppointment extends AssistantNameModel { protected const TABLE = 'doctor_appointment'; }
final class AssistantNameDb
{
public static function name(string $table): AssistantNameQuery { return new AssistantNameQuery($table); }
}
final class AssistantNameRevisitDue
{
public static function buildListPayload(...$args): array { return []; }
public static function emptyPayload(): array { return []; }
}
class_alias(AssistantNameDiagnosis::class, 'app\\common\\model\\tcm\\Diagnosis');
class_alias(AssistantNameAdmin::class, 'app\\common\\model\\auth\\Admin');
class_alias(AssistantNameRoles::class, 'app\\common\\model\\auth\\AdminRole');
class_alias(AssistantNameAppointment::class, 'app\\common\\model\\doctor\\Appointment');
foreach (['tcm\\Prescription', 'tcm\\CallRecord', 'tcm\\BloodRecord', 'tcm\\DietRecord',
'tcm\\ExerciseRecord', 'dict\\DictData'] as $model) {
class_alias(AssistantNameModel::class, 'app\\common\\model\\' . $model);
}
class_alias(AssistantNameDb::class, 'think\\facade\\Db');
class_alias(AssistantNameRevisitDue::class, 'app\\common\\service\\tcm\\RevisitDueService');
final class AssistantNameLists extends app\adminapi\lists\tcm\DiagnosisLists
{
public function __construct(private ?array $visibleIds = null, array $params = [], int $offset = 0,
int $length = 100, int $actor = 91)
{
$this->params = $params;
$this->adminId = $actor;
$this->adminInfo = ['admin_id' => $actor];
$this->searchWhere = [];
$this->limitOffset = $offset;
$this->limitLength = $length;
}
protected function dataScopeShouldApply(): bool { return true; }
protected function getDataScopeVisibleAdminIds(): ?array { return $this->visibleIds; }
}
$assertions = 0;
function assistantNameExpect(bool $ok, string $message): void
{
global $assertions;
if (!$ok) {
throw new RuntimeException('FAIL: ' . $message);
}
$assertions++;
}
// Input order is already the normal no-appointment descending-id order.
AssistantNameQuery::$tables = [
'tcm_diagnosis' => [
['id' => 109, 'assistant_id' => 41],
['id' => 108, 'assistant_id' => '42'],
['id' => 107, 'assistant_id' => 41],
['id' => 106, 'assistant_id' => null],
['id' => 105, 'assistant_id' => 0],
['id' => 104, 'assistant_id' => 404],
['id' => 103, 'assistant_id' => 43],
['id' => 102, 'assistant_id' => 44],
['id' => 101, 'assistant_id' => -1],
],
'admin' => [
['id' => 41, 'name' => ' 林医助 ', 'delete_time' => null, 'disable' => 0, 'account' => 'private-41'],
['id' => 42, 'name' => '王医助', 'delete_time' => null, 'disable' => 1, 'account' => 'private-42'],
['id' => 43, 'name' => '已删除医助', 'delete_time' => 123, 'disable' => 0],
['id' => 44, 'name' => ' ', 'delete_time' => null, 'disable' => 0],
['id' => 45, 'name' => '未关联人员', 'delete_time' => null, 'disable' => 0],
],
];
$lists = new AssistantNameLists();
$rows = $lists->lists();
assistantNameExpect(array_column($rows, 'assistant_name', 'id') === [
109 => '林医助', 108 => '王医助', 107 => '林医助', 106 => '', 105 => '',
104 => '', 103 => '', 102 => '', 101 => '',
], 'each row gets the assigned name; unassigned, missing, deleted, blank and invalid assignments remain empty');
assistantNameExpect(AssistantNameQuery::$adminReads === [[
'conditions' => [['id', 'in', [41, 42, 404, 43, 44]], ['delete_time', 'null', null]],
'field' => 'name', 'key' => 'id',
]], 'one batch reads only positive unique assigned ids and the name/id projection');
assistantNameExpect($rows[1]['assistant_id'] === '42' && $rows[1]['assistant_name'] === '王医助',
'disabled but still assigned staff retain their name without changing the original id');
assistantNameExpect(!str_contains(json_encode($rows, JSON_UNESCAPED_UNICODE), 'private-')
&& !str_contains(json_encode($rows, JSON_UNESCAPED_UNICODE), '未关联人员'),
'enrichment exposes neither admin account fields nor unrelated names');
assistantNameExpect($lists->count() === count($rows), 'enrichment does not change total count');
AssistantNameQuery::$adminReads = [];
$scoped = new AssistantNameLists([41]);
$rows = $scoped->lists();
assistantNameExpect(array_column($rows, 'id') === [109, 107] && $scoped->count() === 2,
'real data-scope filtering remains active for lists and count');
assistantNameExpect(AssistantNameQuery::$adminReads[0]['conditions'][0] === ['id', 'in', [41]],
'names outside the authorized rows are never queried');
AssistantNameQuery::$adminReads = [];
$page = new AssistantNameLists(null, [], 1, 1);
$rows = $page->lists();
assistantNameExpect(array_column($rows, 'id') === [108] && $rows[0]['assistant_name'] === '王医助'
&& $page->count() === 9, 'pagination and count are preserved');
assistantNameExpect(AssistantNameQuery::$adminReads[0]['conditions'][0] === ['id', 'in', [42]],
'only names on the requested page are queried');
AssistantNameQuery::$tables['admin_role'] = [['admin_id' => 41, 'role_id' => 2]];
AssistantNameQuery::$adminReads = [];
$self = new AssistantNameLists(null, [], 0, 100, 41);
assistantNameExpect(array_column($self->lists(), 'id') === [109, 107] && $self->count() === 2,
'assistant-role self filtering is unchanged');
assistantNameExpect(AssistantNameQuery::$adminReads[0]['conditions'][0] === ['id', 'in', [41]],
'assistant-role name hydration uses the self-filtered result only');
AssistantNameQuery::$tables['admin_role'] = [];
foreach ([new AssistantNameLists([]), new AssistantNameLists(null, [], 50, 10)] as $empty) {
AssistantNameQuery::$adminReads = [];
assistantNameExpect($empty->lists() === [] && AssistantNameQuery::$adminReads === [],
'empty authorization scope or an empty page does not query admin data');
}
AssistantNameQuery::$adminReads = [];
$pending = new AssistantNameLists(null, ['pending_assign' => 1]);
$rows = $pending->lists();
assistantNameExpect(array_column($rows, 'id') === [106, 105] && $pending->count() === 2
&& array_column($rows, 'assistant_name') === ['', ''] && AssistantNameQuery::$adminReads === [],
'unassigned-only lists have an explicit empty name and need no admin query');
echo "Diagnosis assistant names: {$assertions} assertions passed (no DB or network)\n";