239 lines
7.5 KiB
C#
239 lines
7.5 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 AuthService : IAuthService
|
||
{
|
||
private readonly MilliSimDbContext _db;
|
||
private readonly IJwtService _jwtService;
|
||
private readonly ICaptchaService _captchaService;
|
||
private readonly ICurrentUserService _currentUser;
|
||
private readonly IFileStorageService _fileStorage;
|
||
private readonly IUploadTrackingService _uploadTracking;
|
||
|
||
public AuthService(
|
||
MilliSimDbContext db,
|
||
IJwtService jwtService,
|
||
ICaptchaService captchaService,
|
||
ICurrentUserService currentUser,
|
||
IFileStorageService fileStorage,
|
||
IUploadTrackingService uploadTracking)
|
||
{
|
||
_db = db;
|
||
_jwtService = jwtService;
|
||
_captchaService = captchaService;
|
||
_currentUser = currentUser;
|
||
_fileStorage = fileStorage;
|
||
_uploadTracking = uploadTracking;
|
||
}
|
||
|
||
public Task<ApiResponse<CaptchaResult>> GetCaptchaAsync()
|
||
{
|
||
var (captchaId, svg) = _captchaService.GenerateCaptcha();
|
||
var result = new CaptchaResult { CaptchaId = captchaId, Svg = svg };
|
||
return Task.FromResult(ApiResponse<CaptchaResult>.Success(result));
|
||
}
|
||
|
||
public async Task<ApiResponse<LoginResponse>> LoginAsync(LoginRequest request)
|
||
{
|
||
// 验证码校验
|
||
if (!_captchaService.ValidateCaptcha(request.CaptchaId, request.CaptchaCode))
|
||
{
|
||
return ApiResponse<LoginResponse>.Fail("验证码错误或已失效", 400);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
||
{
|
||
return ApiResponse<LoginResponse>.Fail("用户名或密码不能为空", 400);
|
||
}
|
||
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == request.Username);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse<LoginResponse>.Fail("用户名或密码错误", 400);
|
||
}
|
||
|
||
if (!BCryptHelper.VerifyPassword(request.Password, user.Password))
|
||
{
|
||
return ApiResponse<LoginResponse>.Fail("用户名或密码错误", 400);
|
||
}
|
||
|
||
if (user.Status == UserStatus.Disabled)
|
||
{
|
||
return ApiResponse<LoginResponse>.Fail("账号已被禁用,请联系管理员", 403);
|
||
}
|
||
|
||
var token = _jwtService.GenerateToken(user);
|
||
|
||
var response = new LoginResponse
|
||
{
|
||
Token = token,
|
||
UserId = user.Id,
|
||
Username = user.Username,
|
||
Nickname = user.Nickname,
|
||
Avatar = string.IsNullOrEmpty(user.Avatar) ? "" : _fileStorage.GetSignedUrl(user.Avatar),
|
||
Role = user.Role,
|
||
RoleName = user.Role.ToString()
|
||
};
|
||
|
||
return ApiResponse<LoginResponse>.Success(response);
|
||
}
|
||
|
||
public async Task<ApiResponse> ChangePasswordAsync(ChangePasswordRequest request)
|
||
{
|
||
var userId = _currentUser.UserId;
|
||
if (userId == 0)
|
||
{
|
||
return ApiResponse.Fail("用户未登录", 401);
|
||
}
|
||
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse.Fail("用户不存在", 404);
|
||
}
|
||
|
||
if (!BCryptHelper.VerifyPassword(request.OldPassword, user.Password))
|
||
{
|
||
return ApiResponse.Fail("原密码错误", 400);
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(request.NewPassword))
|
||
{
|
||
return ApiResponse.Fail("新密码不能为空", 400);
|
||
}
|
||
|
||
if (request.NewPassword == request.OldPassword)
|
||
{
|
||
return ApiResponse.Fail("新密码不能与原密码相同", 400);
|
||
}
|
||
|
||
var strengthError = ValidatePasswordStrength(request.NewPassword);
|
||
if (strengthError != null)
|
||
{
|
||
return ApiResponse.Fail(strengthError, 400);
|
||
}
|
||
|
||
user.Password = BCryptHelper.HashPassword(request.NewPassword);
|
||
user.UpdatedAt = DateTime.Now;
|
||
await _db.SaveChangesAsync();
|
||
|
||
return ApiResponse.Success("密码修改成功");
|
||
}
|
||
|
||
public async Task<ApiResponse<UserDto>> GetProfileAsync()
|
||
{
|
||
var userId = _currentUser.UserId;
|
||
if (userId == 0)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户未登录", 401);
|
||
}
|
||
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||
}
|
||
|
||
return ApiResponse<UserDto>.Success(MapToDto(user, _fileStorage));
|
||
}
|
||
|
||
public async Task<ApiResponse<UserDto>> UpdateProfileAsync(UpdateProfileRequest request)
|
||
{
|
||
var userId = _currentUser.UserId;
|
||
if (userId == 0)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户未登录", 401);
|
||
}
|
||
|
||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
||
if (user == null)
|
||
{
|
||
return ApiResponse<UserDto>.Fail("用户不存在", 404);
|
||
}
|
||
|
||
// 记录旧头像 key(用于替换时删除旧文件)
|
||
var oldAvatarKey = _fileStorage.ExtractKey(user.Avatar);
|
||
var newAvatarKey = oldAvatarKey;
|
||
|
||
if (!string.IsNullOrWhiteSpace(request.Nickname))
|
||
{
|
||
user.Nickname = request.Nickname;
|
||
}
|
||
if (request.Avatar != null)
|
||
{
|
||
// 存储 COS 对象键而非签名URL,避免 URL 过期后无法刷新
|
||
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;
|
||
}
|
||
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);
|
||
}
|
||
|
||
return ApiResponse<UserDto>.Success(MapToDto(user, _fileStorage));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 密码强度校验:8位以上含字母和数字
|
||
/// </summary>
|
||
internal static string? ValidatePasswordStrength(string password)
|
||
{
|
||
if (password.Length < 8)
|
||
{
|
||
return "密码长度不能少于8位";
|
||
}
|
||
if (!password.Any(char.IsLetter))
|
||
{
|
||
return "密码必须包含字母";
|
||
}
|
||
if (!password.Any(char.IsDigit))
|
||
{
|
||
return "密码必须包含数字";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
internal static UserDto MapToDto(User user, IFileStorageService fileStorage)
|
||
{
|
||
return new UserDto
|
||
{
|
||
Id = user.Id,
|
||
Username = user.Username,
|
||
Nickname = user.Nickname,
|
||
// 头像存储为 COS 对象键,读取时刷新为签名URL
|
||
Avatar = string.IsNullOrEmpty(user.Avatar) ? "" : fileStorage.GetSignedUrl(user.Avatar),
|
||
Phone = user.Phone,
|
||
Email = user.Email,
|
||
Role = user.Role,
|
||
RoleName = user.Role.ToString(),
|
||
Status = user.Status,
|
||
CreatedBy = user.CreatedBy,
|
||
CreatedAt = user.CreatedAt
|
||
};
|
||
}
|
||
}
|