Files
zyt/server/tests/MyPatientScopeMergeTest.php
Your Name b9ac9a6635 Merge branch 'master' into 11-5
# Conflicts:
#	server/public/admin/assets/.pnpm-Cy9nIqIg.js
#	server/public/admin/assets/403-DZv43rg8.js
#	server/public/admin/assets/404-BdEnY8Op.js
#	server/public/admin/assets/@tencentcloud/call-engine-js-DC0nMlpX.js
#	server/public/admin/assets/@tencentcloud/chat-BgZr4keH.js
#	server/public/admin/assets/@tencentcloud/chat-uikit-engine-D2vNRuTA.js
#	server/public/admin/assets/AppointmentRecordPanel-BLiYiAbY.js
#	server/public/admin/assets/AssignLogPanel-B37Pa478.js
#	server/public/admin/assets/AssistantWatchCallDialog-CwSHnJSG.js
#	server/public/admin/assets/BloodRecordList-Drn0jNIG.js
#	server/public/admin/assets/CaseRecordList-DOWsaiL5.js
#	server/public/admin/assets/ConsultationTranscriptPanel-fqClePUL.js
#	server/public/admin/assets/DailyMatrix-B_BuE6Wk.js
#	server/public/admin/assets/DiagnosisTodoList-BvKuWevJ.js
#	server/public/admin/assets/DietRecordList-CNcxCdHz.js
#	server/public/admin/assets/ExerciseRecordList-DcrDxj4-.js
#	server/public/admin/assets/FollowupAudioField-DwFRovp6.js
#	server/public/admin/assets/FollowupAudioPanel-161TbfEd.js
#	server/public/admin/assets/FollowupAudioReview-DUx7pf7f.js
#	server/public/admin/assets/GancaoSubmissionReconcileButton.vue_vue_type_script_setup_true_lang-8OIk4lHB.js
#	server/public/admin/assets/ImChatRecordPanel-DHff4nFV.js
#	server/public/admin/assets/MediaSourceSelect-DAiidBXd.js
#	server/public/admin/assets/MediaSourceSelect.vue_vue_type_script_setup_true_lang-BnUzDRdh.js
#	server/public/admin/assets/MessageBubble-bYTr-oyb.js
#	server/public/admin/assets/NoteTimeline-jPsAfPnk.js
#	server/public/admin/assets/OrderActionHost-CM9FpHNh.js
#	server/public/admin/assets/OrderPanel-Co_XYZDR.js
#	server/public/admin/assets/PatientCaseCard-CDC9oDgL.js
#	server/public/admin/assets/PatientInfoCard-wIayMpdX.js
#	server/public/admin/assets/PatientOrderList-C0nBgRYQ.js
#	server/public/admin/assets/PrescriptionAiBatchGenerateDialog--28G2reg.js
#	server/public/admin/assets/PrescriptionAiReportDialog-BElqcJhz.js
#	server/public/admin/assets/PrescriptionOrderDetailDrawer-C1YLaoua.js
#	server/public/admin/assets/PrescriptionOrderTimeDialog.vue_vue_type_script_setup_true_lang-zSycYEpH.js
#	server/public/admin/assets/ProgressPanel-DuOaWdPj.js
#	server/public/admin/assets/PromotionAutomationForm-CEi-wURu.js
#	server/public/admin/assets/RecordingPlaybackBlock-B1YEgQib.js
#	server/public/admin/assets/RecordingVideoPlayer-CzGfJLYq.js
#	server/public/admin/assets/SendPanel-C81B60j9.js
#	server/public/admin/assets/TrackingMatrix-Dvlry4c2.js
#	server/public/admin/assets/TrackingNoteTimeline-CcOnnAQR.js
#	server/public/admin/assets/WecomFloatingWidgetBuilder-BZ1rtSPX.js
#	server/public/admin/assets/WelcomeMessageEditor-FXBSgF_s.js
#	server/public/admin/assets/account-adjust.vue_vue_type_script_setup_true_lang-BDg3Vt-5.js
#	server/public/admin/assets/account_cost-1Kig3__5.js
#	server/public/admin/assets/add-CnMHBvcC.js
#	server/public/admin/assets/admin-mWKPRS5Z.js
#	server/public/admin/assets/ai_mcp-BdB18eax.js
#	server/public/admin/assets/appointment-BABD2Tar.js
#	server/public/admin/assets/article-B2IS97ge.js
#	server/public/admin/assets/asset-fny0UJmY.js
#	server/public/admin/assets/attr-BO203wr6.js
#	server/public/admin/assets/attr-CYxX_t_S.js
#	server/public/admin/assets/attr-setting.vue_vue_type_script_setup_true_lang-BK3AJqkU.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-C1vfB9ju.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-CA3Eqz_I.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-CISdFe5h.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-CfyBAgmS.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-LP4IWWFR.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-R-6FX7Mu.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-bgUhDqjK.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-hGjVuUay.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-hudO2m34.js
#	server/public/admin/assets/attr.vue_vue_type_script_setup_true_lang-i4BbFcAt.js
#	server/public/admin/assets/auth.vue_vue_type_script_setup_true_lang-BoZeVoZP.js
#	server/public/admin/assets/balance_details-DOLi1Dm0.js
#	server/public/admin/assets/bind-work-wechat-DFHKss6n.js
#	server/public/admin/assets/cache-nfUb4Iya.js
#	server/public/admin/assets/change-password-CeOlId8o.js
#	server/public/admin/assets/code-B18aVPXd.js
#	server/public/admin/assets/code-preview.vue_vue_type_script_setup_true_lang-DGCCjmlM.js
#	server/public/admin/assets/commission-settlement-CdEu_C9J.js
#	server/public/admin/assets/commission-settlement-CdlIY0iL.css
#	server/public/admin/assets/config-J5lzqSHX.js
#	server/public/admin/assets/consumer-BpKM0EGl.js
#	server/public/admin/assets/content-69Fk_4pj.js
#	server/public/admin/assets/content-BKD0OzuH.js
#	server/public/admin/assets/content-CBy84fnS.js
#	server/public/admin/assets/content.vue_vue_type_script_setup_true_lang-AQZfsEkF.js
#	server/public/admin/assets/content.vue_vue_type_script_setup_true_lang-BlBOVIVr.js
#	server/public/admin/assets/content.vue_vue_type_script_setup_true_lang-ByXjA9a8.js
#	server/public/admin/assets/content.vue_vue_type_script_setup_true_lang-DBQ1TxzP.js
#	server/public/admin/assets/content.vue_vue_type_script_setup_true_lang-DSdHmkxS.js
#	server/public/admin/assets/cost-edit.vue_vue_type_script_setup_true_lang-CFW87alA.js
#	server/public/admin/assets/data-table.vue_vue_type_script_setup_true_lang-DLwRo6Tk.js
#	server/public/admin/assets/dayjs-LzqDqVlD.js
#	server/public/admin/assets/decoration-DiTN-PkH.js
#	server/public/admin/assets/decoration-img-BHQwkVvP.js
#	server/public/admin/assets/default_reply-D8zCYW7U.js
#	server/public/admin/assets/department-DWsa1OjT.js
#	server/public/admin/assets/dept-tongji-eyhTfbdn.js
#	server/public/admin/assets/desktop_workstation-DjyYOet-.js
#	server/public/admin/assets/detail-BZAR9ddi.js
#	server/public/admin/assets/detail-CFyC8ImZ.js
#	server/public/admin/assets/dict-K5jXOVeu.js
#	server/public/admin/assets/doctor-67HUqvH6.js
#	server/public/admin/assets/edit-BVGUIRWC.js
#	server/public/admin/assets/edit-BYRC6YDz.css
#	server/public/admin/assets/edit-CiIgrwSx.js
#	server/public/admin/assets/edit-CtQmrXFk.js
#	server/public/admin/assets/edit-DFAdp19K.js
#	server/public/admin/assets/edit-DNccqn8z.js
#	server/public/admin/assets/edit-DjWNlPnW.js
#	server/public/admin/assets/edit-Qrv_aqRa.js
#	server/public/admin/assets/edit-vVRINxRB.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-B_oC0AEU.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-C7-_pAL8.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-CTFgk6QV.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-CX_KUDU8.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-DyP0DJmF.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-DyqPlqpu.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-KBdoyuFG.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-NbrHRZf2.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-z-MjcUR0.js
#	server/public/admin/assets/edit.vue_vue_type_script_setup_true_name_articleColumnEdit_lang-DKmZkTfn.js
#	server/public/admin/assets/environment-CypnLxrG.js
#	server/public/admin/assets/error-qleaM0eA.js
#	server/public/admin/assets/fans-BEy6y9dV.js
#	server/public/admin/assets/file-BbPuzGA_.js
#	server/public/admin/assets/file-CYwMKS77.js
#	server/public/admin/assets/filing-CD0AWXGl.js
#	server/public/admin/assets/finance-D_CR5ikx.js
#	server/public/admin/assets/first_visit-D4R0U3AW.js
#	server/public/admin/assets/follow_reply-c3sC_GVa.js
#	server/public/admin/assets/footer.vue_vue_type_script_setup_true_lang-g2gc-rGu.js
#	server/public/admin/assets/guahao-Dx7MIOXD.js
#	server/public/admin/assets/h5-DEYp6ziA.js
#	server/public/admin/assets/icon-CXrybs-g.js
#	server/public/admin/assets/im-business-message-parse-D_ibrMLq.js
#	server/public/admin/assets/index-01yWb2WC.js
#	server/public/admin/assets/index-6D3QYnU-.js
#	server/public/admin/assets/index-B1iWGyoA.js
#	server/public/admin/assets/index-B4AHD13Q.js
#	server/public/admin/assets/index-BBfgtFWE.js
#	server/public/admin/assets/index-BBw5kvK3.js
#	server/public/admin/assets/index-BDAWyLAZ.js
#	server/public/admin/assets/index-BJGIfP1w.js
#	server/public/admin/assets/index-BQUdwcn-.js
#	server/public/admin/assets/index-BSMBP3_x.js
#	server/public/admin/assets/index-BdkP3TxP.js
#	server/public/admin/assets/index-BgMeTKOG.js
#	server/public/admin/assets/index-Bmrtumah.js
#	server/public/admin/assets/index-Brsj06xV.js
#	server/public/admin/assets/index-Burmlw_C.js
#	server/public/admin/assets/index-C06c4h92.js
#	server/public/admin/assets/index-C1h5WVyC.js
#	server/public/admin/assets/index-C2Gny0kk.js
#	server/public/admin/assets/index-C5O-FG0j.js
#	server/public/admin/assets/index-C8BoF1ue.js
#	server/public/admin/assets/index-CJuWPVul.js
#	server/public/admin/assets/index-CV08lFR0.js
#	server/public/admin/assets/index-C_WKJDWz.js
#	server/public/admin/assets/index-Cew5Amf6.js
#	server/public/admin/assets/index-CkXW7n_b.js
#	server/public/admin/assets/index-CprgR4Ho.js
#	server/public/admin/assets/index-Csf0cSVI.js
#	server/public/admin/assets/index-Cux23s8_.js
#	server/public/admin/assets/index-D0fXvBxu.js
#	server/public/admin/assets/index-D8yz_vWn.js
#	server/public/admin/assets/index-DBKQxzkM.js
#	server/public/admin/assets/index-DIjSXq4q.js
#	server/public/admin/assets/index-DIqVZCk0.js
#	server/public/admin/assets/index-DNsVxejj.js
#	server/public/admin/assets/index-DPWF9bqo.js
#	server/public/admin/assets/index-DQ51fSAo.js
#	server/public/admin/assets/index-DQ9Uu3LT.js
#	server/public/admin/assets/index-DWRHbuqT.js
#	server/public/admin/assets/index-Db2Y9m7h.js
#	server/public/admin/assets/index-DcHdMI2B.js
#	server/public/admin/assets/index-DgPgE-Vl.js
#	server/public/admin/assets/index-Dr8oROcK.js
#	server/public/admin/assets/index-Ds_Mrym4.js
#	server/public/admin/assets/index-DtSyPsbW.js
#	server/public/admin/assets/index-GT7BM7PS.js
#	server/public/admin/assets/index-JZkFc8I0.js
#	server/public/admin/assets/index-hNEe3rMW.js
#	server/public/admin/assets/index-kMiV0948.js
#	server/public/admin/assets/index-qwjw9zcC.js
#	server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-BClfSqof.js
#	server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-BtlhchBa.js
#	server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-C6aeOkz-.js
#	server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-Cxgoepog.js
#	server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-Dx6SeCGU.js
#	server/public/admin/assets/index.vue_vue_type_style_index_0_lang-DFOUY-wL.js
#	server/public/admin/assets/information-D4VXaoVX.js
#	server/public/admin/assets/keyword_reply-CtUT2T_C.js
#	server/public/admin/assets/link-DAAa4-8h.js
#	server/public/admin/assets/list-B31OC0dY.js
#	server/public/admin/assets/login-D-CTVkUr.js
#	server/public/admin/assets/login_register-BiM6Wf25.js
#	server/public/admin/assets/medicine-BGJ8mZD8.js
#	server/public/admin/assets/medicine-CNGccw4f.js
#	server/public/admin/assets/menu-BhV_PotF.js
#	server/public/admin/assets/menu-CixPRDpt.js
#	server/public/admin/assets/menu-set.vue_vue_type_script_setup_true_lang-B0ghs-Pc.js
#	server/public/admin/assets/message-DWVo1Jk8.js
#	server/public/admin/assets/mobile-style.vue_vue_type_script_setup_true_lang-B9BK24HQ.js
#	server/public/admin/assets/mubiao-A1xD0JzP.js
#	server/public/admin/assets/mubiao-dept-card-DDiegAnr.js
#	server/public/admin/assets/mubiao-dept-node-Cmw8YiIy.js
#	server/public/admin/assets/oa-attr-DZLm3ufW.js
#	server/public/admin/assets/oa-menu-form-edit.vue_vue_type_script_setup_true_lang-CXz4UbLJ.js
#	server/public/admin/assets/oa-menu-form.vue_vue_type_script_setup_true_lang-BPJDNffP.js
#	server/public/admin/assets/open_setting-YVf3y2zz.js
#	server/public/admin/assets/order-Bc7o9SOH.js
#	server/public/admin/assets/overflow-DhSFQzu9.js
#	server/public/admin/assets/paiban-BY9Nnrmr.js
#	server/public/admin/assets/patient-DxJlPLVv.js
#	server/public/admin/assets/patient-call-q2qMhhFV.js
#	server/public/admin/assets/pay-DWoC89w_.js
#	server/public/admin/assets/pc-CDHcPjgN.js
#	server/public/admin/assets/perm-DaS8_T_U.js
#	server/public/admin/assets/picker-ruBeJVAv.js
#	server/public/admin/assets/picker.vue_vue_type_script_setup_true_lang-DsjIkNAZ.js
#	server/public/admin/assets/popover_input-CrckcagQ.js
#	server/public/admin/assets/post-DGDTwH-Z.js
#	server/public/admin/assets/prescription-drawer-DN1NpnaF.js
#	server/public/admin/assets/preview-BbXyDhjZ.js
#	server/public/admin/assets/preview-pc-DIFp8DvX.js
#	server/public/admin/assets/progress-BUj9Y2lB.js
#	server/public/admin/assets/protocol-Clsp_lEu.js
#	server/public/admin/assets/qywx-CxG05a7f.js
#	server/public/admin/assets/recharge_record-CymZKQ4E.js
#	server/public/admin/assets/refund-log.vue_vue_type_script_setup_true_lang-Choelpx6.js
#	server/public/admin/assets/refund_record-BLbeOAEj.js
#	server/public/admin/assets/relations-add.vue_vue_type_script_setup_true_lang-BRi9G5mf.js
#	server/public/admin/assets/role-Czcmecam.js
#	server/public/admin/assets/roster-Bh9PjuXi.js
#	server/public/admin/assets/rtc-detect-BFElJVVn.js
#	server/public/admin/assets/self_input_stats-P0WLo9tC.js
#	server/public/admin/assets/setting-CCd8WCbC.js
#	server/public/admin/assets/setup-HuwOPKIm.js
#	server/public/admin/assets/statistics-Cj3WlBni.js
#	server/public/admin/assets/stats-E_9WKdl6.js
#	server/public/admin/assets/style-DoPXLvzO.js
#	server/public/admin/assets/tabbar-DD2RRhme.js
#	server/public/admin/assets/tcm-E9u-W8Oy.js
#	server/public/admin/assets/theme-picker-NdSgC88S.js
#	server/public/admin/assets/tim-profanity-filter-plugin-BvbMOVpi.js
#	server/public/admin/assets/tim-upload-plugin-D7nWSdhF.js
#	server/public/admin/assets/tongji-yCfKN-LL.js
#	server/public/admin/assets/upload-CjtahvI1.js
#	server/public/admin/assets/useDictOptions-LZFsDBEH.js
#	server/public/admin/assets/useListTimeFilter-CUZgt-II.js
#	server/public/admin/assets/useLockFn-C2guENRT.js
#	server/public/admin/assets/useMediaSourceOptions-wCazM6uJ.js
#	server/public/admin/assets/useMenuOa-92GS9rAv.js
#	server/public/admin/assets/usePaging-DzgVFG9b.js
#	server/public/admin/assets/user-DHg9QKqg.js
#	server/public/admin/assets/weapp-C5C4Oypv.js
#	server/public/admin/assets/weapp-CgFixEn4.js
#	server/public/admin/assets/website-Dk_rtWyw.js
#	server/public/admin/assets/wx_oa-BR5bK2fX.js
#	server/public/admin/assets/yeji-BlMBwtyi.js
#	server/public/admin/assets/yeji-edit.vue_vue_type_script_setup_true_lang-AeltfJWh.js
#	server/public/admin/index.html
2026-11-12 14:21:14 +08:00

139 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
// Real authorization and ORM on synthetic in-memory SQLite; no application init, .env or network.
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
use app\adminapi\logic\firstvisit\MyPatientLogic;
use app\common\service\DataScope\DataScopeService as Scope;
use app\common\service\assistantservice\AssistantServiceAccess as Access;
use think\facade\Db;
if (!in_array('sqlite', PDO::getAvailableDrivers(), true)) {
throw new RuntimeException('In-memory SQLite required; external database fallback is prohibited');
}
$app = new think\App(sys_get_temp_dir() . '/scope-merge-no-app-' . bin2hex(random_bytes(8)));
$manager = new think\DbManager();
$manager->setConfig(['default' => 'sqlite', 'auto_timestamp' => false, 'datetime_format' => false,
'connections' => ['sqlite' => ['type' => 'sqlite', 'database' => ':memory:', 'prefix' => 'test_']]]);
$app->instance('think\DbManager', $manager);
$config = new think\Config();
$config->set(['data_scope' => ['enabled' => true, 'no_dept_fallback_self' => true,
'frontline_roles' => [1, 2], 'team_roles' => [3, 7, 8], 'exempt_roles' => []]], 'project');
$app->instance('config', $config);
foreach ([
'CREATE TABLE test_admin_role (admin_id INT, role_id INT)',
'CREATE TABLE test_system_role (id INT PRIMARY KEY, name TEXT, data_scope INT, delete_time INT NULL)',
'CREATE TABLE test_admin_dept (admin_id INT, dept_id INT)',
'CREATE TABLE test_dept (id INT PRIMARY KEY, pid INT, name TEXT, delete_time INT NULL)',
'CREATE TABLE test_tcm_diagnosis (id INT PRIMARY KEY, assistant_id INT, status INT, delete_time INT NULL)',
'CREATE TABLE test_doctor_appointment (id INT PRIMARY KEY, patient_id INT, doctor_id INT, status INT)',
] as $ddl) { Db::execute($ddl); }
Db::name('system_role')->insertAll([
['id' => 1, 'name' => '医生', 'data_scope' => 1, 'delete_time' => null],
['id' => 2, 'name' => '医助', 'data_scope' => 4, 'delete_time' => null],
['id' => 3, 'name' => '管理员', 'data_scope' => 1, 'delete_time' => null],
['id' => 7, 'name' => '已删除经理', 'data_scope' => 1, 'delete_time' => 1],
['id' => 17, 'name' => '自定义主管', 'data_scope' => 3, 'delete_time' => null],
['id' => 18, 'name' => '自定义区域主管', 'data_scope' => 2, 'delete_time' => null],
['id' => 19, 'name' => '普通功能', 'data_scope' => 1, 'delete_time' => null],
]);
Db::name('dept')->insertAll([
['id' => 10, 'pid' => 0, 'name' => 'Synthetic team'],
['id' => 20, 'pid' => 10, 'name' => 'Synthetic child'],
['id' => 30, 'pid' => 0, 'name' => 'Synthetic other'],
]);
Db::name('admin_dept')->insertAll([
['admin_id' => 11, 'dept_id' => 10], ['admin_id' => 12, 'dept_id' => 10],
['admin_id' => 13, 'dept_id' => 20], ['admin_id' => 14, 'dept_id' => 30],
]);
foreach ([101 => 11, 102 => 12, 103 => 13, 104 => 14, 105 => 14, 106 => 14, 107 => 14, 108 => 14] as $id => $assistant) {
Db::name('tcm_diagnosis')->insert(['id' => $id, 'assistant_id' => $assistant, 'status' => 1]);
}
Db::name('doctor_appointment')->insertAll([
['id' => 1, 'patient_id' => 105, 'doctor_id' => 11, 'status' => 3],
['id' => 2, 'patient_id' => 106, 'doctor_id' => 12, 'status' => 1],
['id' => 3, 'patient_id' => 107, 'doctor_id' => 13, 'status' => 4],
['id' => 4, 'patient_id' => 108, 'doctor_id' => 11, 'status' => 0],
]);
$checks = 0;
$expect = static function (bool $condition, string $label) use (&$checks): void {
if (!$condition) { throw new RuntimeException($label); }
$checks++;
};
$setRoles = static function (array $roles): void {
Db::name('admin_role')->where('admin_id', 11)->delete();
foreach ($roles as $role) { Db::name('admin_role')->insert(['admin_id' => 11, 'role_id' => $role]); }
};
$actor = static fn(array $roles): array => ['id' => 11, 'admin_id' => 11, 'root' => 0, 'role_id' => $roles];
$patients = static function (array $info, ?array $currentRoles = null): array {
$query = Db::name('tcm_diagnosis')->alias('d')->whereNull('d.delete_time')->where('d.status', 1);
MyPatientLogic::applyScope($query, 11, $info, $currentRoles);
return array_map('intval', $query->order('d.id')->column('d.id'));
};
$setRoles([2]);
$expect($patients($actor([3])) === [101], 'legacy call uses live assistant role instead of cached administrator');
$expect(MyPatientLogic::scopeMeta(11, $actor([3]))['mode'] === 'self', 'scope metadata uses same live role set');
$setRoles([1, 2]);
$expect($patients($actor([1, 2])) === [101, 105], 'doctor and assistant see union of own relationships');
$expect(Scope::selfOnlyRoleIds(11, $actor([1, 2])) === [1, 2], 'legacy doctor ALL does not widen the assistant role');
$setRoles([2, 17]);
$expect($patients($actor([2, 17])) === [101, 102, 105, 106], 'custom department supervisor plus assistant sees team union');
$expect(Scope::selfOnlyRoleIds(11, $actor([2, 17])) === [], 'custom team role removes frontline-only narrowing');
$setRoles([2, 18]);
$expect($patients($actor([2, 18])) === [101, 102, 103, 105, 106, 107], 'custom parent-department scope includes descendants');
$setRoles([2, 7]);
$expect(!Scope::hasTeamRole(11, $actor([2, 7])), 'deleted configured manager role is not a team grant');
$expect($patients($actor([2, 7])) === [101], 'deleted role relationship cannot expand patient scope');
$setRoles([19]);
$expect(!Scope::hasTeamRole(11, $actor([19])) && $patients($actor([19])) === [101, 105], 'ordinary ALL functional role retains narrow relationship fallback');
$setRoles([3]);
$expect(count($patients($actor([3]))) === 8, 'explicit administrator retains all-patient scope');
// The supplied role set is an internal current-read result, not a request parameter.
$setRoles([3]);
$expect(!Scope::hasTeamRole(11, $actor([3]), []), 'explicit empty effective roles never fall back to cache or database');
$expect($patients($actor([3]), []) === [101, 105], 'empty current roles do not reopen cached full scope');
$expect($patients($actor([3]), [2]) === [101], 'current assistant roles override stale manager in both branch and range');
$expect($patients($actor([3]), [2, 17]) === [101, 102, 105, 106], 'current custom team range cannot reuse stale administrator ALL scope');
$expect($patients($actor([2]), [2, 17]) === [101, 102, 105, 106], 'current custom team grant is not narrowed by old assistant cache');
$expect($patients($actor([2]), ['2', 17, 17, 0, -1]) === [101, 102, 105, 106], 'internal effective role ids are normalized');
$roleLocks = [];
Db::event('before_select', static function ($query) use (&$roleLocks): void {
if ($query->getTable() === 'test_system_role') { $roleLocks[] = $query->getOptions('lock'); }
});
Scope::hasTeamRole(11, $actor([3]), [2, 17]);
$expect(in_array(true, $roleLocks, true), 'effective-role team classification requests current metadata read');
// Exercise the real assistant-service integration, including its department snapshot checks.
$setRoles([2, 17]);
$teamActor = $actor([2, 17]);
$expect(Access::visibleStaffIds(11, $teamActor) === [11, 12], 'custom team role enters existing staff/department snapshot validation');
$query = Db::name('tcm_diagnosis')->alias('d');
Access::applyScope($query, 11, $teamActor);
$expect(array_map('intval', $query->order('d.id')->column('d.id')) === [101, 102, 105, 106], 'assistant-service patient scope recognizes the custom supervisor');
// Only inspect SQL construction for MySQL; every executed query still uses in-memory SQLite.
final class ScopeMysqlQueryProbe extends think\db\Query {
public array $rawPredicates = [];
public function getConfig(string $name = '') { return $name === 'type' ? 'mysql' : parent::getConfig($name); }
public function whereRaw(string $where, array $bind = [], string $logic = 'AND') {
$this->rawPredicates[] = $where; return $this;
}
}
foreach ([[1], [19], [2, 17]] as $roles) {
$probe = new ScopeMysqlQueryProbe(Db::connect());
MyPatientLogic::applyScope($probe, 11, $actor([3]), $roles);
$sql = implode(' ', $probe->rawPredicates);
$expect(str_contains($sql, 'scope_apt') && str_contains($sql, ' FOR UPDATE)'), 'current appointment subquery retains lock for ' . implode(',', $roles));
}
$setRoles([1]);
$probe = new ScopeMysqlQueryProbe(Db::connect());
MyPatientLogic::applyScope($probe, 11, $actor([1]));
$expect(!str_contains(implode(' ', $probe->rawPredicates), 'FOR UPDATE'), 'legacy optional-argument behavior remains unlocked');
$expect(!$app->initialized(), 'application configuration and .env were never initialized');
echo "MyPatientScopeMergeTest: {$checks} checks passed (real ORM/authorization, synthetic SQLite, no network).\n";