feat: add reviewed follow-up audio patient enrichment
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace think\facade {
|
||||
// In-memory transaction/row-lock fixture: no application bootstrap or database.
|
||||
class Db
|
||||
{
|
||||
public static int $depth = 0;
|
||||
public static int $calls = 0;
|
||||
public static function transaction(callable $callback)
|
||||
{
|
||||
self::$calls++;
|
||||
$snapshot = \app\common\model\tcm\TrackingNote::$rows;
|
||||
self::$depth++;
|
||||
try { return $callback(); }
|
||||
catch (\Throwable $error) { \app\common\model\tcm\TrackingNote::$rows = $snapshot; throw $error; }
|
||||
finally { self::$depth--; }
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace app\common\model\tcm {
|
||||
class FixtureQuery
|
||||
{
|
||||
public static array $events = [];
|
||||
public static bool $enforceLocks = false;
|
||||
private string $table;
|
||||
private array $where = [];
|
||||
private bool $locked = false;
|
||||
public function __construct(string $table) { $this->table = $table; }
|
||||
public function where(string $key, $value): self { $this->where[$key] = $value; return $this; }
|
||||
public function whereNull(string $key): self { return $this->where($key, null); }
|
||||
public function lock(bool $value): self { $this->locked = $value; return $this; }
|
||||
public function find()
|
||||
{
|
||||
self::$events[] = ['table' => $this->table, 'where' => $this->where, 'locked' => $this->locked];
|
||||
if (self::$enforceLocks && (!$this->locked || \think\facade\Db::$depth < 1)) {
|
||||
throw new \RuntimeException('write read must lock inside caller transaction');
|
||||
}
|
||||
if ($this->table === 'diagnosis') return ($this->where['id'] ?? 0) === 7 ? (object) ['id' => 7] : null;
|
||||
foreach (TrackingNote::$rows as $row) {
|
||||
$matches = true;
|
||||
foreach ($this->where as $key => $value) { if (($row[$key] ?? null) !== $value) $matches = false; }
|
||||
if ($matches) return new FixtureRow($row);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
class FixtureRow
|
||||
{
|
||||
public function __construct(private array $data) {}
|
||||
public function __get(string $key) { return $this->data[$key] ?? null; }
|
||||
public function __isset(string $key): bool { return isset($this->data[$key]); }
|
||||
public function __set(string $key, $value): void { $this->data[$key] = $value; }
|
||||
public function save(): void { TrackingNote::$rows[$this->data['id']] = $this->data; }
|
||||
}
|
||||
class Diagnosis
|
||||
{
|
||||
public static function where(string $key, $value): FixtureQuery { return (new FixtureQuery('diagnosis'))->where($key, $value); }
|
||||
}
|
||||
class TrackingNote
|
||||
{
|
||||
public static array $rows = [];
|
||||
public static function where(string $key, $value): FixtureQuery { return (new FixtureQuery('tracking_note'))->where($key, $value); }
|
||||
public static function create(array $data): FixtureRow
|
||||
{
|
||||
$data['id'] = count(self::$rows) + 1;
|
||||
$data['delete_time'] = null;
|
||||
self::$rows[$data['id']] = $data;
|
||||
return new FixtureRow($data);
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace {
|
||||
use app\adminapi\logic\tcm\TrackingNoteLogic;
|
||||
use app\common\model\tcm\TrackingNote;
|
||||
use app\common\model\tcm\FixtureQuery;
|
||||
use think\facade\Db;
|
||||
|
||||
require dirname(__DIR__) . '/app/common/logic/BaseLogic.php';
|
||||
$sourceFlag = array_search('--source', $argv, true);
|
||||
require $sourceFlag === false ? dirname(__DIR__) . '/app/adminapi/logic/tcm/TrackingNoteLogic.php' : $argv[$sourceFlag + 1];
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
function check(bool $condition, string $message): void { if (!$condition) throw new RuntimeException($message); }
|
||||
function rejects(callable $fn, string $message): void
|
||||
{
|
||||
try { $fn(); } catch (DomainException $expected) { return; }
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
$today = (new DateTimeImmutable('now', new DateTimeZone('Asia/Shanghai')))->format('Y-m-d');
|
||||
check(TrackingNoteLogic::addOrAppend(['diagnosis_id' => 7, 'admin_id' => 9, 'content' => '今日合成备注', 'note_date' => '2000-01-01']), 'legacy call remains accepted');
|
||||
$legacy = reset(TrackingNote::$rows);
|
||||
check($legacy['note_date'] === $today, 'legacy entry point still ignores submitted historical date');
|
||||
if (in_array('--observe', $argv, true)) {
|
||||
echo json_encode(['legacyTodayOnly' => $legacy['note_date'] === $today, 'explicitHistoricalDateSupported' => method_exists(TrackingNoteLogic::class, 'appendForDate')], JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
exit(0);
|
||||
}
|
||||
check(method_exists(TrackingNoteLogic::class, 'appendForDate'), 'explicit historical API exists');
|
||||
TrackingNote::$rows = [];
|
||||
FixtureQuery::$events = [];
|
||||
FixtureQuery::$enforceLocks = true;
|
||||
$ownTransactions = Db::$calls;
|
||||
$id = Db::transaction(static fn() => TrackingNoteLogic::appendForDate(7, ' 2024-02-29 ', ' 第一条合成历史备注 ', 9));
|
||||
check(Db::$calls === $ownTransactions + 1, 'new method does not open or commit a nested transaction');
|
||||
check($id === 1, 'actual created id returned');
|
||||
check(TrackingNote::$rows[$id]['note_date'] === '2024-02-29', 'valid leap-day date normalized explicitly');
|
||||
check(preg_match('/^\[\d{2}:\d{2}\] 第一条合成历史备注$/u', TrackingNote::$rows[$id]['content']) === 1, 'trimmed content has insertion-time prefix');
|
||||
$again = Db::transaction(static fn() => TrackingNoteLogic::appendForDate(7, '2024-02-29', '第二条合成历史备注', 10));
|
||||
check($again === $id && count(TrackingNote::$rows) === 1, 'same day appends instead of replacing or creating second note');
|
||||
check(substr_count(TrackingNote::$rows[$id]['content'], "\n") === 1, 'two original lines preserved');
|
||||
check(str_contains(TrackingNote::$rows[$id]['content'], '第一条合成历史备注'), 'first content retained');
|
||||
check(TrackingNote::$rows[$id]['admin_id'] === 9, 'original creator retained; caller audit owns latest actor');
|
||||
check(array_column(FixtureQuery::$events, 'table') === ['diagnosis', 'tracking_note', 'diagnosis', 'tracking_note'], 'diagnosis lock precedes exact-day row lock for both insert/update');
|
||||
check(FixtureQuery::$events[1]['where'] === ['diagnosis_id' => 7, 'note_date' => '2024-02-29'], 'date and diagnosis scoped lookup includes deleted rows explicitly');
|
||||
|
||||
$before = TrackingNote::$rows;
|
||||
try {
|
||||
Db::transaction(static function (): void {
|
||||
TrackingNoteLogic::appendForDate(7, '2024-02-29', '必须随调用者回滚', 9);
|
||||
throw new RuntimeException('synthetic audit insert failure');
|
||||
});
|
||||
} catch (RuntimeException $error) { check($error->getMessage() === 'synthetic audit insert failure', 'expected synthetic rollback'); }
|
||||
check(TrackingNote::$rows === $before, 'caller rollback restores note alongside task/audit transaction');
|
||||
$tomorrow = (new DateTimeImmutable('tomorrow', new DateTimeZone('Asia/Shanghai')))->format('Y-m-d');
|
||||
foreach (['', '2025-02-29', '2024-02-30', '2024-2-01', '2024/02/01', '0000-01-01', '2024-01-01 08:00:00', $tomorrow] as $invalid) {
|
||||
rejects(static fn() => TrackingNoteLogic::appendForDate(7, $invalid, '合成备注', 9), 'invalid date rejected: ' . $invalid);
|
||||
}
|
||||
rejects(static fn() => TrackingNoteLogic::appendForDate(0, '2024-01-01', '内容', 9), 'missing diagnosis rejected');
|
||||
rejects(static fn() => TrackingNoteLogic::appendForDate(7, '2024-01-01', '内容', 0), 'missing actor rejected');
|
||||
rejects(static fn() => TrackingNoteLogic::appendForDate(7, '2024-01-01', ' ', 9), 'blank content rejected');
|
||||
rejects(static fn() => Db::transaction(static fn() => TrackingNoteLogic::appendForDate(999, '2024-01-01', '内容', 9)), 'missing diagnosis row rejected');
|
||||
TrackingNote::$rows[$id]['delete_time'] = 123;
|
||||
rejects(static fn() => Db::transaction(static fn() => TrackingNoteLogic::appendForDate(7, '2024-02-29', '不能复活已删除备注', 9)), 'deleted note cannot be resurrected');
|
||||
check(TrackingNote::$rows[$id]['delete_time'] === 123, 'deleted state retained');
|
||||
TrackingNote::$rows = [];
|
||||
FixtureQuery::$events = [];
|
||||
check(TrackingNoteLogic::addOrAppend(['diagnosis_id' => 7, 'content' => '旧接口仍可追加']), 'legacy zero-actor semantics preserved');
|
||||
check(reset(TrackingNote::$rows)['note_date'] === $today, 'legacy remains today only');
|
||||
check(FixtureQuery::$events[0]['table'] === 'diagnosis' && FixtureQuery::$events[0]['locked'], 'legacy shares serialization lock with new service');
|
||||
check(!TrackingNoteLogic::addOrAppend(['diagnosis_id' => 0, 'content' => '无效']), 'legacy invalid ID returns false');
|
||||
check(!TrackingNoteLogic::addOrAppend(['diagnosis_id' => 7, 'content' => '']), 'legacy blank content returns false');
|
||||
echo "FollowupAudioTrackingNoteBehaviorTest: historical date/leap-day validation, caller transaction rollback, lock order, append-only identity, deleted guard, legacy today semantics: PASS\n";
|
||||
}
|
||||
Reference in New Issue
Block a user