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