feat(rbac): 新增内部用户列表接口,排除纯客户账号
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ad367998d2
commit
0bf909dc2b
42
src/Oms.Application/Dtos/Rbac/InternalUserListDtos.cs
Normal file
42
src/Oms.Application/Dtos/Rbac/InternalUserListDtos.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Oms.Application.Dtos.Rbac;
|
||||
|
||||
public class InternalUserListItemDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? Account { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Avatar { get; set; }
|
||||
|
||||
public string? OrganizationId { get; set; }
|
||||
|
||||
public string? OrganizationName { get; set; }
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
public List<InternalUserRoleDto> Roles { get; set; } = [];
|
||||
}
|
||||
|
||||
public class InternalUserRoleDto
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? Code { get; set; }
|
||||
}
|
||||
|
||||
public class InternalUserListResultDto
|
||||
{
|
||||
[JsonPropertyName("total")]
|
||||
public long Total { get; set; }
|
||||
|
||||
[JsonPropertyName("items")]
|
||||
public List<InternalUserListItemDto> Items { get; set; } = [];
|
||||
}
|
||||
163
src/Oms.Application/Rbac/InternalUserRepository.cs
Normal file
163
src/Oms.Application/Rbac/InternalUserRepository.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Oms.Application.Dtos.Rbac;
|
||||
|
||||
namespace Oms.Application.Rbac;
|
||||
|
||||
public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
{
|
||||
private const string InternalUserFilter = """
|
||||
(
|
||||
NOT EXISTS (SELECT 1 FROM `user_role` ur0 WHERE ur0.`UserId` = u.`Id`)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM `user_role` ur
|
||||
INNER JOIN `role` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
|
||||
WHERE ur.`UserId` = u.`Id`
|
||||
AND r.`Code` <> 'Customer'
|
||||
AND r.`Code` NOT LIKE 'CustomerSub\_%'
|
||||
)
|
||||
)
|
||||
""";
|
||||
|
||||
public async Task<InternalUserListResultDto> GetInternalUsersAsync(
|
||||
string? keyword,
|
||||
Guid? organizationId,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var where = $"WHERE u.`IsDeleted` = 0 AND {InternalUserFilter}";
|
||||
var args = new List<object>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var pattern = $"%{keyword.Trim()}%";
|
||||
where += $" AND (u.`Name` LIKE {{{args.Count}}} OR u.`Account` LIKE {{{args.Count + 1}}})";
|
||||
args.Add(pattern);
|
||||
args.Add(pattern);
|
||||
}
|
||||
|
||||
if (organizationId.HasValue && organizationId.Value != Guid.Empty)
|
||||
{
|
||||
where += $" AND u.`OrganizationId` = {{{args.Count}}}";
|
||||
args.Add(organizationId.Value.ToString());
|
||||
}
|
||||
|
||||
var countSql = $"""
|
||||
SELECT COUNT(*) AS `Value`
|
||||
FROM `user` u
|
||||
{where}
|
||||
""";
|
||||
var total = await dbContext.Database.SqlQueryRaw<long>(countSql, args.ToArray())
|
||||
.FirstAsync(cancellationToken);
|
||||
|
||||
var offset = Math.Max(page - 1, 0) * pageSize;
|
||||
var listSql = $"""
|
||||
SELECT
|
||||
u.`Id` AS `Id`,
|
||||
u.`Name` AS `Name`,
|
||||
u.`Account` AS `Account`,
|
||||
u.`Email` AS `Email`,
|
||||
u.`Avatar` AS `Avatar`,
|
||||
u.`OrganizationId` AS `OrganizationId`,
|
||||
o.`Name` AS `OrganizationName`,
|
||||
u.`CreateTime` AS `CreatedAt`
|
||||
FROM `user` u
|
||||
LEFT JOIN `organization` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
{where}
|
||||
ORDER BY u.`CreateTime` DESC
|
||||
LIMIT {pageSize} OFFSET {offset}
|
||||
""";
|
||||
|
||||
var rows = await dbContext.Database.SqlQueryRaw<InternalUserRow>(listSql, args.ToArray())
|
||||
.ToListAsync(cancellationToken);
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
return new InternalUserListResultDto { Total = total, Items = [] };
|
||||
}
|
||||
|
||||
var userIds = rows.Select(x => x.Id).Distinct().ToList();
|
||||
var roleMap = await LoadRolesByUserIdsAsync(userIds, cancellationToken);
|
||||
var items = rows.Select(row => new InternalUserListItemDto
|
||||
{
|
||||
Id = row.Id.ToString(),
|
||||
Name = row.Name,
|
||||
Account = row.Account,
|
||||
Email = row.Email,
|
||||
Avatar = row.Avatar,
|
||||
OrganizationId = row.OrganizationId?.ToString(),
|
||||
OrganizationName = row.OrganizationName,
|
||||
CreatedAt = row.CreatedAt,
|
||||
Roles = roleMap.TryGetValue(row.Id, out var roles) ? roles : []
|
||||
}).ToList();
|
||||
|
||||
return new InternalUserListResultDto { Total = total, Items = items };
|
||||
}
|
||||
|
||||
private async Task<Dictionary<Guid, List<InternalUserRoleDto>>> LoadRolesByUserIdsAsync(
|
||||
IReadOnlyList<Guid> userIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (userIds.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var inClause = string.Join(",", userIds.Select(x => $"'{x}'"));
|
||||
var sql = $"""
|
||||
SELECT
|
||||
ur.`UserId` AS `UserId`,
|
||||
r.`Id` AS `RoleId`,
|
||||
r.`Name` AS `RoleName`,
|
||||
r.`Code` AS `RoleCode`
|
||||
FROM `user_role` ur
|
||||
INNER JOIN `role` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
|
||||
WHERE ur.`UserId` IN ({inClause})
|
||||
ORDER BY r.`Name`
|
||||
""";
|
||||
|
||||
var roleRows = await dbContext.Database.SqlQueryRaw<UserRoleRow>(sql).ToListAsync(cancellationToken);
|
||||
return roleRows
|
||||
.GroupBy(x => x.UserId)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Select(x => new InternalUserRoleDto
|
||||
{
|
||||
Id = x.RoleId.ToString(),
|
||||
Name = x.RoleName,
|
||||
Code = x.RoleCode
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
private sealed class InternalUserRow
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? Account { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Avatar { get; set; }
|
||||
|
||||
public Guid? OrganizationId { get; set; }
|
||||
|
||||
public string? OrganizationName { get; set; }
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
private sealed class UserRoleRow
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public Guid RoleId { get; set; }
|
||||
|
||||
public string? RoleName { get; set; }
|
||||
|
||||
public string? RoleCode { get; set; }
|
||||
}
|
||||
}
|
||||
38
src/Oms.Application/Services/Rbac/OmsInternalUserService.cs
Normal file
38
src/Oms.Application/Services/Rbac/OmsInternalUserService.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Rbac;
|
||||
using Oms.Application.Rbac;
|
||||
using Oms.Application.Shared;
|
||||
|
||||
namespace Oms.Application.Services.Rbac;
|
||||
|
||||
[Authorize]
|
||||
[ApiExplorerSettings(GroupName = "RBAC-User")]
|
||||
public class OmsInternalUserService(InternalUserRepository repository) : ApplicationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部用户分页列表(排除纯 Customer / CustomerSub 客户账号)。
|
||||
/// </summary>
|
||||
[HttpGet("/rbac/user/internal-list")]
|
||||
public Task<InternalUserListResultDto> GetInternalListAsync(
|
||||
[FromQuery] string? keyword,
|
||||
[FromQuery] string? organizationId,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 30,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Guid? orgId = null;
|
||||
if (!string.IsNullOrWhiteSpace(organizationId) && Guid.TryParse(organizationId.Trim(), out var parsedOrgId))
|
||||
{
|
||||
orgId = parsedOrgId;
|
||||
}
|
||||
|
||||
return repository.GetInternalUsersAsync(
|
||||
keyword,
|
||||
orgId,
|
||||
OmsDocHelper.NormalizePage(page),
|
||||
OmsDocHelper.NormalizePageSize(pageSize),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,8 @@ public class DataPermApiIntegrationTests
|
||||
"rbac/data-perm/field-mask/save",
|
||||
"rbac/data-perm/change-log/list",
|
||||
"rbac/data-perm/access-log/list",
|
||||
"rbac/data-perm/invalidate-cache"
|
||||
"rbac/data-perm/invalidate-cache",
|
||||
"rbac/user/internal-list"
|
||||
];
|
||||
|
||||
[Fact]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user