where('id', $adminId)->whereNull('delete_time') ->where('disable', 0)->lock($lock)->find(); if ($adminId <= 0 || !$row) { throw new DomainException('账号已停用或登录状态失效'); } $roles = Db::name('admin_role')->where('admin_id', $adminId)->column('role_id'); $info = ['admin_id' => $adminId, 'id' => $adminId, 'root' => (int) $row['root'], 'role_id' => $roles]; if ($info['root'] !== 1) { $menuIds = $roles ? Db::name('system_role_menu')->whereIn('role_id', $roles)->column('menu_id') : []; $allowed = $menuIds && Db::name('system_menu')->whereIn('id', $menuIds) ->where('is_disable', 0)->where('perms', self::PAGE_PERMISSION)->count() > 0; if (!$allowed) { throw new DomainException('权限不足,无法访问我的患者录音关联'); } } return $info; } public static function bind(array $params, int $adminId): array { $p = self::bindingInput($params); $garbage = []; $result = Db::transaction(static function () use ($p, $adminId, &$garbage): array { // Serialize mutations for this actor, including the first INSERT for an absent key. $info = self::actor($adminId, true); $existing = self::ownedQuery($adminId, $p)->lock(true)->find(); $ids = array_unique([$p['diagnosis_id'], (int) ($existing['diagnosis_id'] ?? $p['diagnosis_id'])]); sort($ids); $patients = []; foreach ($ids as $id) { $patients[$id] = self::diagnosis($id, $adminId, $info, true); } $now = time(); if ($existing) { $id = (int) $existing['id']; if ((int) $existing['diagnosis_id'] !== $p['diagnosis_id']) { $garbage = MobileRecordingMediaLogic::bindingMoved($id, $p['diagnosis_id']); } self::ownedQuery($adminId, $p)->where('id', $id)->update($p + ['updated_at' => $now]); $row = array_merge($existing, $p, ['updated_at' => $now]); } else { $row = $p + ['admin_id' => $adminId, 'created_at' => $now, 'updated_at' => $now]; $row['id'] = (int) Db::name('mobile_recording_binding')->insertGetId($row); } return ['binding' => self::deviceRow($row, $patients[$p['diagnosis_id']])]; }); MobileRecordingStorage::cleanup($garbage); return $result; } public static function unbind(array $params, int $adminId): array { self::only($params, ['installation_id', 'recording_key']); $identity = self::identity($params); $garbage = []; $result = Db::transaction(static function () use ($identity, $adminId, &$garbage): array { $info = self::actor($adminId, true); $row = self::ownedQuery($adminId, $identity)->lock(true)->find(); if ($row) { self::diagnosis((int) $row['diagnosis_id'], $adminId, $info, true); $garbage = MobileRecordingMediaLogic::bindingRemoved((int) $row['id']); self::ownedQuery($adminId, $identity)->where('id', (int) $row['id'])->delete(); } return ['unbound' => true]; }); MobileRecordingStorage::cleanup($garbage); return $result; } public static function deviceLists(array $params, int $adminId): array { self::only($params, ['installation_id', 'page_no', 'page_size']); $installationId = self::installation($params['installation_id'] ?? null); [$page, $size] = self::pagination($params, 50); $info = self::actor($adminId); $query = Db::name('mobile_recording_binding')->alias('b') ->join([(new Diagnosis())->getTable() => 'd'], 'd.id = b.diagnosis_id') ->where('b.admin_id', $adminId)->where('b.installation_id', $installationId) ->where('d.status', 1)->whereNull('d.delete_time'); MyPatientLogic::applyScope($query, $adminId, $info); $count = (int) (clone $query)->count('b.id'); $rows = $query->field('b.*,d.patient_id AS source_patient_id,d.patient_name,d.phone AS patient_phone') ->order('b.id', 'desc')->page($page, $size)->select()->toArray(); $items = array_map(static fn (array $row): array => self::deviceRow($row, [ 'id' => $row['diagnosis_id'], 'patient_id' => $row['source_patient_id'], 'patient_name' => $row['patient_name'], 'phone' => $row['patient_phone'], ]), $rows); return ['lists' => $items, 'count' => $count, 'page_no' => $page, 'page_size' => $size]; } public static function patientLists(array $params, int $adminId): array { self::only($params, ['diagnosis_id', 'page_no', 'page_size']); $diagnosisId = self::integer($params['diagnosis_id'] ?? null, 1, 4294967295, '诊单标识'); [$page, $size] = self::pagination($params, 20); $info = self::actor($adminId); self::diagnosis($diagnosisId, $adminId, $info); $query = Db::name('mobile_recording_binding')->alias('b')->where('b.diagnosis_id', $diagnosisId); $count = (int) (clone $query)->count(); $adminTable = Db::name('admin')->getTable(); $rows = $query->leftJoin([$adminTable => 'a'], 'a.id = b.admin_id') ->field('b.id,b.diagnosis_id,b.source_platform,b.recorded_at,b.duration_ms,b.created_at,b.updated_at,a.name AS bound_by') ->order('b.recorded_at', 'desc')->order('b.id', 'desc')->page($page, $size)->select()->toArray(); $media = MobileRecordingMediaLogic::summaries(array_column($rows, 'id')); $items = array_map(static function (array $row) use ($media): array { $dto = self::metadata($row); $dto['bound_by'] = trim((string) ($row['bound_by'] ?? '')) ?: '已停用账号'; $dto += $media[$row['id']]; $dto['playback_location'] = $dto['audio_available'] ? 'server' : 'original_device'; return $dto; }, $rows); return ['lists' => $items, 'count' => $count, 'page_no' => $page, 'page_size' => $size]; } /** Public for the controller and executable input-contract tests. */ public static function bindingInput(array $params): array { self::only($params, ['installation_id', 'recording_key', 'diagnosis_id', 'source_platform', 'recorded_at', 'duration_ms']); if (($params['source_platform'] ?? null) !== 'android_bcr') { throw new DomainException('录音来源无效'); } return self::identity($params) + [ 'diagnosis_id' => self::integer($params['diagnosis_id'] ?? null, 1, 4294967295, '诊单标识'), 'source_platform' => 'android_bcr', 'recorded_at' => self::integer($params['recorded_at'] ?? null, 0, (time() + 300) * 1000, '录音时间'), 'duration_ms' => self::integer($params['duration_ms'] ?? null, 0, 604800000, '录音时长'), ]; } private static function ownedQuery(int $adminId, array $identity): Query { return Db::name('mobile_recording_binding')->where('admin_id', $adminId) ->where('installation_id', $identity['installation_id'])->where('recording_key', $identity['recording_key']); } public static function diagnosis(int $id, int $adminId, array $info, bool $lock = false): array { $row = Db::name('tcm_diagnosis')->where('id', $id)->whereNull('delete_time')->where('status', 1) ->lock($lock)->find(); if (!$row || !MyPatientLogic::canAccessDiagnosis($id, $adminId, $info)) { throw new DomainException('患者不存在或无权操作'); } return $row; } private static function deviceRow(array $row, array $patient): array { return self::metadata($row) + [ 'installation_id' => (string) $row['installation_id'], 'recording_key' => (string) $row['recording_key'], 'patient' => [ 'diagnosis_id' => (int) $patient['id'], 'source_patient_id' => (int) ($patient['patient_id'] ?? 0), 'patient_name' => (string) ($patient['patient_name'] ?? ''), 'phone_masked' => self::maskPhone((string) ($patient['phone'] ?? '')), ], ]; } private static function metadata(array $row): array { $result = ['source_platform' => (string) $row['source_platform']]; foreach (['id', 'diagnosis_id', 'recorded_at', 'duration_ms', 'created_at', 'updated_at'] as $field) { $result[$field] = (int) $row[$field]; } return $result; } private static function maskPhone(string $phone): string { $digits = preg_replace('/\D+/', '', $phone) ?? ''; $length = strlen($digits); if ($length <= 4) { return str_repeat('*', $length); } if ($length < 8) { return substr($digits, 0, 1) . str_repeat('*', $length - 2) . substr($digits, -1); } return substr($digits, 0, 3) . str_repeat('*', $length - 7) . substr($digits, -4); } public static function identity(array $params): array { $key = $params['recording_key'] ?? null; if (!is_string($key) || !preg_match('/^[0-9a-f]{64}$/D', $key)) { throw new DomainException('录音标识无效'); } return ['installation_id' => self::installation($params['installation_id'] ?? null), 'recording_key' => $key]; } private static function installation($value): string { if (!is_string($value) || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/D', $value)) { throw new DomainException('设备标识无效'); } return $value; } private static function pagination(array $params, int $defaultSize): array { return [ self::integer($params['page_no'] ?? 1, 1, 1000000, '页码'), self::integer($params['page_size'] ?? $defaultSize, 1, 100, '每页数量'), ]; } public static function integer($value, int $min, int $max, string $label): int { if (!(is_int($value) || is_string($value)) || !preg_match('/^(0|[1-9][0-9]{0,14})$/D', (string) $value) || (int) $value < $min || (int) $value > $max) { throw new DomainException($label . '无效'); } return (int) $value; } public static function only(array $params, array $allowed): void { if (array_diff(array_keys($params), $allowed) !== []) { throw new DomainException('请求包含不支持的字段'); } } }