format('Y-m-d'); Db::transaction(static function () use ($diagnosisId, $today, $newContent, $adminId): void { self::appendLocked($diagnosisId, $today, $newContent, $adminId); }); return true; } catch (\Exception $e) { self::setError($e->getMessage()); return false; } } /** * Explicit historical-date append for trusted application services. * * Caller MUST authorize diagnosis + daily-record scope and hold an active * transaction. This method never starts/commits one: task, note and audit writes * must roll back together. The diagnosis lock serializes the missing-row case * with legacy appends; the note lock protects read-modify-write. * * @return int Actual tracking_note id (existing row or newly inserted row). * @throws \DomainException Invalid inputs; database exceptions propagate. */ public static function appendForDate(int $diagnosisId, string $noteDate, string $content, int $actorId): int { if ($diagnosisId <= 0 || $actorId <= 0) { throw new \DomainException('诊单或操作人缺失'); } $content = trim($content); if ($content === '') { throw new \DomainException('备注内容不能为空'); } return self::appendLocked($diagnosisId, self::normalizeNoteDate($noteDate), $content, $actorId); } private static function normalizeNoteDate(string $value): string { $value = trim($value); $zone = new \DateTimeZone('Asia/Shanghai'); $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $value, $zone); if (!preg_match('/^[1-9]\d{3}-\d{2}-\d{2}$/D', $value) || !$date || $date->format('Y-m-d') !== $value) { throw new \DomainException('跟踪备注日期无效,请使用 YYYY-MM-DD'); } if ($value > (new \DateTimeImmutable('now', $zone))->format('Y-m-d')) { throw new \DomainException('跟踪备注日期不能晚于今天'); } return $value; } private static function appendLocked(int $diagnosisId, string $noteDate, string $content, int $actorId): int { $diagnosis = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->lock(true)->find(); if (!$diagnosis) { throw new \DomainException('诊单不存在'); } $existing = TrackingNote::where('diagnosis_id', $diagnosisId) ->where('note_date', $noteDate) ->lock(true) ->find(); // The table's diagnosis/date unique key includes soft-deleted rows. // Never resurrect a deleted note or silently replace its historic content. if ($existing && $existing->delete_time !== null) { throw new \DomainException('当日跟踪备注已删除,请先核实'); } $time = (new \DateTimeImmutable('now', new \DateTimeZone('Asia/Shanghai')))->format('H:i'); $line = "[{$time}] {$content}"; if ($existing) { $previous = trim((string) ($existing->content ?? '')); $existing->content = $previous !== '' ? $previous . "\n" . $line : $line; $existing->save(); return (int) $existing->id; } $record = TrackingNote::create([ 'diagnosis_id' => $diagnosisId, 'admin_id' => $actorId, 'note_date' => $noteDate, 'content' => $line, ]); return (int) $record->id; } /** * 按 diagnosis_id 获取跟踪备注列表(note_date DESC) * * @param int $diagnosisId * @param int $limit 最近 N 天 * @return array */ public static function getByDiagnosis(int $diagnosisId, int $limit = 60): array { try { if ($diagnosisId <= 0) { return []; } $records = TrackingNote::where('diagnosis_id', $diagnosisId) ->whereNull('delete_time') ->order('note_date', 'desc') ->limit($limit) ->select() ->toArray(); return $records; } catch (\Exception $e) { self::setError($e->getMessage()); return []; } } }