getMessage() . PHP_EOL . $e->getTraceAsString()); exit(1); }); $manager = new think\DbManager(); $manager->setConfig(['default' => 'isolated', 'connections' => ['isolated' => [ 'type' => 'sqlite', 'database' => ':memory:', 'prefix' => 'test_', 'fields_strict' => true, ]]]); Container::getInstance()->instance('think\DbManager', $manager); think\Model::setDb($manager); Container::getInstance()->instance('config', new think\Config()); $checks = 0; $expect = static function (bool $ok, string $why) use (&$checks): void { if (!$ok) { throw new RuntimeException($why); } $checks++; }; $deny = static function (callable $action, string $why) use ($expect): void { try { $action(); } catch (DomainException $e) { $expect(true, $why); return; } throw new RuntimeException('Expected rejection: ' . $why); }; $todo = ['id' => 1, 'diagnosis_id' => 101, 'patient_id' => 201, 'creator_id' => 13]; $expect(Recipient::resolve($todo) === 13, 'legacy reminder works before module migration'); $schemas = [ 'admin' => 'id INTEGER PRIMARY KEY,name TEXT,root INTEGER DEFAULT 0,disable INTEGER DEFAULT 0,delete_time INTEGER NULL', 'admin_role' => 'admin_id INTEGER,role_id INTEGER', 'admin_dept' => 'admin_id INTEGER,dept_id INTEGER', 'system_role' => 'id INTEGER PRIMARY KEY,name TEXT,data_scope INTEGER,delete_time INTEGER NULL', 'system_role_menu' => 'role_id INTEGER,menu_id INTEGER', 'system_menu' => 'id INTEGER PRIMARY KEY,perms TEXT,is_disable INTEGER DEFAULT 0', 'tcm_diagnosis' => 'id INTEGER PRIMARY KEY,patient_id INTEGER,assistant_id INTEGER,status INTEGER DEFAULT 1,delete_time INTEGER NULL', 'assistant_service_action' => 'id INTEGER PRIMARY KEY,todo_id INTEGER,diagnosis_id INTEGER,job_id INTEGER,assignee_id INTEGER,business_state TEXT', 'assistant_service_job' => 'id INTEGER PRIMARY KEY,diagnosis_id INTEGER,patient_id INTEGER,payload_json TEXT', ]; foreach ($schemas as $table => $schema) { Db::execute('CREATE TABLE test_' . $table . ' (' . $schema . ')'); } Db::name('admin')->insertAll([['id' => 11, 'name' => '合成医助'], ['id' => 13, 'name' => '合成主管']]); Db::name('system_role')->insert(['id' => 2, 'name' => '医助', 'data_scope' => 4]); Db::name('admin_role')->insert(['admin_id' => 11, 'role_id' => 2]); Db::name('system_menu')->insert(['id' => 1, 'perms' => 'tcm.assistantService/view']); Db::name('system_role_menu')->insert(['role_id' => 2, 'menu_id' => 1]); Db::name('tcm_diagnosis')->insert(['id' => 101, 'patient_id' => 201, 'assistant_id' => 11]); $expect(Recipient::resolve($todo) === 13, 'ordinary reminder keeps creator after module installed'); Db::name('assistant_service_job')->insert(['id' => 5, 'diagnosis_id' => 101, 'patient_id' => 201]); Db::name('assistant_service_action')->insert(['id' => 7, 'todo_id' => 1, 'diagnosis_id' => 101, 'job_id' => 5, 'assignee_id' => 11, 'business_state' => 'pending']); $expect(Recipient::resolve($todo) === 11, 'module reminder goes to assignee rather than manager creator'); $expect(Recipient::resolve($todo + ['assignee_id' => 99]) === 11, 'caller cannot override stored responsibility'); Db::name('admin')->where('id', 11)->update(['disable' => 1]); $deny(fn () => Recipient::resolve($todo), 'disabled assignee not notified'); Db::name('admin')->where('id', 11)->update(['disable' => 0]); Db::name('system_menu')->where('id', 1)->update(['is_disable' => 1]); $deny(fn () => Recipient::resolve($todo), 'revoked module permission not notified'); Db::name('system_menu')->where('id', 1)->update(['is_disable' => 0]); Db::name('tcm_diagnosis')->where('id', 101)->update(['assistant_id' => 12]); $deny(fn () => Recipient::resolve($todo), 'transferred patient not disclosed to prior assignee'); Db::name('tcm_diagnosis')->where('id', 101)->update(['assistant_id' => 11, 'patient_id' => 202]); $deny(fn () => Recipient::resolve($todo), 'rebound patient not disclosed through old reminder'); Db::name('tcm_diagnosis')->where('id', 101)->update(['patient_id' => 201]); Db::name('assistant_service_action')->where('id', 7)->update(['business_state' => 'completed']); $deny(fn () => Recipient::resolve($todo), 'finished action does not send stale reminder'); Db::name('assistant_service_action')->where('id', 7)->update(['business_state' => 'in_progress']); $expect(Recipient::resolve($todo) === 11, 'in-progress action remains deliverable'); $deny(fn () => Recipient::resolve(array_replace($todo, ['diagnosis_id' => 102])), 'mismatched reminder link rejected'); Db::name('assistant_service_job')->where('id', 5)->delete(); $deny(fn () => Recipient::resolve($todo), 'orphaned module task does not fall back to manager'); Db::name('assistant_service_job')->insert(['id' => 5, 'diagnosis_id' => 101, 'patient_id' => 201, 'payload_json' => json_encode(['context' => ['subject_admin_id' => 11]])]); Db::name('system_role_menu')->where('role_id', 2)->delete(); $expect(Recipient::resolve($todo) === 11, 'valid target without module menu receives supervisor-assigned reminder'); Db::name('assistant_service_job')->where('id', 5)->update(['payload_json' => json_encode(['context' => ['subject_admin_id' => 12]])]); $deny(fn () => Recipient::resolve($todo), 'stored subject must match assigned target'); Db::name('assistant_service_job')->where('id', 5)->update(['payload_json' => json_encode(['context' => ['subject_admin_id' => 11]])]); Db::name('system_role')->insert(['id' => 1, 'name' => '医生', 'data_scope' => 1]); Db::name('admin_role')->where('admin_id', 11)->update(['role_id' => 1]); $deny(fn () => Recipient::resolve($todo), 'former assistant with doctor role is not a valid target recipient'); Db::name('admin_role')->where('admin_id', 11)->update(['role_id' => 2]); Db::name('tcm_diagnosis')->where('id', 101)->update(['assistant_id' => 12]); $deny(fn () => Recipient::resolve($todo), 'target reminder rechecks direct patient responsibility'); Db::name('tcm_diagnosis')->where('id', 101)->update(['assistant_id' => 11]); Db::name('system_role')->where('id', 2)->update(['delete_time' => 1]); $deny(fn () => Recipient::resolve($todo), 'deleted assistant role cannot receive target reminder'); echo 'ASSISTANT_SERVICE_REMINDER_RECIPIENT assertions=' . $checks . ' PASS real_access_orm=1 sends=0' . PHP_EOL;