135 lines
5.5 KiB
PHP
135 lines
5.5 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Auth;
|
||
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||
|
||
/** Single-person, current HR authorization lookup; no HR rows or stale grants are cached. */
|
||
class FinanceRoster
|
||
{
|
||
public static function isDesignatedSuperAdmin(string $userId): bool
|
||
{
|
||
return $userId !== '' && in_array($userId, config('dingtalk.super_admin_user_ids', []), true);
|
||
}
|
||
|
||
public static function isLimitedLoginUser(string $userId): bool
|
||
{
|
||
return $userId !== '' && in_array($userId, config('dingtalk.limited_login_user_ids', []), true);
|
||
}
|
||
|
||
public function __construct(private readonly UpstreamReadOnlyQueryFactory $queries)
|
||
{
|
||
}
|
||
|
||
public function findEligible(string $userId): ?array
|
||
{
|
||
if (trim($userId) === '') {
|
||
return null;
|
||
}
|
||
|
||
$query = $this->queries->forSource('operations_replica');
|
||
$flowers = $query->select(
|
||
'SELECT TOP (2) Id, name, flowerName, deptId, status, empStatus, leaveDate
|
||
FROM rpt.hr_flowerName_book WHERE flowerNameId = ? AND leaveDate IS NULL
|
||
ORDER BY Id DESC', [$userId]
|
||
);
|
||
if (count($flowers) !== 1) {
|
||
return null;
|
||
}
|
||
$flower = $flowers[0];
|
||
if ($this->isResigned($flower['status'] ?? null) || $this->isResigned($flower['empStatus'] ?? null)) {
|
||
return null;
|
||
}
|
||
if ($query->first('SELECT TOP (1) userid FROM rpt.hr_employee_dimission WHERE userid = ?', [$userId])) {
|
||
return null;
|
||
}
|
||
|
||
$employees = $query->select('SELECT TOP (2) active, dept_id_list FROM rpt.hr_employee WHERE userid = ?', [$userId]);
|
||
if (count($employees) > 1 || (isset($employees[0]['active']) && ! (bool) $employees[0]['active'])) {
|
||
return null;
|
||
}
|
||
$registrations = $query->select(
|
||
'SELECT TOP (20) [sys01-employeeStatus] AS employment_status, [sys00-deptIds] AS department_ids,
|
||
[sys00-mainDeptId] AS main_department_id FROM rpt.hr_employee_register WHERE userid = ?', [$userId]
|
||
);
|
||
if (count($registrations) === 20) {
|
||
return null;
|
||
}
|
||
|
||
$departmentIds = $this->departmentIds($flower['deptId'] ?? null);
|
||
foreach ($employees as $employee) {
|
||
$departmentIds = array_merge($departmentIds, $this->departmentIds($employee['dept_id_list'] ?? null));
|
||
}
|
||
foreach ($registrations as $registration) {
|
||
if ($this->isResigned($registration['employment_status'] ?? null)) {
|
||
return null;
|
||
}
|
||
$departmentIds = array_merge($departmentIds,
|
||
$this->departmentIds($registration['department_ids'] ?? null),
|
||
$this->departmentIds($registration['main_department_id'] ?? null));
|
||
}
|
||
$departmentIds = array_values(array_unique($departmentIds));
|
||
if ($departmentIds === [] || count($departmentIds) > 64) {
|
||
return null;
|
||
}
|
||
|
||
$placeholders = implode(',', array_fill(0, count($departmentIds), '?'));
|
||
$ancestors = $query->select(
|
||
'WITH lineage AS (
|
||
SELECT Id, parent_id, 0 AS depth FROM rpt.hr_groups WHERE Id IN ('.$placeholders.')
|
||
UNION ALL
|
||
SELECT parent.Id, parent.parent_id, child.depth + 1
|
||
FROM rpt.hr_groups parent JOIN lineage child ON parent.Id = child.parent_id
|
||
WHERE child.depth < 20
|
||
) SELECT TOP (2048) Id, parent_id, depth FROM lineage', $departmentIds
|
||
);
|
||
if (count($ancestors) === 2048 || array_filter($ancestors, fn ($row) => (int) $row['depth'] >= 20)) {
|
||
return null;
|
||
}
|
||
$allowed = array_map('strval', config('dingtalk.finance_department_ids', []));
|
||
if (! self::isDesignatedSuperAdmin($userId)
|
||
&& ! self::isLimitedLoginUser($userId)
|
||
&& ! array_intersect($allowed, array_map('strval', array_column($ancestors, 'Id')))) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'roster_id' => (string) $flower['Id'],
|
||
'display_name' => trim((string) ($flower['flowerName'] ?: $flower['name'])),
|
||
'department_ids' => $departmentIds,
|
||
];
|
||
}
|
||
|
||
/** Minimal, bounded directory search for workflow selection; no HR dataset is cached. */
|
||
public function search(string $term): array
|
||
{
|
||
if (trim($term) === '') {
|
||
return [];
|
||
}
|
||
$like = '%' . str_replace(['!', '%', '_', '['], ['!!', '!%', '!_', '!['], trim($term)) . '%';
|
||
return $this->queries->forSource('operations_replica')->select(
|
||
"SELECT TOP (12) flowerNameId, name, flowerName FROM rpt.hr_flowerName_book
|
||
WHERE leaveDate IS NULL AND flowerNameId IS NOT NULL
|
||
AND (name LIKE ? ESCAPE '!' OR flowerName LIKE ? ESCAPE '!') ORDER BY Id DESC", [$like, $like]
|
||
);
|
||
}
|
||
|
||
private function isResigned(mixed $value): bool
|
||
{
|
||
$status = trim((string) $value);
|
||
return $status === '5' || preg_match('/离职|停用|退休/u', $status) === 1;
|
||
}
|
||
|
||
private function departmentIds(mixed $value): array
|
||
{
|
||
$value = trim((string) $value);
|
||
$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(
|
||
fn ($id) => is_scalar($id) ? trim((string) $id) : '', $parts
|
||
), fn ($id) => ctype_digit($id) && (int) $id > 0));
|
||
}
|
||
}
|