CaiWuXiTong/app/Services/Auth/FinanceRoster.php

208 lines
8.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\Auth;
use Illuminate\Support\Facades\Cache;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
/** Single-person, current HR authorization lookup; no HR rows or stale grants are cached. */
class FinanceRoster
{
private const FINANCE_MEMBERS_CACHE_KEY = 'dingtalk:finance-members:v1';
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', []));
// 财务中心(或指定超级管理员/受限登录名单)拥有完整权限;
// 其它在职员工返回 restricted,只能访问风险管控下的两个页面。
$inFinance = self::isDesignatedSuperAdmin($userId)
|| self::isLimitedLoginUser($userId)
|| (bool) array_intersect($allowed, array_map('strval', array_column($ancestors, 'Id')));
return [
'roster_id' => (string) $flower['Id'],
'display_name' => trim((string) ($flower['flowerName'] ?: $flower['name'])),
'department_ids' => $departmentIds,
'restricted' => ! $inFinance,
];
}
/**
* 当前财务中心在职人员,只用于日志应提交名单和统计口径。
*
* @return list<array{userid:string,name:string}>
*/
public function financeMembers(): array
{
$roots = array_values(array_filter(
array_map('strval', (array) config('dingtalk.finance_department_ids', [])),
static fn (string $id): bool => ctype_digit($id) && (int) $id > 0,
));
if ($roots === []) {
return [];
}
$placeholders = implode(',', array_fill(0, count($roots), '?'));
$sql = <<<SQL
WITH finance_groups AS (
SELECT Id, parent_id, 0 AS depth FROM rpt.hr_groups WHERE Id IN ({$placeholders})
UNION ALL
SELECT child.Id, child.parent_id, parent.depth + 1
FROM rpt.hr_groups child
JOIN finance_groups parent ON child.parent_id = parent.Id
WHERE parent.depth < 20
), employee_departments AS (
SELECT e.userid, TRY_CAST(REPLACE(value, '"', '') AS bigint) AS dept_id
FROM rpt.hr_employee e
CROSS APPLY STRING_SPLIT(REPLACE(REPLACE(e.dept_id_list, '[', ''), ']', ''), ',')
WHERE e.active = 1
), flower_departments AS (
SELECT f.flowerNameId AS userid, TRY_CAST(REPLACE(value, '"', '') AS bigint) AS dept_id
FROM rpt.hr_flowerName_book f
CROSS APPLY STRING_SPLIT(REPLACE(REPLACE(f.deptId, '[', ''), ']', ''), ',')
WHERE f.leaveDate IS NULL AND f.flowerNameId IS NOT NULL
), finance_users AS (
SELECT DISTINCT userid FROM employee_departments WHERE dept_id IN (SELECT Id FROM finance_groups)
UNION
SELECT DISTINCT userid FROM flower_departments WHERE dept_id IN (SELECT Id FROM finance_groups)
)
SELECT DISTINCT f.flowerNameId AS userid, f.name, f.flowerName
FROM finance_users u
JOIN rpt.hr_flowerName_book f ON f.flowerNameId = u.userid
WHERE f.leaveDate IS NULL
ORDER BY f.flowerName, f.name
SQL;
$rows = $this->queries->forSourceWithTimeout('operations_replica', 60)->select($sql, $roots);
return array_values(array_filter(array_map(static function (array $row): ?array {
$userId = trim((string) ($row['userid'] ?? ''));
$flowerName = trim((string) ($row['flowerName'] ?? ''));
$name = trim((string) ($row['name'] ?? ''));
if ($userId === '' || ($flowerName === '' && $name === '')) {
return null;
}
return ['userid' => $userId, 'name' => $flowerName !== '' ? $flowerName : $name];
}, $rows)));
}
/** @return list<array{userid:string,name:string}> */
public function cachedFinanceMembers(): array
{
return (array) Cache::remember(
self::FINANCE_MEMBERS_CACHE_KEY,
now()->addMinutes(max((int) config('dingtalk.finance_roster_cache_minutes', 720), 1)),
fn (): array => $this->financeMembers(),
);
}
/** 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));
}
}