From 913d6203d989e6cde8680243c23769949b4ce69e Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Sat, 24 Oct 2026 13:24:59 +0800 Subject: [PATCH] =?UTF-8?q?=E9=92=89=E9=92=89=E7=99=BB=E5=BD=95=E6=9D=83?= =?UTF-8?q?=E9=99=90=EF=BC=9A=E8=B4=A2=E5=8A=A1=E4=B8=AD=E5=BF=83=E7=9C=8B?= =?UTF-8?q?=E5=85=A8=E9=83=A8=EF=BC=8C=E5=85=B6=E4=BB=96=E5=9C=A8=E8=81=8C?= =?UTF-8?q?=E5=91=98=E5=B7=A5=E5=8F=AA=E7=9C=8B=E9=A3=8E=E9=99=A9=E7=AE=A1?= =?UTF-8?q?=E6=8E=A7=E4=B8=A4=E4=B8=AA=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FinanceRoster 返回 restricted 标记:非财务中心在职员工可以登录,但只允许访问 店铺违规信息预警、店铺流程 - 每次登录按当前部门刷新访问范围(财务中心=全部,其它=两个页面),手工账号不受影响 - SidebarPermissions 增加 restricted 范围,菜单与页面权限同步收口 - 补充登录范围测试 --- app/Services/Auth/DingTalkClosedAccess.php | 30 ++++++++++++++-- app/Services/Auth/FinanceRoster.php | 11 +++--- app/Utilities/SidebarPermissions.php | 10 ++++++ config/dingtalk.php | 5 +++ tests/Feature/Auth/DingTalkLoginTest.php | 40 ++++++++++++++++++++++ 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/app/Services/Auth/DingTalkClosedAccess.php b/app/Services/Auth/DingTalkClosedAccess.php index b3f80c92..3de54934 100644 --- a/app/Services/Auth/DingTalkClosedAccess.php +++ b/app/Services/Auth/DingTalkClosedAccess.php @@ -28,6 +28,7 @@ class DingTalkClosedAccess } $roster = app(FinanceRoster::class)->findEligible((string) ($profile['dingtalk_user_id'] ?? '')); + $restricted = $roster !== null && ($roster['restricted'] ?? false) === true; $supplierRoster = $roster ? null : app(SupplierPortalAccess::class)->resolve( (string) ($profile['dingtalk_user_id'] ?? ''), (string) ($profile['union_id'] ?? ''), @@ -77,6 +78,9 @@ class DingTalkClosedAccess 'nickname' => $profile['nickname'] ?: $identity->nickname, ])->save(); + // 按当前部门刷新访问范围:财务中心=全部,其它在职员工=风险管控两个页面。 + $this->applyAccessScope($identity->user, $restricted); + return $identity; } @@ -90,7 +94,7 @@ class DingTalkClosedAccess return null; } - return DB::transaction(function () use ($profile, $roster, $supplierRoster, $request): ?DingTalkIdentity { + return DB::transaction(function () use ($profile, $roster, $restricted, $supplierRoster, $request): ?DingTalkIdentity { // Serialize first-time provisioning on the target company. Both the // account and unique identity are committed together, including retries. $companies = Company::enabled() @@ -135,8 +139,10 @@ class DingTalkClosedAccess 'created_by' => 0, 'created_from' => $supplierPortal ? SupplierPortalAccess::CREATED_FROM - : (FinanceRoster::isLimitedLoginUser((string) $profile['dingtalk_user_id']) - ? 'dingtalk_roster_limited' : 'dingtalk_roster'), + : ($restricted + ? 'dingtalk_restricted' + : (FinanceRoster::isLimitedLoginUser((string) $profile['dingtalk_user_id']) + ? 'dingtalk_roster_limited' : 'dingtalk_roster')), ]); $account->roles()->attach($role->id); // Explicit designation controls the role; ordinary roster users get shell access. @@ -168,6 +174,24 @@ class DingTalkClosedAccess }, 3); } + /** + * 自动开通的钉钉账号按当前部门刷新访问范围;手工账号不受影响。 + */ + private function applyAccessScope(User $user, bool $restricted): void + { + $createdFrom = (string) $user->created_from; + + if (! in_array($createdFrom, ['dingtalk_roster', 'dingtalk_restricted'], true)) { + return; + } + + $target = $restricted ? 'dingtalk_restricted' : 'dingtalk_roster'; + + if ($createdFrom !== $target) { + $user->forceFill(['created_from' => $target])->save(); + } + } + public function recordLogin(DingTalkIdentity $identity, Request $request): void { $this->audit('login_success', 'success', $identity->user_id, null, $identity->union_id, $request); diff --git a/app/Services/Auth/FinanceRoster.php b/app/Services/Auth/FinanceRoster.php index ee8deae1..3a0e32a0 100644 --- a/app/Services/Auth/FinanceRoster.php +++ b/app/Services/Auth/FinanceRoster.php @@ -87,16 +87,17 @@ class FinanceRoster return null; } $allowed = array_map('strval', config('dingtalk.finance_department_ids', [])); - if (! self::isDesignatedSuperAdmin($userId) - && ! self::isLimitedLoginUser($userId) - && ! array_intersect($allowed, array_map('strval', array_column($ancestors, 'Id')))) { - return null; - } + // 财务中心(或指定超级管理员/受限登录名单)拥有完整权限; + // 其它在职员工返回 restricted,只能访问风险管控下的两个页面。 + $inFinance = self::isDesignatedSuperAdmin($userId) + || self::isLimitedLoginUser($userId) + || (bool) array_intersect($allowed, array_map('strval', array_column($ancestors, 'Id'))); return [ 'roster_id' => (string) $flower['Id'], 'display_name' => trim((string) ($flower['flowerName'] ?: $flower['name'])), 'department_ids' => $departmentIds, + 'restricted' => ! $inFinance, ]; } diff --git a/app/Utilities/SidebarPermissions.php b/app/Utilities/SidebarPermissions.php index 86238163..496baf59 100644 --- a/app/Utilities/SidebarPermissions.php +++ b/app/Utilities/SidebarPermissions.php @@ -50,6 +50,11 @@ class SidebarPermissions return [SupplierPortalAccess::PAGE_KEY]; } + // 非财务中心员工:只能看到财务经营风险管控下的两个页面。 + if (self::hasRestrictedDingTalkAccess($user)) { + return self::normalizeAllowedKeys((array) config('dingtalk.restricted_page_keys', [])); + } + if (self::hasLimitedDingTalkAccess($user)) { return config('dingtalk.limited_login_page_keys', []); } @@ -88,6 +93,11 @@ class SidebarPermissions && FinanceRoster::isLimitedLoginUser((string) $user->dingtalkIdentity?->dingtalk_user_id)); } + public static function hasRestrictedDingTalkAccess(User $user): bool + { + return $user->created_from === 'dingtalk_restricted'; + } + public static function navigationFor(User $user): array { $navigation = BusinessNavigation::all(); diff --git a/config/dingtalk.php b/config/dingtalk.php index eda77691..46e6fdb2 100644 --- a/config/dingtalk.php +++ b/config/dingtalk.php @@ -20,6 +20,11 @@ return [ 'courier-payables/courier-information', 'courier-payables/quote-management', ], + // 非财务中心员工:可以登录,但只能访问财务经营风险管控下的这两个页面。 + 'restricted_page_keys' => [ + 'financial-risk-control/store-violation-alert', + 'financial-risk-control/process-statistics', + ], // When omitted, automatic provisioning requires exactly one enabled company. 'company_id' => env('DINGTALK_COMPANY_ID'), diff --git a/tests/Feature/Auth/DingTalkLoginTest.php b/tests/Feature/Auth/DingTalkLoginTest.php index f47cd93e..d4e5ddf1 100644 --- a/tests/Feature/Auth/DingTalkLoginTest.php +++ b/tests/Feature/Auth/DingTalkLoginTest.php @@ -138,6 +138,46 @@ class DingTalkLoginTest extends FeatureTestCase ->assertSee('data-cockpit', false); } + public function test_non_finance_employee_only_gets_the_two_risk_control_pages(): void + { + $this->mock(FinanceRoster::class)->shouldReceive('findEligible')->andReturn([ + 'roster_id' => '99001', + 'display_name' => '运营中心员工', + 'department_ids' => ['777001'], + 'restricted' => true, + ]); + Http::preventStrayRequests(); + Http::fake($this->dingtalkResponses('union-restricted', 'open-restricted')); + $state = $this->startAuthorization(); + $this->get(route('login.dingtalk.callback', ['code' => 'fixture', 'state' => $state])) + ->assertRedirect(route('dashboard', ['company_id' => $this->company->id])); + + $created = auth()->user(); + $this->assertSame('dingtalk_restricted', $created->created_from); + $this->assertSame([ + 'financial-risk-control/store-violation-alert', + 'financial-risk-control/process-statistics', + ], \App\Utilities\SidebarPermissions::allowedKeys($created)); + + $this->get(route('business.pages.show', [ + 'company_id' => $this->company->id, + 'section' => 'financial-risk-control', + 'page' => 'store-violation-alert', + ]))->assertOk(); + + $this->get(route('business.pages.show', [ + 'company_id' => $this->company->id, + 'section' => 'financial-risk-control', + 'page' => 'process-statistics', + ]))->assertOk(); + + $this->withExceptionHandling()->get(route('business.pages.show', [ + 'company_id' => $this->company->id, + 'section' => 'order-data', + 'page' => 'raw-data', + ]))->assertStatus(403); + } + public function test_limited_login_gets_only_three_courier_pages_and_blocks_direct_urls(): void { config(['dingtalk.limited_login_user_ids' => ['ding-user-1']]); -- 2.47.2