333 lines
11 KiB
C#
333 lines
11 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using MilliSim.Common.Dtos;
|
||
using MilliSim.Common.Entities;
|
||
using MilliSim.Common.Models;
|
||
using MilliSim.Data;
|
||
using MilliSim.Services.Infrastructure;
|
||
|
||
namespace MilliSim.Services;
|
||
|
||
public class UserService : IUserService
|
||
{
|
||
private readonly MilliSimDbContext _db;
|
||
private readonly ICurrentUserService _currentUser;
|
||
private readonly IOperationLogService _operationLog;
|
||
private readonly INotificationService _notification;
|
||
private readonly IFileStorageService _fileStorage;
|
||
private readonly IUploadTrackingService _uploadTracking;
|
||
|
||
public UserService(
|
||
MilliSimDbContext db,
|
||
ICurrentUserService currentUser,
|
||
IOperationLogService operationLog,
|
||
INotificationService notification,
|
||
IFileStorageService fileStorage,
|
||
IUploadTrackingService uploadTracking)
|
||
{
|
||
_db = db;
|
||
_currentUser = currentUser;
|
||
_operationLog = operationLog;
|
||
_notification = notification;
|
||
_fileStorage = fileStorage;
|
||
_uploadTracking = uploadTracking;
|
||
}
|
||
|
||
public async Task<ApiResponse<PagedResult<UserDto>>> GetUsersAsync(UserQueryRequest request)
|
||
{
|
||
var query = _db.Users.AsQueryable();
|
||
|
||
// 销售数据隔离:销售只能看自己创建的客户
|
||
if (_currentUser.Role == UserRole.Sales)
|
||
{
|
||
query = query.Where(u => u.CreatedBy == _currentUser.UserId);
|
||
}
|
||
|
||
if (request.Role.HasValue)
|
||
{
|
||
query = query.Where(u => u.Role == request.Role.Value);
|
||
}
|
||
else if (request.StaffOnly)
|
||
{
|
||
// 员工管理页面:未指定具体角色时,排除客户角色
|
||
query = query.Where(u => u.Role != UserRole.Customer);
|
||
}
|
||
|
||
if (request.Status.HasValue)
|
||
{
|
||
query = query.Where(u => u.Status == request.Status.Value);
|
||
}
|
||
|
||
if (request.CreatedBy.HasValue)
|
||
{
|
||
query = query.Where(u => u.CreatedBy == request.CreatedBy.Value);
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(request.Keyword))
|
||
{
|
||
var keyword = request.Keyword.Trim();
|
||
query = query.Where(u =>
|
||
u.Username.Contains(keyword) ||
|
||
u.Nickname.Contains(keyword) ||
|
||
u.Phone.Contains(keyword) ||
|
||
u.Email.Contains(keyword));
|
||
}
|
||
|
||
var total = await query.LongCountAsync();
|
||
|
||
var users = await query
|
||
.OrderByDescending(u => u.CreatedAt)
|
||
.Skip((request.PageIndex - 1) * request.PageSize)
|
||
.Take(request.PageSize)
|
||
.ToListAsync();
|
||
|
||
// 预加载创建人名称
|
||
var creatorIds = users.Where(u => u.CreatedBy.HasValue).Select(u => u.CreatedBy!.Value).Distinct().ToList();
|
||
var creators = await _db.Users
|
||
.Where(u => creatorIds.Contains(u.Id))
|
||
.ToDictionaryAsync(u => u.Id, u => u.Nickname);
|
||
|
||
// 预加载客户授权信息(一个客户最多一条授权记录,历史数据可能多条,取最新一条)
|
||
var userIds = users.Select(u => u.Id).ToList();
|
||
var authList = await _db.Authorizations
|
||
.Where(a => userIds.Contains(a.UserId))
|
||
.ToListAsync();
|
||
var authDict = authList
|
||
.GroupBy(a => a.UserId)
|
||
.ToDictionary(g => g.Key, g => g.OrderByDescending(a => a.CreatedAt).First());
|
||
|
||
var items = users.Select(u =>
|
||
{
|
||
var dto = AuthService.MapToDto(u, _fileStorage);
|
||
if (u.CreatedBy.HasValue && creators.TryGetValue(u.CreatedBy.Value, out var name))
|
||
{
|
||
dto.CreatedByName = name;
|
||
}
|
||
if (authDict.TryGetValue(u.Id, out var auth))
|
||
{
|
||
dto.AuthorizationId = auth.Id;
|
||
dto.AuthorizationStatus = auth.Status;
|
||
dto.AuthorizationEndTime = auth.EndTime;
|
||
}
|
||
return dto;
|
||
}).ToList();
|
||
|
||
var result = PagedResult<UserDto>.Create(items, total, request.PageIndex, request.PageSize);
|
||
return ApiResponse<PagedResult<UserDto>>.Success(result);
|
||
}
|
||
|
||
public async Task<ApiResponse<UserDto>> GetUserByIdAsync(long id)
|
||
{
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||
}
|
||
|
||
// 销售数据隔离
|
||
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("无权查看该用户", 403);
|
||
}
|
||
|
||
var dto = AuthService.MapToDto(user, _fileStorage);
|
||
if (user.CreatedBy.HasValue)
|
||
{
|
||
var creator = await _db.Users.FirstOrDefaultAsync(u => u.Id == user.CreatedBy.Value);
|
||
if (creator != null)
|
||
{
|
||
dto.CreatedByName = creator.Nickname;
|
||
}
|
||
}
|
||
|
||
return ApiResponse<UserDto>.Success(dto);
|
||
}
|
||
|
||
public async Task<ApiResponse<UserDto>> CreateUserAsync(CreateUserRequest request)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(request.Username))
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户名不能为空", 400);
|
||
}
|
||
|
||
var exists = await _db.Users.AnyAsync(u => u.Username == request.Username);
|
||
if (exists)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户名已存在", 400);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(request.Password))
|
||
{
|
||
return ApiResponse<UserDto>.Fail("密码不能为空", 400);
|
||
}
|
||
|
||
var strengthError = AuthService.ValidatePasswordStrength(request.Password);
|
||
if (strengthError != null)
|
||
{
|
||
return ApiResponse<UserDto>.Fail(strengthError, 400);
|
||
}
|
||
|
||
var user = new User
|
||
{
|
||
Username = request.Username,
|
||
Password = BCryptHelper.HashPassword(request.Password),
|
||
Nickname = string.IsNullOrWhiteSpace(request.Nickname) ? request.Username : request.Nickname,
|
||
Phone = request.Phone ?? string.Empty,
|
||
Email = request.Email ?? string.Empty,
|
||
Avatar = _fileStorage.ExtractKey(request.Avatar ?? string.Empty),
|
||
Role = request.Role,
|
||
Status = UserStatus.Active,
|
||
CreatedBy = _currentUser.UserId
|
||
};
|
||
|
||
_db.Users.Add(user);
|
||
await _db.SaveChangesAsync();
|
||
|
||
// 消费上传跟踪(头像)
|
||
if (!string.IsNullOrEmpty(request.Avatar))
|
||
{
|
||
await _uploadTracking.ConsumeAsync(_fileStorage.ExtractKey(request.Avatar));
|
||
}
|
||
|
||
await _operationLog.LogAsync("用户管理", "创建用户", $"创建用户 {user.Username}({user.Role})");
|
||
|
||
// 给新用户发送欢迎通知
|
||
try
|
||
{
|
||
await _notification.CreateNotificationAsync(
|
||
NotificationType.Account,
|
||
"欢迎加入平台",
|
||
$"{user.Nickname},欢迎加入!您可以在项目中心浏览和体验已发布的项目,如有疑问请联系销售或客服。",
|
||
userId: user.Id,
|
||
eventId: $"user-welcome-{user.Id}");
|
||
}
|
||
catch
|
||
{
|
||
// 通知发送失败不影响用户创建结果
|
||
}
|
||
|
||
return ApiResponse<UserDto>.Success(AuthService.MapToDto(user, _fileStorage));
|
||
}
|
||
|
||
public async Task<ApiResponse<UserDto>> UpdateUserAsync(long id, UpdateUserRequest request)
|
||
{
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||
}
|
||
|
||
// 销售数据隔离
|
||
if (_currentUser.Role == UserRole.Sales && user.CreatedBy != _currentUser.UserId)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("无权修改该用户", 403);
|
||
}
|
||
|
||
// 记录旧头像 key(用于替换时删除旧文件)
|
||
var oldAvatarKey = _fileStorage.ExtractKey(user.Avatar);
|
||
var newAvatarKey = oldAvatarKey;
|
||
|
||
if (request.Nickname != null)
|
||
{
|
||
user.Nickname = request.Nickname;
|
||
}
|
||
if (request.Avatar != null)
|
||
{
|
||
user.Avatar = _fileStorage.ExtractKey(request.Avatar);
|
||
newAvatarKey = _fileStorage.ExtractKey(request.Avatar);
|
||
}
|
||
if (request.Phone != null)
|
||
{
|
||
user.Phone = request.Phone;
|
||
}
|
||
if (request.Email != null)
|
||
{
|
||
user.Email = request.Email;
|
||
}
|
||
|
||
// 更新状态(所有 Staff 角色均可修改)
|
||
user.Status = request.Status;
|
||
|
||
user.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
// 头像替换:删除旧文件
|
||
if (!string.IsNullOrEmpty(oldAvatarKey) && oldAvatarKey != newAvatarKey)
|
||
{
|
||
await _fileStorage.DeleteAsync(oldAvatarKey);
|
||
}
|
||
// 消费新上传的头像
|
||
if (request.Avatar != null)
|
||
{
|
||
await _uploadTracking.ConsumeAsync(newAvatarKey);
|
||
}
|
||
|
||
await _operationLog.LogAsync("用户管理", "更新用户", $"更新用户 {user.Username}");
|
||
|
||
return ApiResponse<UserDto>.Success(AuthService.MapToDto(user, _fileStorage));
|
||
}
|
||
|
||
public async Task<ApiResponse> DeleteUserAsync(long id)
|
||
{
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse.Fail("用户不存在", 404);
|
||
}
|
||
|
||
if (user.Id == _currentUser.UserId)
|
||
{
|
||
return ApiResponse.Fail("不能删除当前登录用户", 400);
|
||
}
|
||
|
||
// 软删除
|
||
user.IsDeleted = true;
|
||
user.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
await _operationLog.LogAsync("用户管理", "删除用户", $"删除用户 {user.Username}");
|
||
|
||
return ApiResponse.Success("删除成功");
|
||
}
|
||
|
||
public async Task<ApiResponse> ResetPasswordAsync(long id)
|
||
{
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse.Fail("用户不存在", 404);
|
||
}
|
||
|
||
const string defaultPassword = "hm123456";
|
||
user.Password = BCryptHelper.HashPassword(defaultPassword);
|
||
user.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
await _operationLog.LogAsync("用户管理", "重置密码", $"重置用户 {user.Username} 的密码");
|
||
|
||
return ApiResponse.Success("密码已重置为 hm123456");
|
||
}
|
||
|
||
public async Task<ApiResponse> ToggleStatusAsync(long id)
|
||
{
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == id);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse.Fail("用户不存在", 404);
|
||
}
|
||
|
||
if (user.Id == _currentUser.UserId)
|
||
{
|
||
return ApiResponse.Fail("不能禁用当前登录用户", 400);
|
||
}
|
||
|
||
user.Status = user.Status == UserStatus.Active ? UserStatus.Disabled : UserStatus.Active;
|
||
user.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
var action = user.Status == UserStatus.Active ? "启用" : "禁用";
|
||
await _operationLog.LogAsync("用户管理", "切换状态", $"{action}用户 {user.Username}");
|
||
|
||
return ApiResponse.Success($"{action}成功");
|
||
}
|
||
}
|