195 lines
6.9 KiB
PHP
195 lines
6.9 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Auth;
|
||
|
||
use App\Models\Auth\DingTalkIdentity;
|
||
use App\Models\Auth\SupplierPortalIdentity;
|
||
use App\Models\Auth\User;
|
||
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||
|
||
/**
|
||
* 钉钉企业组织中的供应商成员映射。
|
||
*
|
||
* 只有当前有效的钉钉企业员工、且其部门祖先精确对应唯一供应商主档名称时,
|
||
* 才建立供应商门户身份。财务花名册不是供应商人员的准入前提。
|
||
*/
|
||
class SupplierPortalAccess
|
||
{
|
||
public const CREATED_FROM = 'dingtalk_supplier_portal';
|
||
|
||
public const PAGE_KEY = 'supplier-reconciliation/supplier-data';
|
||
|
||
public function __construct(
|
||
private readonly UpstreamReadOnlyQueryFactory $queries,
|
||
private readonly SupplierManagementMirrorRepository $suppliers,
|
||
) {
|
||
}
|
||
|
||
public static function isSupplierUser(User $user): bool
|
||
{
|
||
return $user->created_from === self::CREATED_FROM;
|
||
}
|
||
|
||
/** @return array{supplier_name:string,supplier_code:string,dingtalk_group_id:int,dingtalk_group_name:string}|null */
|
||
public function resolve(string $userId, string $unionId = ''): ?array
|
||
{
|
||
$userId = trim($userId);
|
||
$unionId = trim($unionId);
|
||
if ($userId === '') {
|
||
return null;
|
||
}
|
||
|
||
$query = $this->queries->forSource('operations_replica');
|
||
$employees = $query->select(
|
||
'SELECT TOP (2) userid, unionid, name, active, dept_id_list
|
||
FROM rpt.hr_employee WHERE userid = ?',
|
||
[$userId],
|
||
);
|
||
if (count($employees) !== 1 || ! (bool) ($employees[0]['active'] ?? false)) {
|
||
return null;
|
||
}
|
||
$employee = $employees[0];
|
||
$employeeUnionId = trim((string) ($employee['unionid'] ?? ''));
|
||
if ($employeeUnionId === '' || ($unionId !== '' && ! hash_equals($employeeUnionId, $unionId))) {
|
||
return null;
|
||
}
|
||
|
||
$departmentIds = $this->departmentIds($employee['dept_id_list'] ?? null);
|
||
if ($departmentIds === [] || count($departmentIds) > 64) {
|
||
return null;
|
||
}
|
||
|
||
$placeholders = implode(',', array_fill(0, count($departmentIds), '?'));
|
||
$ancestors = $query->select(
|
||
'WITH lineage AS (
|
||
SELECT Id, parent_id, name, 0 AS depth FROM rpt.hr_groups WHERE Id IN ('.$placeholders.')
|
||
UNION ALL
|
||
SELECT parent.Id, parent.parent_id, parent.name, child.depth + 1
|
||
FROM rpt.hr_groups parent JOIN lineage child ON parent.Id = child.parent_id
|
||
WHERE child.depth < 20
|
||
) SELECT TOP (256) Id, parent_id, name, depth FROM lineage',
|
||
$departmentIds,
|
||
);
|
||
if ($ancestors === [] || count($ancestors) >= 256
|
||
|| array_filter($ancestors, static fn (array $row): bool => (int) ($row['depth'] ?? 0) >= 20)) {
|
||
return null;
|
||
}
|
||
|
||
$groupsByName = [];
|
||
foreach ($ancestors as $ancestor) {
|
||
$name = trim((string) ($ancestor['name'] ?? ''));
|
||
if ($name !== '') {
|
||
$groupsByName[$name][] = $ancestor;
|
||
}
|
||
}
|
||
if ($groupsByName === []) {
|
||
return null;
|
||
}
|
||
|
||
$supplierRows = $this->suppliers->suppliersByNames(array_keys($groupsByName));
|
||
$matchedSupplierNames = [];
|
||
foreach ($supplierRows as $supplier) {
|
||
$name = trim((string) ($supplier->scmname ?? ''));
|
||
if ($name !== '' && isset($groupsByName[$name])) {
|
||
$matchedSupplierNames[$name] = true;
|
||
}
|
||
}
|
||
if (count($matchedSupplierNames) !== 1) {
|
||
return null;
|
||
}
|
||
|
||
$supplierName = (string) array_key_first($matchedSupplierNames);
|
||
$matchingRows = array_values(array_filter(
|
||
$supplierRows,
|
||
static fn (object $supplier): bool => trim((string) ($supplier->scmname ?? '')) === $supplierName,
|
||
));
|
||
if (count($matchingRows) !== 1) {
|
||
return null;
|
||
}
|
||
|
||
usort($groupsByName[$supplierName], static fn (array $left, array $right): int =>
|
||
(int) ($right['depth'] ?? 0) <=> (int) ($left['depth'] ?? 0));
|
||
$group = $groupsByName[$supplierName][0];
|
||
|
||
return [
|
||
'supplier_name' => $supplierName,
|
||
'supplier_code' => trim((string) ($matchingRows[0]->scmcode ?? '')),
|
||
'dingtalk_group_id' => (int) ($group['Id'] ?? 0),
|
||
'dingtalk_group_name' => $supplierName,
|
||
];
|
||
}
|
||
|
||
public function bind(User $user, int $companyId, array $resolved): SupplierPortalIdentity
|
||
{
|
||
return SupplierPortalIdentity::query()->updateOrCreate(
|
||
['user_id' => $user->id],
|
||
[
|
||
'company_id' => $companyId,
|
||
'supplier_code' => $resolved['supplier_code'] ?? null,
|
||
'supplier_name' => $resolved['supplier_name'],
|
||
'dingtalk_group_id' => (int) $resolved['dingtalk_group_id'],
|
||
'dingtalk_group_name' => $resolved['dingtalk_group_name'],
|
||
'last_verified_at' => now(),
|
||
],
|
||
);
|
||
}
|
||
|
||
public function mappingFor(User $user): ?SupplierPortalIdentity
|
||
{
|
||
if (! self::isSupplierUser($user)) {
|
||
return null;
|
||
}
|
||
|
||
return SupplierPortalIdentity::query()
|
||
->where('user_id', $user->id)
|
||
->where('company_id', company_id())
|
||
->first();
|
||
}
|
||
|
||
public function supplierFor(User $user): ?string
|
||
{
|
||
$mapping = $this->mappingFor($user);
|
||
$name = trim((string) ($mapping?->supplier_name ?? ''));
|
||
|
||
return $name !== '' ? $name : null;
|
||
}
|
||
|
||
public function revalidate(User $user): ?SupplierPortalIdentity
|
||
{
|
||
if (! self::isSupplierUser($user)) {
|
||
return null;
|
||
}
|
||
|
||
$dingTalk = DingTalkIdentity::query()->where('user_id', $user->id)->first();
|
||
$company = $user->withoutEvents(fn () => $user->companies()->enabled()->first());
|
||
if (! $dingTalk || ! $company) {
|
||
return null;
|
||
}
|
||
|
||
$resolved = $this->resolve(
|
||
(string) $dingTalk->dingtalk_user_id,
|
||
(string) $dingTalk->union_id,
|
||
);
|
||
|
||
return $resolved ? $this->bind($user, (int) $company->id, $resolved) : null;
|
||
}
|
||
|
||
private function departmentIds(mixed $value): array
|
||
{
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return [];
|
||
}
|
||
$parts = json_decode($value, true);
|
||
if (! is_array($parts)) {
|
||
$parts = preg_split('/[\[\]\/>,,;;||\s]+/u', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||
}
|
||
|
||
return array_values(array_filter(array_map(
|
||
static fn (mixed $id): string => is_scalar($id) ? trim((string) $id) : '',
|
||
is_array($parts) ? $parts : [],
|
||
), static fn (string $id): bool => ctype_digit($id) && (int) $id > 0));
|
||
}
|
||
}
|