where('note_date', $today) ->whereNull('delete_time') ->find(); $newContent = trim($params['content'] ?? ''); $newImages = self::normalizeNewAttachmentPaths($params['tongue_images'] ?? []); $newReports = self::normalizeNewAttachmentPaths($params['report_files'] ?? []); if ($existing) { $data = []; if ($newContent !== '') { $prev = trim($existing->content ?? ''); $line = "[{$time}] {$newContent}"; $data['content'] = $prev !== '' ? ($prev . "\n" . $line) : $line; } if (!empty($newImages)) { $prev = self::parseJsonArray($existing->tongue_images); $merged = array_values(array_unique(array_merge($prev, $newImages))); $data['tongue_images'] = json_encode($merged, JSON_UNESCAPED_UNICODE); } if (!empty($newReports)) { $prev = self::parseJsonArray($existing->report_files); $merged = array_values(array_unique(array_merge($prev, $newReports))); $data['report_files'] = json_encode($merged, JSON_UNESCAPED_UNICODE); } if (!empty($data)) { $existing->save($data); } } else { $content = $newContent !== '' ? "[{$time}] {$newContent}" : ''; DoctorNote::create([ 'diagnosis_id' => $diagnosisId, 'doctor_id' => $doctorId, 'note_date' => $today, 'content' => $content, 'tongue_images' => !empty($newImages) ? json_encode($newImages, JSON_UNESCAPED_UNICODE) : null, 'report_files' => !empty($newReports) ? json_encode($newReports, JSON_UNESCAPED_UNICODE) : null, ]); } return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * 按 diagnosis_id 获取备注列表(note_date DESC) */ public static function getByDiagnosis(int $diagnosisId, int $limit = 30): array { try { if ($diagnosisId <= 0) { return []; } $records = DoctorNote::where('diagnosis_id', $diagnosisId) ->whereNull('delete_time') ->order('note_date', 'desc') ->limit($limit) ->select() ->toArray(); foreach ($records as &$record) { $record['tongue_images'] = array_map(function ($url) { return empty($url) ? $url : FileService::getFileUrl($url); }, self::parseJsonArray($record['tongue_images'] ?? [])); $record['report_files'] = array_map(function ($url) { return empty($url) ? $url : FileService::getFileUrl($url); }, self::parseJsonArray($record['report_files'] ?? [])); } return $records; } catch (\Exception $e) { self::setError($e->getMessage()); return []; } } /** * 删除备注中的单张图片 */ public static function deleteImage(int $noteId, string $imageType, string $imagePath): bool { try { $note = DoctorNote::where('id', $noteId)->whereNull('delete_time')->find(); if (!$note) { self::setError('记录不存在'); return false; } if (!in_array($imageType, ['tongue_images', 'report_files'])) { self::setError('类型无效'); return false; } $images = self::parseJsonArray($note->$imageType); // 统一转为相对路径再匹配 $targetPath = self::toRelativePath($imagePath); $images = array_values(array_filter($images, fn($url) => self::toRelativePath($url) !== $targetPath)); $note->$imageType = empty($images) ? null : json_encode($images, JSON_UNESCAPED_UNICODE); $note->save(); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * 聚合某诊单所有备注中的图片(供 DiagnosisLogic::detail 使用) */ public static function getAggregatedImages(int $diagnosisId): array { $records = DoctorNote::where('diagnosis_id', $diagnosisId) ->whereNull('delete_time') ->select(); $tongueImages = []; $reportFiles = []; foreach ($records as $r) { $tongueImages = array_merge($tongueImages, self::parseJsonArray($r->tongue_images)); $reportFiles = array_merge($reportFiles, self::parseJsonArray($r->report_files)); } return [ 'tongue_images' => array_map( fn($u) => empty($u) ? $u : FileService::getFileUrl($u), array_values(array_unique($tongueImages)) ), 'report_files' => array_map( fn($u) => empty($u) ? $u : FileService::getFileUrl($u), array_values(array_unique($reportFiles)) ), ]; } /** * 如果 URL 的域名是当前配置的存储域名则去掉,否则原样保留 */ private static function toRelativePath(string $url): string { $url = trim($url); if ($url === '') return $url; $urlParts = parse_url($url); if (!is_array($urlParts) || empty($urlParts['scheme'])) { return $url; } $scheme = strtolower((string) $urlParts['scheme']); if (!in_array($scheme, ['http', 'https'], true)) { return $url; } // 获取当前存储域名 $domain = rtrim(self::getStorageDomain(), '/'); $domainParts = $domain !== '' ? parse_url($domain) : false; if (is_array($domainParts)) { $domainScheme = strtolower((string) ($domainParts['scheme'] ?? '')); $urlHost = strtolower(rtrim((string) ($urlParts['host'] ?? ''), '.')); $domainHost = strtolower(rtrim((string) ($domainParts['host'] ?? ''), '.')); $urlPort = (int) ($urlParts['port'] ?? ($scheme === 'https' ? 443 : 80)); $domainPort = (int) ( $domainParts['port'] ?? ($domainScheme === 'https' ? 443 : 80) ); $urlPath = (string) ($urlParts['path'] ?? ''); $domainPath = rtrim((string) ($domainParts['path'] ?? ''), '/'); $pathInsideDomain = $domainPath === '' || $urlPath === $domainPath || str_starts_with($urlPath, $domainPath . '/'); if ( $domainScheme === $scheme && $domainHost !== '' && $domainHost === $urlHost && $domainPort === $urlPort && $pathInsideDomain ) { $relative = $domainPath === '' ? $urlPath : substr($urlPath, strlen($domainPath)); if (isset($urlParts['query']) && $urlParts['query'] !== '') { $relative .= '?' . $urlParts['query']; } return ltrim($relative, '/'); } } // 非当前存储域名,保留完整 URL return $url; } private static function getStorageDomain(): string { $default = \app\common\service\ConfigService::get('storage', 'default', 'local'); if ($default === 'local') { return request()->domain() . '/'; } $storage = \app\common\service\ConfigService::get('storage', $default); return $storage ? ($storage['domain'] ?? '') : ''; } /** * 备注附件只接受站内相对路径或当前存储域已上传的 URL。 * 存储域 URL 先转为相对路径,避免将任意外部 URL 持久化到病例页。 * * @param mixed $value * @return array */ private static function normalizeNewAttachmentPaths($value): array { $paths = []; foreach (self::parseJsonArray($value) as $rawPath) { $path = trim((string) $rawPath); if ($path === '') { continue; } $path = self::toRelativePath($path); $scheme = parse_url($path, PHP_URL_SCHEME); if ( (is_string($scheme) && $scheme !== '') || str_starts_with($path, '//') || str_contains($path, "\0") ) { throw new \InvalidArgumentException('备注附件必须来自当前文件存储域'); } $paths[] = $path; } return array_values(array_unique($paths)); } private static function parseJsonArray($value): array { if (is_array($value)) return $value; if (is_string($value)) { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } return []; } }