All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
仓库地址增加排序/备注字段与迁移;用户创建时对邮箱冲突等给出明确中文提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
395 lines
14 KiB
C#
395 lines
14 KiB
C#
using Fake.Application;
|
||
using Fake.Domain.Exceptions;
|
||
using Fake.Rbac.Application.Dtos.Common;
|
||
using Fake.Rbac.Application.Dtos.Role;
|
||
using Fake.Rbac.Application.Dtos.User;
|
||
using Fake.Rbac.Application.Services;
|
||
using Fake.Rbac.Domain.Managers;
|
||
using Fake.Rbac.Infrastructure;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Fake.ObjectStorage;
|
||
using Oms.Application.Dtos.Rbac;
|
||
using Oms.Application.Rbac;
|
||
using Oms.Application.Shared;
|
||
using Oms.Application.Storage;
|
||
using Oms.Domain.SystemAggregate.Entities;
|
||
using Oms.Domain.SystemAggregate.Enums;
|
||
using Oms.Domain.SystemAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Services.Rbac;
|
||
|
||
/// <summary>
|
||
/// 接管 RBAC 用户接口,扩展电话、工号、微信号、微信二维码字段。
|
||
/// </summary>
|
||
[Authorize]
|
||
[ApiExplorerSettings(GroupName = "RBAC-User")]
|
||
public class OmsUserService(
|
||
UserService userService,
|
||
AccountManager accountManager,
|
||
UserProfileRepository profileRepository,
|
||
IEmployeeRepository employeeRepository,
|
||
FakeRbacDbContext rbacDbContext,
|
||
IObjectStorage fileStorage) : ApplicationService
|
||
{
|
||
/// <summary>
|
||
/// 获取用户详情(含扩展资料)。
|
||
/// </summary>
|
||
[HttpGet("/rbac/user/get")]
|
||
public async Task<OmsUserDetailDto> GetAsync([FromQuery] Guid id, CancellationToken cancellationToken = default)
|
||
{
|
||
var user = await userService.GetAsync(id, cancellationToken);
|
||
var profile = await profileRepository.GetAsync(id, cancellationToken);
|
||
return MapUserDetail(user, profile);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询用户列表(含扩展资料)。
|
||
/// </summary>
|
||
[HttpGet("/rbac/user/get-list")]
|
||
public async Task<PagedResultDto<OmsUserDetailDto>> GetListAsync(
|
||
[FromQuery] UserPagedRequestDto input,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
var result = await userService.GetListAsync(input, cancellationToken);
|
||
if (result.Items.Count == 0)
|
||
{
|
||
return new PagedResultDto<OmsUserDetailDto>
|
||
{
|
||
Total = result.Total,
|
||
Items = [],
|
||
};
|
||
}
|
||
|
||
var userIds = result.Items
|
||
.Select(x => x.Id)
|
||
.Where(x => x != Guid.Empty)
|
||
.Distinct()
|
||
.ToList();
|
||
var profileMap = await profileRepository.GetManyAsync(userIds, cancellationToken);
|
||
return new PagedResultDto<OmsUserDetailDto>
|
||
{
|
||
Total = result.Total,
|
||
Items = result.Items
|
||
.Select(user =>
|
||
{
|
||
profileMap.TryGetValue(user.Id, out var profile);
|
||
return MapUserDetail(user, profile);
|
||
})
|
||
.ToList(),
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按角色查询用户简要列表。
|
||
/// </summary>
|
||
[HttpGet("/rbac/user/get-users-by-role")]
|
||
public Task<List<UserSimpleDto>> GetUsersByRoleAsync(
|
||
[FromQuery] Guid roleId,
|
||
CancellationToken cancellationToken = default) =>
|
||
userService.GetUsersByRoleAsync(roleId, cancellationToken);
|
||
|
||
/// <summary>
|
||
/// 创建用户并保存扩展资料。
|
||
/// </summary>
|
||
[HttpPost("/rbac/user/create")]
|
||
public async Task<OmsUserDetailDto> CreateAsync(
|
||
[FromBody] OmsUserCreateInput input,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
var account = input.Account?.Trim() ?? string.Empty;
|
||
OmsRbacRegistrationGuard.ValidateAdminUserFields(
|
||
input.Name,
|
||
account,
|
||
input.Password,
|
||
input.Email,
|
||
input.Phone,
|
||
input.WechatId,
|
||
requirePassword: true);
|
||
|
||
var employeeNo = await ResolveEmployeeNoForCreateAsync(input.EmployeeNo, cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsureLoginIdentityAvailableAsync(
|
||
rbacDbContext,
|
||
account,
|
||
input.Email,
|
||
forAdminUserCreate: true,
|
||
excludeUserId: null,
|
||
cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsurePhoneAvailableAsync(
|
||
rbacDbContext,
|
||
input.Phone,
|
||
excludeUserId: null,
|
||
cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsureWechatIdAvailableAsync(
|
||
rbacDbContext,
|
||
input.WechatId,
|
||
excludeUserId: null,
|
||
cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsureOrganizationExistsAsync(
|
||
rbacDbContext,
|
||
input.OrganizationId,
|
||
cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsureRolesExistAsync(
|
||
rbacDbContext,
|
||
input.RoleIds,
|
||
cancellationToken);
|
||
|
||
try
|
||
{
|
||
var user = await userService.CreateAsync(new UserCreateDto
|
||
{
|
||
Name = input.Name!.Trim(),
|
||
Account = account,
|
||
Password = input.Password,
|
||
// 禁止 Email='':uk_user_email 会把空串当成占用
|
||
Email = string.IsNullOrWhiteSpace(input.Email) ? null : input.Email.Trim(),
|
||
Avatar = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
|
||
OrganizationId = input.OrganizationId,
|
||
RoleIds = input.RoleIds,
|
||
}, cancellationToken);
|
||
|
||
await profileRepository.SaveAsync(
|
||
user.Id,
|
||
new UserProfileData
|
||
{
|
||
Phone = input.Phone,
|
||
EmployeeNo = employeeNo,
|
||
WechatId = input.WechatId,
|
||
WechatQrCode = input.WechatQrCode,
|
||
WecomKfUrl = input.WecomKfUrl,
|
||
},
|
||
cancellationToken);
|
||
|
||
await EnsureEmployeeProfileAsync(user.Id, input, cancellationToken);
|
||
|
||
var profile = await profileRepository.GetAsync(user.Id, cancellationToken);
|
||
return MapUserDetail(user, profile);
|
||
}
|
||
catch (Exception ex) when (ex is not BusinessException)
|
||
{
|
||
throw OmsRbacRegistrationGuard.ToSaveBusinessException(ex, forAdminUserCreate: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新用户及扩展资料。
|
||
/// </summary>
|
||
[HttpPut("/rbac/user/update")]
|
||
public async Task<OmsUserDetailDto> UpdateAsync(
|
||
[FromQuery] Guid id,
|
||
[FromBody] OmsUserUpdateInput input,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (id == Guid.Empty)
|
||
{
|
||
throw new BusinessException("用户 Id 无效");
|
||
}
|
||
|
||
// 更新不改登录账号/密码
|
||
OmsRbacRegistrationGuard.ValidateAdminUserFields(
|
||
input.Name,
|
||
account: "placeholder",
|
||
password: null,
|
||
input.Email,
|
||
input.Phone,
|
||
input.WechatId,
|
||
requirePassword: false);
|
||
|
||
var email = string.IsNullOrWhiteSpace(input.Email) ? null : input.Email.Trim();
|
||
await OmsRbacRegistrationGuard.EnsureEmailAvailableAsync(
|
||
rbacDbContext,
|
||
email,
|
||
excludeUserId: id,
|
||
forAdminUserCreate: true,
|
||
cancellationToken);
|
||
|
||
await OmsRbacRegistrationGuard.EnsurePhoneAvailableAsync(
|
||
rbacDbContext,
|
||
input.Phone,
|
||
excludeUserId: id,
|
||
cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsureWechatIdAvailableAsync(
|
||
rbacDbContext,
|
||
input.WechatId,
|
||
excludeUserId: id,
|
||
cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsureOrganizationExistsAsync(
|
||
rbacDbContext,
|
||
input.OrganizationId,
|
||
cancellationToken);
|
||
|
||
try
|
||
{
|
||
var user = await userService.UpdateAsync(
|
||
id,
|
||
new UserUpdateDto
|
||
{
|
||
Name = input.Name!.Trim(),
|
||
Email = email,
|
||
Avatar = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
|
||
OrganizationId = input.OrganizationId,
|
||
},
|
||
cancellationToken);
|
||
|
||
await profileRepository.UpdateContactAsync(
|
||
id,
|
||
new UserProfileData
|
||
{
|
||
Phone = input.Phone,
|
||
WechatId = input.WechatId,
|
||
WechatQrCode = input.WechatQrCode,
|
||
WecomKfUrl = input.WecomKfUrl,
|
||
},
|
||
cancellationToken);
|
||
|
||
var profile = await profileRepository.GetAsync(id, cancellationToken);
|
||
return MapUserDetail(user, profile);
|
||
}
|
||
catch (Exception ex) when (ex is not BusinessException)
|
||
{
|
||
throw OmsRbacRegistrationGuard.ToSaveBusinessException(ex, forAdminUserCreate: true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 修改用户密码。
|
||
/// </summary>
|
||
[HttpPut("/rbac/user/update-password")]
|
||
public Task UpdatePasswordAsync(
|
||
[FromQuery] Guid id,
|
||
[FromBody] UpdatePasswordDto input,
|
||
CancellationToken cancellationToken = default) =>
|
||
userService.UpdatePasswordAsync(id, input, cancellationToken);
|
||
|
||
/// <summary>
|
||
/// 管理员重置用户密码(无需旧密码)。
|
||
/// </summary>
|
||
[HttpPost("/rbac/user/reset-password")]
|
||
public async Task ResetPasswordAsync(
|
||
[FromQuery] Guid id,
|
||
[FromBody] ResetUserPasswordInput input,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (id == Guid.Empty)
|
||
{
|
||
throw new BusinessException("用户 Id 无效");
|
||
}
|
||
|
||
var newPassword = input?.NewPassword?.Trim();
|
||
if (string.IsNullOrWhiteSpace(newPassword))
|
||
{
|
||
throw new BusinessException("新密码不能为空");
|
||
}
|
||
|
||
if (newPassword.Length < 6)
|
||
{
|
||
throw new BusinessException("密码长度不能少于6位");
|
||
}
|
||
|
||
await accountManager.ResetPasswordAsync(id, newPassword, cancellationToken);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新用户头像。
|
||
/// </summary>
|
||
[HttpPut("/rbac/user/update-avatar")]
|
||
public Task UpdateAvatarAsync(
|
||
[FromQuery] Guid id,
|
||
[FromQuery] string avatarUrl,
|
||
CancellationToken cancellationToken = default) =>
|
||
userService.UpdateAvatarAsync(id, OmsObjectStorageExtensions.ToStorageKey(avatarUrl) ?? avatarUrl, cancellationToken);
|
||
|
||
/// <summary>
|
||
/// 删除用户。
|
||
/// </summary>
|
||
[HttpDelete("/rbac/user/delete")]
|
||
public Task DeleteAsync([FromQuery] Guid id, CancellationToken cancellationToken = default) =>
|
||
userService.DeleteAsync(id, cancellationToken);
|
||
|
||
/// <summary>
|
||
/// 批量删除用户。
|
||
/// </summary>
|
||
[HttpDelete("/rbac/user/delete-batch")]
|
||
public Task DeleteBatchAsync([FromQuery] List<Guid> ids, CancellationToken cancellationToken = default) =>
|
||
userService.DeleteBatchAsync(ids, cancellationToken);
|
||
|
||
/// <summary>
|
||
/// 为用户分配角色。
|
||
/// </summary>
|
||
[HttpPost("/rbac/user/assign-roles")]
|
||
public Task AssignRolesAsync(
|
||
[FromQuery] Guid userId,
|
||
[FromBody] List<Guid> roleIds,
|
||
CancellationToken cancellationToken = default) =>
|
||
userService.AssignRolesAsync(userId, roleIds, cancellationToken);
|
||
|
||
/// <summary>
|
||
/// 获取用户已分配角色。
|
||
/// </summary>
|
||
[HttpGet("/rbac/user/get-user-roles")]
|
||
public Task<List<RoleDto>> GetUserRolesAsync(
|
||
[FromQuery] Guid userId,
|
||
CancellationToken cancellationToken = default) =>
|
||
userService.GetUserRolesAsync(userId, cancellationToken);
|
||
|
||
/// <summary>
|
||
/// 获取用户权限码列表。
|
||
/// </summary>
|
||
[HttpGet("/rbac/user/get-user-permissions")]
|
||
public Task<List<string>> GetUserPermissionsAsync(
|
||
[FromQuery] Guid userId,
|
||
CancellationToken cancellationToken = default) =>
|
||
userService.GetUserPermissionsAsync(userId, cancellationToken);
|
||
|
||
private async Task<string> ResolveEmployeeNoForCreateAsync(
|
||
string? requestedEmployeeNo,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
var employeeNo = string.IsNullOrWhiteSpace(requestedEmployeeNo)
|
||
? await profileRepository.GenerateNextEmployeeNoAsync(cancellationToken)
|
||
: requestedEmployeeNo.Trim().ToUpperInvariant();
|
||
|
||
if (!UserProfileRepository.IsValidEmployeeNo(employeeNo))
|
||
{
|
||
throw new BusinessException($"工号「{employeeNo}」格式无效,应为 IDE + 至少 3 位数字,如 IDE001");
|
||
}
|
||
|
||
if (await profileRepository.IsEmployeeNoTakenAsync(employeeNo, null, cancellationToken))
|
||
{
|
||
throw new BusinessException($"工号「{employeeNo}」已存在,请更换工号后再创建");
|
||
}
|
||
|
||
return employeeNo;
|
||
}
|
||
|
||
private async Task EnsureEmployeeProfileAsync(
|
||
Guid userId,
|
||
OmsUserCreateInput input,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
var userIdText = userId.ToString();
|
||
if (await employeeRepository.ExistsByUserIdAsync(userIdText, null, cancellationToken))
|
||
{
|
||
return;
|
||
}
|
||
|
||
await employeeRepository.InsertAsync(
|
||
new Employee
|
||
{
|
||
UserId = userIdText,
|
||
RealName = string.IsNullOrWhiteSpace(input.Name) ? string.Empty : input.Name.Trim(),
|
||
HireDate = DateTime.Today,
|
||
EmploymentStatus = EmploymentStatus.Probation,
|
||
EmploymentType = EmploymentType.FullTime,
|
||
},
|
||
cancellationToken);
|
||
}
|
||
|
||
private OmsUserDetailDto MapUserDetail(UserDto user, UserProfileData? profile)
|
||
{
|
||
var detail = OmsUserProfileMapper.ToDetailDto(user, profile);
|
||
detail.Avatar = fileStorage.GetPublicUrl(detail.Avatar);
|
||
return detail;
|
||
}
|
||
}
|