Files
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

366 lines
13 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\common\service\DataScope;
use app\adminapi\logic\dept\DeptLogic;
use app\common\model\auth\AdminDept;
use app\common\model\auth\AdminRole;
use app\common\model\auth\SystemRole;
use think\facade\Config;
/**
* 数据范围(数据隔离)工具服务。
*
* 设计约定:
* - ALL (1) = 全部数据,不附加过滤
* - DEPT_AND_CHILD (2) = 本部门及所有子部门(取 admin 全部部门的并集)
* - DEPT (3) = 仅本部门(取 admin 全部部门的并集,不含子孙)
* - SELF (4) = 仅本人
*
* 多角色时,2/3/4 范围按授权并集取最宽范围;迁移期遗留的普通 scope=1 角色不会
* 自动冲掉有限范围。只有明确的管理员角色、root 或 exempt_roles 才能在组合角色中放开全部。
* root 管理员固定为 ALL。未挂任何部门时,范围退化为 SELF(可由 config 关闭)。
*
* 关键返回:`getVisibleAdminIds` 返回 int[](可见 admin_id 集合)或 null(ALL = 不过滤)。
*/
class DataScopeService
{
public const SCOPE_ALL = 1;
public const SCOPE_DEPT_AND_CHILD = 2;
public const SCOPE_DEPT = 3;
public const SCOPE_SELF = 4;
/** @var string[] 明确允许在多角色组合中放开全部数据的内置角色名。 */
private const ALL_SCOPE_ROLE_NAMES = ['管理员', '系统管理员'];
/** @var int[] 默认一线角色:医生(1)、医助(2),业务上自带“只看本人”的收窄。 */
private const DEFAULT_FRONTLINE_ROLE_IDS = [1, 2];
/** @var int[] 默认团队/管理角色(经理、组长、管理员等):按数据范围查看团队数据。 */
private const DEFAULT_TEAM_ROLE_IDS = [3, 7, 8];
/**
* 计算当前 admin 的有效数据范围。
*/
public static function getEffectiveScope(array $adminInfo): int
{
if (!self::isEnabled()) {
return self::SCOPE_ALL;
}
if ((int) ($adminInfo['root'] ?? 0) === 1) {
return self::SCOPE_ALL;
}
$roleIds = self::normalizeRoleIds($adminInfo['role_id'] ?? null);
$exempt = array_map('intval', Config::get('project.data_scope.exempt_roles', []) ?: []);
if ($roleIds !== [] && array_intersect($roleIds, $exempt) !== []) {
return self::SCOPE_ALL;
}
if ($roleIds === []) {
return self::SCOPE_SELF;
}
$roleRows = SystemRole::whereIn('id', $roleIds)
->whereNull('delete_time')
->field('id,name,data_scope')
->select()
->toArray();
return self::mergeRoleScopes($roleRows);
}
/**
* 合并多个角色的数据范围。
*
* 历史迁移曾把全部旧角色默认成 scope=1;如果账号同时具有有限范围角色,普通的
* scope=1 视为功能角色而不参与放大,防止“医生 + 医助”等组合意外获得全站数据。
* 2/3/4 是嵌套授权,取最小值即可表达多个有效数据角色的可见范围并集。
*
* @param array<int,array<string,mixed>> $roleRows
*/
private static function mergeRoleScopes(array $roleRows): int
{
$validRows = [];
foreach ($roleRows as $roleRow) {
$scope = (int) ($roleRow['data_scope'] ?? 0);
if ($scope < self::SCOPE_ALL || $scope > self::SCOPE_SELF) {
continue;
}
$validRows[] = [
'name' => trim((string) ($roleRow['name'] ?? '')),
'scope' => $scope,
];
}
// 角色存在但库中无有效 data_scope(缺失/脏数据/已删角色):宁可收窄到本人。
if ($validRows === []) {
return self::SCOPE_SELF;
}
foreach ($validRows as $roleRow) {
if ($roleRow['scope'] === self::SCOPE_ALL
&& in_array($roleRow['name'], self::ALL_SCOPE_ROLE_NAMES, true)) {
return self::SCOPE_ALL;
}
}
$boundedScopes = array_column(array_values(array_filter(
$validRows,
static fn (array $roleRow): bool => $roleRow['scope'] > self::SCOPE_ALL
)), 'scope');
if ($boundedScopes !== []) {
return (int) min($boundedScopes);
}
// 只有普通 scope=1 角色时保持原有“全部数据”行为。
return self::SCOPE_ALL;
}
/**
* 统一解析 token/cache 中的 role_id(数组 | 单整数 | JSON 字符串)。
*
* @return int[]
*/
private static function normalizeRoleIds(mixed $raw): array
{
if ($raw === null || $raw === '') {
return [];
}
if (\is_int($raw) || \is_float($raw)) {
$v = (int) $raw;
return $v > 0 ? [$v] : [];
}
if (\is_string($raw) && is_numeric($raw)) {
$v = (int) $raw;
return $v > 0 ? [$v] : [];
}
if (\is_string($raw)) {
$decoded = json_decode($raw, true);
if (\is_array($decoded)) {
$raw = $decoded;
} else {
return [];
}
}
if (!\is_array($raw)) {
return [];
}
return array_values(array_filter(array_map(
static fn ($v): int => (int) $v,
$raw
), static fn (int $v): bool => $v > 0));
}
/**
* 可见 admin id 集合;null = 不过滤(ALL)
*
* @return array<int>|null
*/
public static function getVisibleAdminIds(int $adminId, array $adminInfo): ?array
{
$scope = self::getEffectiveScope($adminInfo);
if ($scope === self::SCOPE_ALL) {
return null;
}
if ($scope === self::SCOPE_SELF) {
return $adminId > 0 ? [$adminId] : [];
}
$myDeptIds = AdminDept::where('admin_id', $adminId)->column('dept_id');
$myDeptIds = array_values(array_filter(array_map('intval', $myDeptIds), static function (int $v): bool {
return $v > 0;
}));
if ($myDeptIds === []) {
$fallback = (bool) Config::get('project.data_scope.no_dept_fallback_self', true);
return $fallback ? [$adminId] : [];
}
$targetDeptIds = [];
if ($scope === self::SCOPE_DEPT) {
$targetDeptIds = $myDeptIds;
} else {
foreach ($myDeptIds as $did) {
foreach (DeptLogic::getSelfAndDescendantIds($did) as $id) {
$id = (int) $id;
if ($id > 0) {
$targetDeptIds[$id] = true;
}
}
}
$targetDeptIds = array_keys($targetDeptIds);
}
if ($targetDeptIds === []) {
return $adminId > 0 ? [$adminId] : [];
}
$ids = AdminDept::whereIn('dept_id', $targetDeptIds)->column('admin_id');
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static function (int $v): bool {
return $v > 0;
})));
if ($adminId > 0 && !in_array($adminId, $ids, true)) {
$ids[] = $adminId;
}
return $ids;
}
public static function isEnabled(): bool
{
return (bool) Config::get('project.data_scope.enabled', true);
}
public static function isAll(array $adminInfo): bool
{
return self::getEffectiveScope($adminInfo) === self::SCOPE_ALL;
}
/**
* 数据范围下:可见成员所在部门及其下级部门 id(与业绩看板 deptOptions、部门类下拉收窄一致)。
*
* @return array<int, true>|null null 表示不限制;[] 表示无可选部门
*/
public static function getAllowedDeptIdSet(int $adminId, array $adminInfo): ?array
{
if ($adminId <= 0 || !self::isEnabled()) {
return null;
}
$visibleIds = self::getVisibleAdminIds($adminId, $adminInfo);
if ($visibleIds === null) {
return null;
}
if ($visibleIds === []) {
return [];
}
$set = [];
foreach ($visibleIds as $aid) {
$deptRows = AdminDept::where('admin_id', (int) $aid)->column('dept_id');
foreach ($deptRows as $d) {
$d = (int) $d;
if ($d <= 0) {
continue;
}
foreach (DeptLogic::getSelfAndDescendantIds($d) as $x) {
$x = (int) $x;
if ($x > 0) {
$set[$x] = true;
}
}
}
}
return $set;
}
/**
* 文字描述(日志 / 接口返回可选使用)
*/
public static function scopeLabel(int $scope): string
{
return [
self::SCOPE_ALL => '全部',
self::SCOPE_DEPT_AND_CHILD => '本部门及下级',
self::SCOPE_DEPT => '仅本部门',
self::SCOPE_SELF => '仅本人',
][$scope] ?? '全部';
}
/**
* 当前 admin 持有的全部角色 id:优先取登录令牌缓存中的 role_id,缺失时查 admin_role。
*
* @return int[]
*/
public static function roleIdsOf(int $adminId, array $adminInfo = []): array
{
$roleIds = self::normalizeRoleIds($adminInfo['role_id'] ?? null);
if ($roleIds === [] && $adminId > 0) {
$roleIds = self::normalizeRoleIds(AdminRole::where('admin_id', $adminId)->column('role_id'));
}
return array_values(array_unique($roleIds));
}
/**
* 一线角色 id(默认医生 1、医助 2),可由 project.data_scope.frontline_roles 覆盖。
*
* @return int[]
*/
public static function frontlineRoleIds(): array
{
$ids = Config::get('project.data_scope.frontline_roles', self::DEFAULT_FRONTLINE_ROLE_IDS);
return self::normalizeRoleIds(\is_array($ids) && $ids !== [] ? $ids : self::DEFAULT_FRONTLINE_ROLE_IDS);
}
/**
* 是否持有团队/管理角色。
*
* 多角色按并集取最宽:只要有一个角色能看团队数据,就不再因为同时挂了医生/医助而收窄到本人。
* 判定口径:root;角色在 project.data_scope.team_roles 或 exempt_roles 中;
* 角色 data_scope 为「本部门及下级」/「仅本部门」;或 data_scope=1 且为内置管理员角色名。
* 新建角色默认 data_scope=1(全部)且不参与放大,主管类角色须把数据范围设为 2/3 才会被识别为团队角色。
*/
public static function hasTeamRole(int $adminId, array $adminInfo = [], ?array $currentRoleIds = null): bool
{
if ((int) ($adminInfo['root'] ?? 0) === 1) {
return true;
}
// 内部授权入口可传已核验的当前有效角色;空数组是明确无角色,不能回退到缓存或旧快照。
// 禁止把请求参数直接传入 $currentRoleIds。
$roleIds = $currentRoleIds === null
? self::roleIdsOf($adminId, $adminInfo) : self::normalizeRoleIds($currentRoleIds);
if ($roleIds === []) {
return false;
}
$configured = self::normalizeRoleIds(array_merge(
(array) (Config::get('project.data_scope.team_roles', self::DEFAULT_TEAM_ROLE_IDS) ?: []),
(array) (Config::get('project.data_scope.exempt_roles', []) ?: [])
));
$roleRows = SystemRole::whereIn('id', $roleIds)
->whereNull('delete_time')
->field('id,name,data_scope')
->lock($currentRoleIds !== null)
->select()
->toArray();
// 配置中的团队角色也必须仍然存在,已删角色的遗留关系不能放大范围。
if (array_intersect(array_column($roleRows, 'id'), $configured) !== []) {
return true;
}
foreach ($roleRows as $row) {
$scope = (int) ($row['data_scope'] ?? 0);
if ($scope === self::SCOPE_DEPT_AND_CHILD || $scope === self::SCOPE_DEPT) {
return true;
}
if ($scope === self::SCOPE_ALL
&& in_array(trim((string) ($row['name'] ?? '')), self::ALL_SCOPE_ROLE_NAMES, true)) {
return true;
}
}
return false;
}
/**
* 当前 admin 生效的“仅本人”一线角色(医生/医助)子集。
*
* root 或持有团队角色时返回 [](范围交给 DataScope 按部门决定);否则返回其持有的一线角色。
* 业务代码用 in_array(1|2, 本方法结果) 代替直接 in_array(1|2, 全部角色),多角色即取最宽范围。
*
* @return int[]
*/
public static function selfOnlyRoleIds(int $adminId, array $adminInfo = []): array
{
if (self::hasTeamRole($adminId, $adminInfo)) {
return [];
}
return array_values(array_intersect(self::roleIdsOf($adminId, $adminInfo), self::frontlineRoleIds()));
}
}