CaiWuXiTong/app/Services/Auth/FinanceRoster.php
limeng f41a8fa6ba 钉钉登录:单独授权人员仅访问三个快递报价页面 (#339)
按精确企业身份开放受限登录,保留在职校验和普通角色;统一限制菜单、页面、导出与预览。59 项相关测试通过,无数据库迁移。
2026-10-15 15:21:05 +08:00

135 lines
5.5 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 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));
}
}