272 lines
9.9 KiB
PHP
Executable File
272 lines
9.9 KiB
PHP
Executable File
<?php
|
||
|
||
namespace app\adminapi\logic\doctor;
|
||
|
||
use app\common\logic\BaseLogic;
|
||
use app\common\model\doctor\DoctorNote;
|
||
use app\common\service\FileService;
|
||
|
||
class DoctorNoteLogic extends BaseLogic
|
||
{
|
||
/**
|
||
* 按 diagnosis_id + 当天 find-or-create,追加 content / tongue_images / report_files
|
||
*/
|
||
public static function addOrAppend(array $params): bool
|
||
{
|
||
try {
|
||
$diagnosisId = (int) $params['diagnosis_id'];
|
||
$doctorId = (int) ($params['doctor_id'] ?? 0);
|
||
$today = date('Y-m-d');
|
||
$time = date('H:i');
|
||
|
||
$existing = DoctorNote::where('diagnosis_id', $diagnosisId)
|
||
->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<int, string>
|
||
*/
|
||
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 [];
|
||
}
|
||
}
|