admin/src/Oms.Application/DataPerm/DataPermRepository.cs
zzh 5f1f16c9ad fix(数据权限): 组织树递归查询使用 ParentId 而非 PId
修复 get-effective-by-user 展开下级组织时报 Unknown column c.PId。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 16:44:46 +08:00

533 lines
18 KiB
C#

using Fake.DependencyInjection;
using Fake.Rbac.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Oms.Application.Dtos.DataPerm;
namespace Oms.Application.DataPerm;
public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
{
private FakeRbacDbContext Db => dbContext;
public async Task<RbacUserSnapshot?> GetUserSnapshotAsync(Guid userId, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT
u.`Id` AS `Id`,
u.`userid` AS `MapId`,
u.`Account` AS `Account`,
u.`Name` AS `Name`,
u.`OrganizationId` AS `OrganizationId`,
o.`Name` AS `OrganizationName`
FROM `user` u
LEFT JOIN `organization` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0
WHERE u.`Id` = {0} AND u.`IsDeleted` = 0
LIMIT 1
""";
return await Db.Database.SqlQueryRaw<RbacUserSnapshot>(sql, userId.ToString())
.FirstOrDefaultAsync(cancellationToken);
}
public async Task<bool> IsPureCustomerUserAsync(Guid userId, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT COUNT(*) AS `Value`
FROM `user_role` ur
INNER JOIN `role` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
WHERE ur.`UserId` = {0}
AND r.`Code` <> 'Customer'
AND r.`Code` NOT LIKE 'CustomerSub_%'
""";
var nonCustomerRoleCount = await Db.Database.SqlQueryRaw<int>(sql, userId.ToString())
.FirstAsync(cancellationToken);
if (nonCustomerRoleCount > 0)
{
return false;
}
const string hasRoleSql = """
SELECT COUNT(*) AS `Value`
FROM `user_role` ur
WHERE ur.`UserId` = {0}
""";
var roleCount = await Db.Database.SqlQueryRaw<int>(hasRoleSql, userId.ToString())
.FirstAsync(cancellationToken);
return roleCount > 0;
}
public async Task<bool> IsAdminUserAsync(Guid userId, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT COUNT(*) AS `Value`
FROM `user_role` ur
INNER JOIN `role` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
WHERE ur.`UserId` = {0}
AND r.`Code` = 'Admin'
""";
var count = await Db.Database.SqlQueryRaw<int>(sql, userId.ToString()).FirstAsync(cancellationToken);
return count > 0;
}
public async Task<List<Guid>> GetUserRoleIdsAsync(Guid userId, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT ur.`RoleId` AS `Value`
FROM `user_role` ur
WHERE ur.`UserId` = {0}
""";
return await Db.Database.SqlQueryRaw<Guid>(sql, userId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<List<Guid>> GetUserIdsByRoleAsync(Guid roleId, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT ur.`UserId` AS `Value`
FROM `user_role` ur
WHERE ur.`RoleId` = {0}
""";
return await Db.Database.SqlQueryRaw<Guid>(sql, roleId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<List<OrgPermRow>> GetRoleOrgPermsAsync(IEnumerable<Guid> roleIds, CancellationToken cancellationToken = default)
{
var ids = roleIds.Distinct().ToList();
if (ids.Count == 0)
{
return [];
}
var inClause = string.Join(",", ids.Select(x => $"'{x}'"));
var sql = $"""
SELECT
p.`OrganizationId` AS `OrganizationId`,
p.`OrgScope` AS `OrgScope`,
NULL AS `EffectiveTime`,
NULL AS `ExpireTime`
FROM `role_org_perm` p
WHERE p.`IsDeleted` = 0
AND p.`RoleId` IN ({inClause})
""";
return await Db.Database.SqlQueryRaw<OrgPermRow>(sql).ToListAsync(cancellationToken);
}
public async Task<List<OrgPermRow>> GetUserOrgPermsAsync(Guid userId, bool includeExpired, CancellationToken cancellationToken = default)
{
var sql = includeExpired
? """
SELECT
p.`OrganizationId` AS `OrganizationId`,
p.`OrgScope` AS `OrgScope`,
p.`EffectiveTime` AS `EffectiveTime`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_org_perm` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
"""
: """
SELECT
p.`OrganizationId` AS `OrganizationId`,
p.`OrgScope` AS `OrgScope`,
p.`EffectiveTime` AS `EffectiveTime`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_org_perm` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
AND p.`EffectiveTime` <= UTC_TIMESTAMP()
AND (p.`ExpireTime` IS NULL OR p.`ExpireTime` > UTC_TIMESTAMP())
""";
return await Db.Database.SqlQueryRaw<OrgPermRow>(sql, userId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<List<DataScopeRow>> GetRoleDataScopesAsync(IEnumerable<Guid> roleIds, CancellationToken cancellationToken = default)
{
var ids = roleIds.Distinct().ToList();
if (ids.Count == 0)
{
return [];
}
var inClause = string.Join(",", ids.Select(x => $"'{x}'"));
var sql = $"""
SELECT
p.`DataScope` AS `DataScope`,
p.`DataResourceCode` AS `DataResourceCode`,
NULL AS `ExpireTime`
FROM `role_data_scope` p
WHERE p.`IsDeleted` = 0
AND p.`RoleId` IN ({inClause})
""";
return await Db.Database.SqlQueryRaw<DataScopeRow>(sql).ToListAsync(cancellationToken);
}
public async Task<List<DataScopeRow>> GetUserDataScopesAsync(Guid userId, bool includeExpired, CancellationToken cancellationToken = default)
{
var sql = includeExpired
? """
SELECT
p.`DataScope` AS `DataScope`,
p.`DataResourceCode` AS `DataResourceCode`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_data_scope` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
"""
: """
SELECT
p.`DataScope` AS `DataScope`,
p.`DataResourceCode` AS `DataResourceCode`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_data_scope` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
AND p.`EffectiveTime` <= UTC_TIMESTAMP()
AND (p.`ExpireTime` IS NULL OR p.`ExpireTime` > UTC_TIMESTAMP())
""";
return await Db.Database.SqlQueryRaw<DataScopeRow>(sql, userId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<List<BizPermRow>> GetRoleBizPermsAsync(IEnumerable<Guid> roleIds, CancellationToken cancellationToken = default)
{
var ids = roleIds.Distinct().ToList();
if (ids.Count == 0)
{
return [];
}
var inClause = string.Join(",", ids.Select(x => $"'{x}'"));
var sql = $"""
SELECT
p.`BizType` AS `BizType`,
p.`BizId` AS `BizId`,
NULL AS `ExpireTime`
FROM `role_biz_perm` p
WHERE p.`IsDeleted` = 0
AND p.`RoleId` IN ({inClause})
""";
return await Db.Database.SqlQueryRaw<BizPermRow>(sql).ToListAsync(cancellationToken);
}
public async Task<List<BizPermRow>> GetUserBizPermsAsync(Guid userId, bool includeExpired, CancellationToken cancellationToken = default)
{
var sql = includeExpired
? """
SELECT
p.`BizType` AS `BizType`,
p.`BizId` AS `BizId`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_biz_perm` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
"""
: """
SELECT
p.`BizType` AS `BizType`,
p.`BizId` AS `BizId`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_biz_perm` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
AND p.`EffectiveTime` <= UTC_TIMESTAMP()
AND (p.`ExpireTime` IS NULL OR p.`ExpireTime` > UTC_TIMESTAMP())
""";
return await Db.Database.SqlQueryRaw<BizPermRow>(sql, userId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<List<SecretPermRow>> GetRoleSecretPermsAsync(IEnumerable<Guid> roleIds, CancellationToken cancellationToken = default)
{
var ids = roleIds.Distinct().ToList();
if (ids.Count == 0)
{
return [];
}
var inClause = string.Join(",", ids.Select(x => $"'{x}'"));
var sql = $"""
SELECT
p.`SecretLevel` AS `SecretLevel`,
p.`DataResourceCode` AS `DataResourceCode`,
NULL AS `ExpireTime`
FROM `role_secret_perm` p
WHERE p.`IsDeleted` = 0
AND p.`RoleId` IN ({inClause})
""";
return await Db.Database.SqlQueryRaw<SecretPermRow>(sql).ToListAsync(cancellationToken);
}
public async Task<List<SecretPermRow>> GetUserSecretPermsAsync(Guid userId, bool includeExpired, CancellationToken cancellationToken = default)
{
var sql = includeExpired
? """
SELECT
p.`SecretLevel` AS `SecretLevel`,
p.`DataResourceCode` AS `DataResourceCode`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_secret_perm` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
"""
: """
SELECT
p.`SecretLevel` AS `SecretLevel`,
p.`DataResourceCode` AS `DataResourceCode`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_secret_perm` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
AND p.`EffectiveTime` <= UTC_TIMESTAMP()
AND (p.`ExpireTime` IS NULL OR p.`ExpireTime` > UTC_TIMESTAMP())
""";
return await Db.Database.SqlQueryRaw<SecretPermRow>(sql, userId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<List<FilterRuleRow>> GetRoleFilterRulesAsync(
IEnumerable<Guid> roleIds,
string dataResourceCode,
CancellationToken cancellationToken = default)
{
var ids = roleIds.Distinct().ToList();
if (ids.Count == 0)
{
return [];
}
var inClause = string.Join(",", ids.Select(x => $"'{x}'"));
var sql = $"""
SELECT
p.`DataResourceCode` AS `DataResourceCode`,
CAST(p.`FilterContent` AS CHAR) AS `FilterContentJson`,
NULL AS `ExpireTime`
FROM `role_filter_rule` p
WHERE p.`IsDeleted` = 0
AND p.`RoleId` IN ({inClause})
AND p.`DataResourceCode` = '{dataResourceCode.Replace("'", "''")}'
""";
return await Db.Database.SqlQueryRaw<FilterRuleRow>(sql).ToListAsync(cancellationToken);
}
public async Task<List<FilterRuleRow>> GetUserFilterRulesAsync(
Guid userId,
string dataResourceCode,
bool includeExpired,
CancellationToken cancellationToken = default)
{
var resource = dataResourceCode.Replace("'", "''");
var baseSql = """
SELECT
p.`DataResourceCode` AS `DataResourceCode`,
CAST(p.`FilterContent` AS CHAR) AS `FilterContentJson`,
p.`ExpireTime` AS `ExpireTime`
FROM `user_filter_rule` p
WHERE p.`IsDeleted` = 0
AND p.`UserId` = {0}
AND p.`DataResourceCode` =
""" + $"'{resource}'";
var sql = includeExpired
? baseSql
: baseSql + """
AND p.`EffectiveTime` <= UTC_TIMESTAMP()
AND (p.`ExpireTime` IS NULL OR p.`ExpireTime` > UTC_TIMESTAMP())
""";
return await Db.Database.SqlQueryRaw<FilterRuleRow>(sql, userId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<HashSet<Guid>> ExpandOrganizationIdsAsync(
IEnumerable<(Guid OrganizationId, int OrgScope)> orgEntries,
CancellationToken cancellationToken = default)
{
var result = new HashSet<Guid>();
foreach (var (organizationId, orgScope) in orgEntries)
{
if (organizationId == Guid.Empty)
{
continue;
}
if (orgScope == DataPermConstants.OrgScopeSelfOnly)
{
result.Add(organizationId);
continue;
}
var descendants = await GetOrganizationSubtreeIdsAsync(organizationId, cancellationToken);
foreach (var id in descendants)
{
result.Add(id);
}
}
return result;
}
public async Task<List<Guid>> GetOrganizationSubtreeIdsAsync(Guid rootOrganizationId, CancellationToken cancellationToken = default)
{
const string sql = """
WITH RECURSIVE org_tree AS (
SELECT o.`Id`
FROM `organization` o
WHERE o.`Id` = {0} AND o.`IsDeleted` = 0
UNION ALL
SELECT c.`Id`
FROM `organization` c
INNER JOIN org_tree t ON c.`ParentId` = t.`Id`
WHERE c.`IsDeleted` = 0
)
SELECT `Id` AS `Value` FROM org_tree
""";
return await Db.Database.SqlQueryRaw<Guid>(sql, rootOrganizationId.ToString())
.ToListAsync(cancellationToken);
}
public async Task<Dictionary<Guid, string>> GetOrganizationNamesAsync(
IEnumerable<Guid> organizationIds,
CancellationToken cancellationToken = default)
{
var ids = organizationIds.Distinct().Where(x => x != Guid.Empty).ToList();
if (ids.Count == 0)
{
return [];
}
var inClause = string.Join(",", ids.Select(x => $"'{x}'"));
var sql = $"""
SELECT o.`Id` AS `Id`, o.`Name` AS `Name`
FROM `organization` o
WHERE o.`IsDeleted` = 0
AND o.`Id` IN ({inClause})
""";
var rows = await Db.Database.SqlQueryRaw<OrganizationNameRow>(sql).ToListAsync(cancellationToken);
return rows.ToDictionary(x => x.Id, x => x.Name ?? x.Id.ToString());
}
public async Task<bool> OrganizationExistsAndEnabledAsync(Guid organizationId, CancellationToken cancellationToken = default)
{
const string sql = """
SELECT COUNT(*) AS `Value`
FROM `organization` o
WHERE o.`Id` = {0}
AND o.`IsDeleted` = 0
""";
var count = await Db.Database.SqlQueryRaw<int>(sql, organizationId.ToString()).FirstAsync(cancellationToken);
return count > 0;
}
public async Task<List<int>> ResolveOwnerMapIdsAsync(
Guid organizationId,
int dataScope,
CancellationToken cancellationToken = default)
{
if (organizationId == Guid.Empty)
{
return [];
}
if (dataScope == DataPermConstants.DataScopeTeam)
{
const string sql = """
SELECT u.`userid` AS `Value`
FROM `user` u
WHERE u.`IsDeleted` = 0
AND u.`OrganizationId` = {0}
""";
return await Db.Database.SqlQueryRaw<int>(sql, organizationId.ToString())
.ToListAsync(cancellationToken);
}
if (dataScope == DataPermConstants.DataScopeDepartment)
{
var orgIds = await GetOrganizationSubtreeIdsAsync(organizationId, cancellationToken);
if (orgIds.Count == 0)
{
return [];
}
var inClause = string.Join(",", orgIds.Select(x => $"'{x}'"));
var sql = $"""
SELECT u.`userid` AS `Value`
FROM `user` u
WHERE u.`IsDeleted` = 0
AND u.`OrganizationId` IN ({inClause})
""";
return await Db.Database.SqlQueryRaw<int>(sql).ToListAsync(cancellationToken);
}
return [];
}
public async Task<List<DataResourceMetaDto>> GetDataResourcesAsync(CancellationToken cancellationToken = default)
{
const string sql = """
SELECT
r.`Code` AS `Code`,
r.`Name` AS `Name`,
r.`MainTable` AS `MainTable`
FROM `data_resource` r
WHERE r.`IsDeleted` = 0
AND r.`IsEnabled` = 1
ORDER BY r.`SortOrder`, r.`Code`
""";
var rows = await Db.Database.SqlQueryRaw<DataResourceRow>(sql).ToListAsync(cancellationToken);
return rows.Select(row => new DataResourceMetaDto
{
Code = row.Code,
Name = row.Name,
MainTable = row.MainTable,
FilterableFields = DataPermConstants.FilterableFieldsByResource.TryGetValue(row.Code, out var fields)
? fields.ToList()
: []
}).ToList();
}
public async Task InvalidateUserContextCacheKeysAsync(Guid userId, CancellationToken cancellationToken = default)
{
// Cache invalidation handled by DataPermContextCache; repository placeholder for future DB-side hooks.
await Task.CompletedTask;
}
private sealed class OrganizationNameRow
{
public Guid Id { get; set; }
public string? Name { get; set; }
}
private sealed class DataResourceRow
{
public string Code { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? MainTable { get; set; }
}
}