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' => []]; } 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']]; } } if (count(array_unique(array_merge($data['initiators'], $data['reviewers']))) < 2) { 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 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() && $this->settings()['configured'] && $change->created_by === $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' => '钉钉花名册暂时无法核验,请稍后重试。']); } } }