144 lines
6.5 KiB
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' => '钉钉花名册暂时无法核验,请稍后重试。']);
|
|
}
|
|
}
|
|
}
|