CaiWuXiTong/app/Support/CourierQuoteWorkflowPermissions.php

144 lines
6.5 KiB
PHP

<?php
namespace App\Support;
use App\Models\Auth\DingTalkIdentity;
use App\Models\Auth\User;
use App\Models\Business\CourierQuoteChange;
use App\Services\Auth\FinanceRoster;
use App\Utilities\SidebarPermissions;
use App\Utilities\SystemAccess;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
final class CourierQuoteWorkflowPermissions
{
public const KEY = 'workflow.courier_quote_permissions';
public const AUDIT_PREFIX = 'workflow.courier_quote_permission_audit.';
public function __construct(private readonly FinanceRoster $roster)
{
}
public function settings(): array
{
$value = DB::table('settings')->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' => '钉钉花名册暂时无法核验,请稍后重试。']);
}
}
}