# 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
366 lines
13 KiB
PHP
Executable File
366 lines
13 KiB
PHP
Executable File
<?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()));
|
||
}
|
||
}
|