where('company_id', company_id())->where('key', self::KEY)->value('value'); $settings = json_decode($value ?? '', true); $settings = is_array($settings) ? $settings : []; return ['configured' => $value !== null] + $settings + ['initiators' => [], 'reviewers' => [], 'self_reviewers' => []]; } public function history(): array { return DB::table('settings')->where('company_id', company_id())->where('key', 'like', self::AUDIT_PREFIX . '%') ->orderByDesc('key')->limit(10)->pluck('value')->map(fn ($value) => json_decode($value, true))->all(); } public function save(array $data, string $actor, ?int $actorId): void { $next = ['initiators' => [], 'reviewers' => []]; foreach (array_keys($next) as $stage) { $ids = $data[$stage] ?? []; if (! is_array($ids) || count($ids) < 1 || count($ids) > 10 || count(array_unique($ids)) !== count($ids)) { throw ValidationException::withMessages([$stage => '每类请选择 1 至 10 名不同人员。']); } foreach ($ids as $id) { $account = $this->account((string) $id); $person = $account ? $this->eligible((string) $id) : null; if (! $person) { throw ValidationException::withMessages([$stage => '所选人员须为在职财务人员,已绑定启用账号并有报价管理页面权限。']); } $next[$stage][] = ['dingtalk_user_id' => (string) $id, 'display_name' => $person['display_name']]; } } // 未指定时保留已授权的本人单据审核权限;移出审核名单时同步撤销。 $selfIds = $data['self_reviewers'] ?? array_intersect( array_column($this->settings()['self_reviewers'], 'dingtalk_user_id'), $data['reviewers'] ); if (! is_array($selfIds) || count($selfIds) > 10 || count(array_unique($selfIds)) !== count($selfIds) || array_diff($selfIds, $data['reviewers'])) { throw ValidationException::withMessages(['reviewers' => '本人单据审核权限只能授予当前指定审核人员。']); } $next['self_reviewers'] = array_values(array_filter($next['reviewers'], fn ($person) => in_array($person['dingtalk_user_id'], $selfIds, true))); if (count(array_unique(array_merge($data['initiators'], $data['reviewers']))) < 2 && $next['self_reviewers'] === []) { throw ValidationException::withMessages(['reviewers' => '发起人不能自审,请至少配置两名不同人员。']); } DB::transaction(function () use ($next, $actor, $actorId): void { abort_unless(DB::table('companies')->where('id', company_id())->lockForUpdate()->first(), 404); $before = $this->settings(); $next['updated_at'] = now()->toIso8601String(); $next['updated_by'] = $actor; DB::table('settings')->updateOrInsert(['company_id' => company_id(), 'key' => self::KEY], [ 'value' => json_encode($next, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), ]); DB::table('settings')->insert(['company_id' => company_id(), 'key' => self::AUDIT_PREFIX . Str::ulid(), 'value' => json_encode(['at' => $next['updated_at'], 'actor_id' => $actorId, 'actor' => $actor, 'before' => $before, 'after' => $next], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)]); }); } public function search(string $term): array { return array_map(function (array $row): array { $id = (string) $row['flowerNameId']; $account = $this->account($id); $eligible = $account ? $this->eligible($id) : null; return ['id' => $id, 'name' => trim((string) ($row['flowerName'] ?: $row['name'])), 'account' => $account?->name, 'selectable' => $eligible !== null, 'reason' => $eligible ? '可选' : '须在职、绑定启用账号并有报价管理页面权限']; }, $this->roster->search($term)); } public function can(string $stage, int $userId): bool { $settings = $this->settings(); if (! $settings['configured']) { $user = User::find($userId); return $user && ($stage === 'initiators' || in_array(SystemAccess::roleName($user), [SystemAccess::ADMIN, SystemAccess::SUPER_ADMIN], true)); } $identity = DingTalkIdentity::query()->where('user_id', $userId)->first(); if (! $identity || ! in_array($identity->dingtalk_user_id, array_column($settings[$stage] ?? [], 'dingtalk_user_id'), true) || $this->account($identity->dingtalk_user_id)?->id !== $userId) { return false; } return $this->eligible($identity->dingtalk_user_id) !== null; } public function assertInitiator(int $userId): void { abort_unless($this->can('initiators', $userId), 403, '你不是报价变价的指定发起人员。'); } public function allowsSelfReview(int $userId): bool { $settings = $this->settings(); if (! $settings['configured']) { return (bool) config('courier_onboarding.allow_same_reviewer'); } $identity = DingTalkIdentity::query()->where('user_id', $userId)->first(); return $identity && in_array($identity->dingtalk_user_id, array_column($settings['self_reviewers'], 'dingtalk_user_id'), true) && $this->can('reviewers', $userId); } public function assertReviewer(CourierQuoteChange $change, int $userId): void { abort_unless((int) $change->company_id === (int) company_id(), 403); $allowed = $change->isStockQuote() ? in_array(SystemAccess::roleName(User::find($userId)), [SystemAccess::ADMIN, SystemAccess::SUPER_ADMIN], true) : $this->can('reviewers', $userId); abort_unless($allowed, 403, '你不是该报价变价的指定审核人员。'); if (! $change->isStockQuote() && $change->created_by === $userId && ! $this->allowsSelfReview($userId)) { throw ValidationException::withMessages(['approval' => '发起人不能审核自己提交的报价变更。']); } } private function account(string $id): ?User { $identities = DingTalkIdentity::query()->where('dingtalk_user_id', $id)->with('user')->limit(2)->get(); $user = $identities->count() === 1 ? $identities->first()->user : null; if (! $user || ! $user->enabled || ! $user->companies()->where('companies.id', company_id())->exists()) { return null; } $keys = SidebarPermissions::allowedKeys($user); return $keys === null || array_intersect(['courier-payables', 'courier-payables/quote-management'], $keys) ? $user : null; } private function eligible(string $id): ?array { try { $person = $this->roster->findEligible($id); return $person && empty($person['restricted']) ? $person : null; } catch (\Throwable) { throw ValidationException::withMessages(['approval' => '钉钉花名册暂时无法核验,请稍后重试。']); } } }