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']]);