Browse Source

添加登录日志功能和相关配置

norm
hyh 3 months ago
parent
commit
ff634ad4aa
  1. 25
      scripts/update-database.ps1
  2. 1
      src/CellularManagement.Application/CellularManagement.Application.csproj
  3. 2
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommand.cs
  4. 125
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandHandler.cs
  5. 4
      src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandValidator.cs
  6. 2
      src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequest.cs
  7. 28
      src/CellularManagement.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs
  8. 17
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommand.cs
  9. 8
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandHandler.cs
  10. 13
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandValidator.cs
  11. 12
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserResponse.cs
  12. 7
      src/CellularManagement.Application/Features/Auth/Common/UserInfo.cs
  13. 2
      src/CellularManagement.Application/Features/Users/Commands/CreateUser/CreateUserCommand.cs
  14. 10
      src/CellularManagement.Application/Features/Users/Commands/CreateUser/CreateUserCommandValidator.cs
  15. 4
      src/CellularManagement.Application/Features/Users/Commands/UpdateUser/UpdateUserCommand.cs
  16. 5
      src/CellularManagement.Application/Features/Users/Commands/UpdateUser/UpdateUserCommandHandler.cs
  17. 9
      src/CellularManagement.Application/Features/Users/Commands/UpdateUser/UpdateUserCommandValidator.cs
  18. 10
      src/CellularManagement.Domain/Entities/AppUser.cs
  19. 59
      src/CellularManagement.Domain/Entities/LoginLog.cs
  20. 31
      src/CellularManagement.Domain/Repositories/ILoginLogRepository.cs
  21. 6
      src/CellularManagement.Domain/ValueObjects/UserName.cs
  22. 11
      src/CellularManagement.Infrastructure/Configurations/AppUserConfiguration.cs
  23. 80
      src/CellularManagement.Infrastructure/Configurations/LoginLogConfiguration.cs
  24. 36
      src/CellularManagement.Infrastructure/Context/AppDbContext.cs
  25. 6
      src/CellularManagement.Infrastructure/DependencyInjection.cs
  26. 314
      src/CellularManagement.Infrastructure/Migrations/20250520034505_AddLastLoginTime.Designer.cs
  27. 30
      src/CellularManagement.Infrastructure/Migrations/20250520034505_AddLastLoginTime.cs
  28. 396
      src/CellularManagement.Infrastructure/Migrations/20250520044439_AddLoginLog.Designer.cs
  29. 69
      src/CellularManagement.Infrastructure/Migrations/20250520044439_AddLoginLog.cs
  30. 401
      src/CellularManagement.Infrastructure/Migrations/20250520060054_AddRealNameToAppUser.Designer.cs
  31. 30
      src/CellularManagement.Infrastructure/Migrations/20250520060054_AddRealNameToAppUser.cs
  32. 401
      src/CellularManagement.Infrastructure/Migrations/20250520060351_UpdateUserComments.Designer.cs
  33. 94
      src/CellularManagement.Infrastructure/Migrations/20250520060351_UpdateUserComments.cs
  34. 95
      src/CellularManagement.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
  35. 84
      src/CellularManagement.Infrastructure/Repositories/LoginLogRepository.cs
  36. 16
      src/CellularManagement.Infrastructure/Services/UserRegistrationService.cs
  37. 81
      update-database.ps1

25
scripts/update-database.ps1

@ -0,0 +1,25 @@
# 设置错误时停止执行
$ErrorActionPreference = "Stop"
# 设置项目路径
$projectPath = "src/CellularManagement.Infrastructure"
$startupProjectPath = "src/CellularManagement.WebAPI"
# 设置迁移名称
$migrationName = "AddLoginLogs"
# 检查是否已存在迁移
$migrations = dotnet ef migrations list --project $projectPath --startup-project $startupProjectPath
if ($migrations -match $migrationName) {
Write-Host "迁移 '$migrationName' 已存在,跳过创建迁移步骤"
} else {
# 创建迁移
Write-Host "正在创建迁移 '$migrationName'..."
dotnet ef migrations add $migrationName --project $projectPath --startup-project $startupProjectPath
}
# 更新数据库
Write-Host "正在更新数据库..."
dotnet ef database update --project $projectPath --startup-project $startupProjectPath
Write-Host "数据库更新完成!"

1
src/CellularManagement.Application/CellularManagement.Application.csproj

@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.8" />
<PackageReference Include="UAParser" Version="3.0.0" />
</ItemGroup>
<PropertyGroup>

2
src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommand.cs

@ -8,7 +8,7 @@ namespace CellularManagement.Application.Features.Auth.Commands.AuthenticateUser
/// </summary>
public sealed record AuthenticateUserCommand(
/// <summary>
/// 用户名或邮箱
/// 账号或邮箱
/// </summary>
string UserName,

125
src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandHandler.cs

@ -13,6 +13,9 @@ using System.Threading;
using System.Linq;
using System;
using CellularManagement.Domain.Common;
using Microsoft.AspNetCore.Http;
using UAParser;
using Microsoft.EntityFrameworkCore;
namespace CellularManagement.Application.Features.Auth.Commands.AuthenticateUser;
@ -26,6 +29,10 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
private readonly ILogger<AuthenticateUserCommandHandler> _logger;
private readonly IUserRoleRepository _userRoleRepository;
private readonly IRolePermissionRepository _rolePermissionRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly ILoginLogRepository _loginLogRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private const int MaxRetryAttempts = 3;
/// <summary>
/// 初始化处理器
@ -35,13 +42,19 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
IJwtProvider jwtProvider,
ILogger<AuthenticateUserCommandHandler> logger,
IUserRoleRepository userRoleRepository,
IRolePermissionRepository rolePermissionRepository)
IRolePermissionRepository rolePermissionRepository,
IUnitOfWork unitOfWork,
ILoginLogRepository loginLogRepository,
IHttpContextAccessor httpContextAccessor)
{
_userManager = userManager;
_jwtProvider = jwtProvider;
_logger = logger;
_userRoleRepository = userRoleRepository;
_rolePermissionRepository = rolePermissionRepository;
_unitOfWork = unitOfWork;
_loginLogRepository = loginLogRepository;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
@ -53,6 +66,21 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
{
try
{
var httpContext = _httpContextAccessor.HttpContext;
var ipAddress = httpContext?.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
// 检查IP是否被限制
if (await _loginLogRepository.IsIpRestrictedAsync(ipAddress, cancellationToken))
{
_logger.LogWarning("IP {IpAddress} 已被限制登录", ipAddress);
return OperationResult<AuthenticateUserResponse>.CreateFailure("登录尝试次数过多,请稍后再试");
}
// 解析设备信息
var parser = UAParser.Parser.GetDefault();
var clientInfo = parser.Parse(userAgent);
// 先尝试通过邮箱查找用户
AppUser? user = null;
if (request.UserName.IsValidEmail())
@ -60,26 +88,106 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
user = await _userManager.FindByEmailAsync(request.UserName);
}
// 如果通过邮箱没找到用户,则尝试通过用户名查找
// 如果通过邮箱没找到用户,则尝试通过账号查找
if (user == null)
{
user = await _userManager.FindByNameAsync(request.UserName);
}
// 创建登录日志
var loginLog = new LoginLog
{
Id = Guid.NewGuid(),
UserId = user?.Id ?? "Unknown",
LoginTime = DateTime.UtcNow,
IpAddress = ipAddress,
UserAgent = userAgent,
Browser = clientInfo.UA.ToString(),
OperatingSystem = clientInfo.OS.ToString()
};
if (user == null)
{
loginLog.IsSuccess = false;
loginLog.FailureReason = "用户不存在";
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserName} 不存在", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户名或密码错误");
}
// 检查用户是否已删除
if (user.IsDeleted)
{
loginLog.IsSuccess = false;
loginLog.FailureReason = "用户已被删除";
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserName} 已被删除", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户已被删除");
}
// 检查用户是否已禁用
if (!user.IsActive)
{
loginLog.IsSuccess = false;
loginLog.FailureReason = "用户已被禁用";
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserName} 已被禁用", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户已被禁用");
}
// 验证密码
var isValidPassword = await _userManager.CheckPasswordAsync(user, request.Password);
if (!isValidPassword)
{
loginLog.IsSuccess = false;
loginLog.FailureReason = "密码错误";
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
_logger.LogWarning("用户 {UserName} 密码错误", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户名或密码错误");
}
// 更新最后登录时间(带并发控制)
var retryCount = 0;
var updateSuccess = false;
while (!updateSuccess && retryCount < MaxRetryAttempts)
{
try
{
await _unitOfWork.ExecuteTransactionAsync(async () =>
{
user.LastLoginTime = DateTime.UtcNow;
var result = await _userManager.UpdateAsync(user);
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("更新用户最后登录时间失败: {Errors}", string.Join(", ", errors));
throw new InvalidOperationException(string.Join(", ", errors));
}
}, cancellationToken: cancellationToken);
updateSuccess = true;
}
catch (DbUpdateConcurrencyException)
{
retryCount++;
_logger.LogWarning("用户 {UserName} 更新时发生并发冲突,重试次数: {RetryCount}",
request.UserName, retryCount);
if (retryCount >= MaxRetryAttempts)
{
_logger.LogError("用户 {UserName} 更新失败,超过最大重试次数", request.UserName);
return OperationResult<AuthenticateUserResponse>.CreateFailure("系统繁忙,请稍后重试");
}
// 重新获取最新数据
user = await _userManager.FindByIdAsync(user.Id);
if (user == null)
{
return OperationResult<AuthenticateUserResponse>.CreateFailure("用户不存在");
}
}
}
// 获取用户角色
var roles = await _userRoleRepository.GetUserRolesAsync(user.Id, cancellationToken);
@ -91,6 +199,12 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
new(ClaimTypes.Email, user.Email!)
};
// 添加最后登录时间声明(如果存在)
if (user.LastLoginTime.HasValue)
{
claims.Add(new Claim("LastLoginTime", user.LastLoginTime.Value.ToUniversalTime().ToString("o")));
}
// 添加角色声明
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
@ -121,12 +235,17 @@ public sealed class AuthenticateUserCommandHandler : IRequestHandler<Authenticat
var userInfo = new UserInfo(
user.Id,
user.UserName!,
user.RealName,
user.Email!,
user.PhoneNumber,
roles,
permissions);
_logger.LogInformation("用户 {UserName} 认证成功", request.UserName);
// 记录成功的登录日志
loginLog.IsSuccess = true;
await _loginLogRepository.AddAsync(loginLog, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
_logger.LogInformation("账号 {UserName} 认证成功", request.UserName);
// 返回认证结果
return OperationResult<AuthenticateUserResponse>.CreateSuccess(

4
src/CellularManagement.Application/Features/Auth/Commands/AuthenticateUser/AuthenticateUserCommandValidator.cs

@ -16,8 +16,8 @@ public sealed class AuthenticateUserCommandValidator : AbstractValidator<Authent
{
// 验证用户名或邮箱
RuleFor(x => x.UserName)
.NotEmpty().WithMessage("用户名不能为空")
.MaximumLength(256).WithMessage("用户名长度不能超过256个字符")
.NotEmpty().WithMessage("账号不能为空")
.MaximumLength(256).WithMessage("账号长度不能超过256个字符")
.Must(x => x.Contains('@') ? x.IsValidEmail() : true)
.WithMessage("邮箱格式不正确");

2
src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequest.cs

@ -10,7 +10,7 @@ public class LoginRequest
/// <summary>
/// 用户名
/// </summary>
[Required(ErrorMessage = "用户名不能为空")]
[Required(ErrorMessage = "账号不能为空")]
public string UserName { get; set; } = string.Empty;
/// <summary>

28
src/CellularManagement.Application/Features/Auth/Commands/RefreshToken/RefreshTokenCommandHandler.cs

@ -18,11 +18,12 @@ namespace CellularManagement.Application.Features.Auth.Commands.RefreshToken;
/// <summary>
/// 刷新令牌命令处理器
/// </summary>
public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, OperationResult<AuthenticateUserResponse>>
public sealed class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, OperationResult<AuthenticateUserResponse>>
{
private readonly IJwtProvider _jwtProvider;
private readonly ILogger<RefreshTokenCommandHandler> _logger;
private readonly IUserRoleRepository _userRoleRepository;
private readonly IRolePermissionRepository _rolePermissionRepository;
/// <summary>
/// 构造函数
@ -30,14 +31,17 @@ public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, O
/// <param name="jwtProvider">JWT提供者</param>
/// <param name="logger">日志记录器</param>
/// <param name="userRoleRepository">用户角色仓储</param>
/// <param name="rolePermissionRepository">角色权限仓储</param>
public RefreshTokenCommandHandler(
IJwtProvider jwtProvider,
ILogger<RefreshTokenCommandHandler> logger,
IUserRoleRepository userRoleRepository)
IUserRoleRepository userRoleRepository,
IRolePermissionRepository rolePermissionRepository)
{
_jwtProvider = jwtProvider;
_logger = logger;
_userRoleRepository = userRoleRepository;
_rolePermissionRepository = rolePermissionRepository;
}
/// <summary>
@ -95,6 +99,20 @@ public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, O
// 获取用户角色
var roles = await _userRoleRepository.GetUserRolesAsync(userId, cancellationToken);
// 获取所有角色的权限
var permissions = new Dictionary<string, bool>();
foreach (var role in roles)
{
var rolePermissions = await _rolePermissionRepository.GetRolePermissionsWithDetailsAsync(role, cancellationToken);
foreach (var rolePermission in rolePermissions)
{
if (!permissions.ContainsKey(rolePermission.Permission.Code))
{
permissions[rolePermission.Permission.Code] = true;
}
}
}
// 生成新的访问令牌
var accessToken = _jwtProvider.GenerateAccessToken(claims);
@ -106,14 +124,16 @@ public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, O
// 获取令牌过期时间
var expiresAt = _jwtProvider.GetTokenExpiration(accessToken);
var permissions = new Dictionary<string, bool>();
// 创建用户信息
var userInfo = new UserInfo(
userId,
claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? string.Empty,
claims.FirstOrDefault(c => c.Type == "RealName")?.Value,
claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? string.Empty,
claims.FirstOrDefault(c => c.Type == ClaimTypes.MobilePhone)?.Value,
roles, permissions);
roles,
permissions);
_logger.LogInformation("刷新令牌成功");
return OperationResult<AuthenticateUserResponse>.CreateSuccess(

17
src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommand.cs

@ -8,25 +8,30 @@ namespace CellularManagement.Application.Features.Auth.Commands.RegisterUser;
/// </summary>
public sealed record RegisterUserCommand(
/// <summary>
/// 用户名
/// 账号
/// </summary>
string UserName,
/// <summary>
/// 用户名
/// </summary>
string RealName,
/// <summary>
/// 邮箱
/// </summary>
string Email,
/// <summary>
/// 密码
/// </summary>
string Password,
/// <summary>
/// 确认密码
/// </summary>
string ConfirmPassword,
/// <summary>
/// 电话号码
/// </summary>
@ -38,6 +43,6 @@ public sealed record RegisterUserCommand(
string CaptchaId,
/// <summary>
/// 用户输入的验证码
/// 验证码
/// </summary>
string CaptchaCode) : IRequest<OperationResult<RegisterUserResponse>>;

8
src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandHandler.cs

@ -75,6 +75,7 @@ public sealed class RegisterUserCommandHandler : IRequestHandler<RegisterUserCom
var user = new AppUser
{
UserName = request.UserName,
RealName = request.RealName,
Email = request.Email,
PhoneNumber = request.PhoneNumber
};
@ -103,11 +104,14 @@ public sealed class RegisterUserCommandHandler : IRequestHandler<RegisterUserCom
_logger.LogInformation("用户 {UserName} 注册成功", request.UserName);
return OperationResult<RegisterUserResponse>.CreateSuccess(
new RegisterUserResponse(user.Id));
new RegisterUserResponse(
Id: user.Id,
UserName: user.UserName,
RealName: user.RealName ?? user.UserName));
}
catch (UserNameAlreadyExistsException ex)
{
_logger.LogWarning(ex, "用户名已存在");
_logger.LogWarning(ex, "账号已存在");
return OperationResult<RegisterUserResponse>.CreateFailure(ex.Message);
}
catch (EmailAlreadyExistsException ex)

13
src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandValidator.cs

@ -13,12 +13,17 @@ public sealed class RegisterUserCommandValidator : AbstractValidator<RegisterUse
/// </summary>
public RegisterUserCommandValidator()
{
// 验证用户名
// 验证账号
RuleFor(x => x.UserName)
.NotEmpty().WithMessage("账号不能为空")
.MinimumLength(3).WithMessage("账号长度不能少于3个字符")
.MaximumLength(50).WithMessage("账号长度不能超过50个字符")
.Matches("^[a-zA-Z0-9_]+$").WithMessage("账号只能包含字母、数字和下划线");
// 验证用户名
RuleFor(x => x.RealName)
.NotEmpty().WithMessage("用户名不能为空")
.MinimumLength(3).WithMessage("用户名长度不能少于3个字符")
.MaximumLength(50).WithMessage("用户名长度不能超过50个字符")
.Matches("^[a-zA-Z0-9_]+$").WithMessage("用户名只能包含字母、数字和下划线");
.MaximumLength(50).WithMessage("用户名长度不能超过50个字符");
// 验证邮箱
RuleFor(x => x.Email)

12
src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserResponse.cs

@ -7,4 +7,14 @@ public sealed record RegisterUserResponse(
/// <summary>
/// 用户ID
/// </summary>
string UserId);
string Id,
/// <summary>
/// 账号
/// </summary>
string UserName,
/// <summary>
/// 用户名
/// </summary>
string RealName);

7
src/CellularManagement.Application/Features/Auth/Common/UserInfo.cs

@ -12,10 +12,15 @@ public sealed record UserInfo(
string Id,
/// <summary>
/// 用户名
/// 账号
/// </summary>
string UserName,
/// <summary>
/// 用户名
/// </summary>
string? RealName,
/// <summary>
/// 邮箱
/// </summary>

2
src/CellularManagement.Application/Features/Users/Commands/CreateUser/CreateUserCommand.cs

@ -7,7 +7,7 @@ namespace CellularManagement.Application.Features.Users.Commands.CreateUser;
/// 创建用户命令
/// 用于创建新用户的命令对象
/// </summary>
/// <param name="UserName">用户名</param>
/// <param name="UserName">账号</param>
/// <param name="Email">电子邮箱</param>
/// <param name="PhoneNumber">电话号码</param>
/// <param name="Password">密码</param>

10
src/CellularManagement.Application/Features/Users/Commands/CreateUser/CreateUserCommandValidator.cs

@ -7,12 +7,12 @@ public sealed class CreateUserCommandValidator : AbstractValidator<CreateUserCom
{
public CreateUserCommandValidator()
{
// 验证用户名
// 验证账号
RuleFor(x => x.UserName)
.NotEmpty().WithMessage("用户名不能为空")
.MinimumLength(3).WithMessage("用户名长度不能少于3个字符")
.MaximumLength(50).WithMessage("用户名长度不能超过50个字符")
.Matches("^[a-zA-Z0-9_]+$").WithMessage("用户名只能包含字母、数字和下划线");
.NotEmpty().WithMessage("账号不能为空")
.MinimumLength(3).WithMessage("账号长度不能少于3个字符")
.MaximumLength(50).WithMessage("账号长度不能超过50个字符")
.Matches("^[a-zA-Z0-9_]+$").WithMessage("账号只能包含字母、数字和下划线");
// 验证邮箱
RuleFor(x => x.Email)

4
src/CellularManagement.Application/Features/Users/Commands/UpdateUser/UpdateUserCommand.cs

@ -8,11 +8,13 @@ namespace CellularManagement.Application.Features.Users.Commands.UpdateUser;
/// 用于更新指定用户信息的命令对象
/// </summary>
/// <param name="UserId">要更新的用户ID</param>
/// <param name="UserName">新的用户名</param>
/// <param name="UserName">新的账号</param>
/// <param name="RealName">新的用户名</param>
/// <param name="Email">新的电子邮箱</param>
/// <param name="PhoneNumber">新的电话号码</param>
public sealed record UpdateUserCommand(
string UserId,
string UserName,
string RealName,
string Email,
string PhoneNumber) : IRequest<OperationResult<UpdateUserResponse>>;

5
src/CellularManagement.Application/Features/Users/Commands/UpdateUser/UpdateUserCommandHandler.cs

@ -37,11 +37,12 @@ public sealed class UpdateUserCommandHandler : IRequestHandler<UpdateUserCommand
if (user == null)
{
_logger.LogWarning("User {UserId} not found", request.UserId);
return OperationResult<UpdateUserResponse>.CreateFailure("User not found");
return OperationResult<UpdateUserResponse>.CreateFailure("用户不存在");
}
// Update user properties if provided
if (request.UserName != null) user.UserName = request.UserName;
if (request.RealName != null) user.RealName = request.RealName;
if (request.Email != null) user.Email = request.Email;
if (request.PhoneNumber != null) user.PhoneNumber = request.PhoneNumber;
@ -65,7 +66,7 @@ public sealed class UpdateUserCommandHandler : IRequestHandler<UpdateUserCommand
catch (Exception ex)
{
_logger.LogError(ex, "Error updating user");
return OperationResult<UpdateUserResponse>.CreateFailure("Failed to update user");
return OperationResult<UpdateUserResponse>.CreateFailure("更新用户失败");
}
}
}

9
src/CellularManagement.Application/Features/Users/Commands/UpdateUser/UpdateUserCommandValidator.cs

@ -12,7 +12,14 @@ public sealed class UpdateUserCommandValidator : AbstractValidator<UpdateUserCom
When(x => x.UserName != null, () =>
{
RuleFor(x => x.UserName)
.Length(3, 50).WithMessage("用户名长度必须在3-50个字符之间");
.Length(3, 50).WithMessage("账号长度必须在3-50个字符之间")
.Matches("^[a-zA-Z0-9_]+$").WithMessage("账号只能包含字母、数字和下划线");
});
When(x => x.RealName != null, () =>
{
RuleFor(x => x.RealName)
.MaximumLength(50).WithMessage("用户名长度不能超过50个字符");
});
When(x => x.Email != null, () =>

10
src/CellularManagement.Domain/Entities/AppUser.cs

@ -28,6 +28,16 @@ public sealed class AppUser : BaseIdentityUser
/// </summary>
public bool IsDeleted { get; set; }
/// <summary>
/// 最后登录时间
/// </summary>
public DateTime? LastLoginTime { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string? RealName { get; set; }
/// <summary>
/// 更新用户信息
/// </summary>

59
src/CellularManagement.Domain/Entities/LoginLog.cs

@ -0,0 +1,59 @@
using System;
namespace CellularManagement.Domain.Entities;
/// <summary>
/// 用户登录日志
/// </summary>
public class LoginLog
{
/// <summary>
/// 日志ID
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
public string UserId { get; set; }
/// <summary>
/// 登录时间
/// </summary>
public DateTime LoginTime { get; set; }
/// <summary>
/// 登录IP
/// </summary>
public string IpAddress { get; set; }
/// <summary>
/// 设备信息
/// </summary>
public string UserAgent { get; set; }
/// <summary>
/// 登录状态(成功/失败)
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 失败原因
/// </summary>
public string? FailureReason { get; set; }
/// <summary>
/// 登录位置
/// </summary>
public string? Location { get; set; }
/// <summary>
/// 浏览器信息
/// </summary>
public string? Browser { get; set; }
/// <summary>
/// 操作系统信息
/// </summary>
public string? OperatingSystem { get; set; }
}

31
src/CellularManagement.Domain/Repositories/ILoginLogRepository.cs

@ -0,0 +1,31 @@
using System.Threading;
using System.Threading.Tasks;
using CellularManagement.Domain.Entities;
namespace CellularManagement.Domain.Repositories;
/// <summary>
/// 登录日志仓储接口
/// </summary>
public interface ILoginLogRepository
{
/// <summary>
/// 添加登录日志
/// </summary>
Task AddAsync(LoginLog log, CancellationToken cancellationToken = default);
/// <summary>
/// 获取用户最近的登录日志
/// </summary>
Task<LoginLog[]> GetRecentLogsAsync(string userId, int count, CancellationToken cancellationToken = default);
/// <summary>
/// 检查IP是否被限制
/// </summary>
Task<bool> IsIpRestrictedAsync(string ipAddress, CancellationToken cancellationToken = default);
/// <summary>
/// 获取IP的失败登录次数
/// </summary>
Task<int> GetIpFailureCountAsync(string ipAddress, CancellationToken cancellationToken = default);
}

6
src/CellularManagement.Domain/ValueObjects/UserName.cs

@ -5,7 +5,7 @@ using CellularManagement.Domain.Common;
namespace CellularManagement.Domain.ValueObjects;
/// <summary>
/// 用户名值对象
/// 账号值对象
/// </summary>
public class UserName : ValueObject
{
@ -23,12 +23,12 @@ public class UserName : ValueObject
{
if (string.IsNullOrWhiteSpace(userName))
{
return Result<UserName>.Failure("用户名不能为空");
return Result<UserName>.Failure("账号不能为空");
}
if (!UserNameRegex.IsMatch(userName))
{
return Result<UserName>.Failure("用户名只能包含字母、数字和下划线,长度在3-50个字符之间");
return Result<UserName>.Failure("账号只能包含字母、数字和下划线,长度在3-50个字符之间");
}
return Result<UserName>.Success(new UserName(userName));

11
src/CellularManagement.Infrastructure/Configurations/AppUserConfiguration.cs

@ -33,11 +33,11 @@ public sealed class AppUserConfiguration : IEntityTypeConfiguration<AppUser>
builder.Property(u => u.UserName)
.IsRequired()
.HasMaxLength(256)
.HasComment("用户名");
.HasComment("账号");
builder.Property(u => u.NormalizedUserName)
.HasMaxLength(256)
.HasComment("标准化用户名(大写)");
.HasComment("标准化账号(大写)");
builder.Property(u => u.Email)
.IsRequired()
@ -80,6 +80,10 @@ public sealed class AppUserConfiguration : IEntityTypeConfiguration<AppUser>
builder.Property(u => u.AccessFailedCount)
.HasComment("登录失败次数");
builder.Property(u => u.RealName)
.HasMaxLength(50)
.HasComment("用户名");
// 配置审计字段
builder.Property(u => u.CreatedTime)
.IsRequired()
@ -98,6 +102,9 @@ public sealed class AppUserConfiguration : IEntityTypeConfiguration<AppUser>
.HasDefaultValue(false)
.HasComment("是否已删除");
builder.Property(u => u.LastLoginTime)
.HasComment("最后登录时间");
// 添加软删除过滤器
builder.HasQueryFilter(u => !u.IsDeleted);
}

80
src/CellularManagement.Infrastructure/Configurations/LoginLogConfiguration.cs

@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using CellularManagement.Domain.Entities;
namespace CellularManagement.Infrastructure.Configurations;
/// <summary>
/// LoginLog 实体配置类
/// 用于配置登录日志实体在数据库中的映射关系
/// </summary>
public sealed class LoginLogConfiguration : IEntityTypeConfiguration<LoginLog>
{
/// <summary>
/// 配置 LoginLog 实体
/// </summary>
/// <param name="builder">实体类型构建器</param>
public void Configure(EntityTypeBuilder<LoginLog> builder)
{
// 配置表名
builder.ToTable("LoginLogs", t => t.HasComment("用户登录日志表"));
// 配置主键
builder.HasKey(l => l.Id);
// 配置索引
builder.HasIndex(l => l.UserId).HasDatabaseName("IX_LoginLogs_UserId");
builder.HasIndex(l => l.LoginTime).HasDatabaseName("IX_LoginLogs_LoginTime");
builder.HasIndex(l => l.IpAddress).HasDatabaseName("IX_LoginLogs_IpAddress");
builder.HasIndex(l => new { l.UserId, l.LoginTime }).HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
// 配置属性
builder.Property(l => l.Id)
.HasComment("日志ID");
builder.Property(l => l.UserId)
.IsRequired()
.HasMaxLength(450)
.HasComment("用户ID");
builder.Property(l => l.LoginTime)
.IsRequired()
.HasComment("登录时间");
builder.Property(l => l.IpAddress)
.IsRequired()
.HasMaxLength(50)
.HasComment("登录IP");
builder.Property(l => l.UserAgent)
.IsRequired()
.HasMaxLength(500)
.HasComment("设备信息");
builder.Property(l => l.IsSuccess)
.IsRequired()
.HasComment("登录状态(成功/失败)");
builder.Property(l => l.FailureReason)
.HasMaxLength(200)
.HasComment("失败原因");
builder.Property(l => l.Location)
.HasMaxLength(200)
.HasComment("登录位置");
builder.Property(l => l.Browser)
.HasMaxLength(100)
.HasComment("浏览器信息");
builder.Property(l => l.OperatingSystem)
.HasMaxLength(100)
.HasComment("操作系统信息");
// 配置外键关系
builder.HasOne<AppUser>()
.WithMany()
.HasForeignKey(l => l.UserId)
.OnDelete(DeleteBehavior.Restrict);
}
}

36
src/CellularManagement.Infrastructure/Context/AppDbContext.cs

@ -29,6 +29,11 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
/// </summary>
public DbSet<RolePermission> RolePermissions { get; set; }
/// <summary>
/// 用户登录日志
/// </summary>
public DbSet<LoginLog> LoginLogs { get; set; }
/// <summary>
/// 初始化数据库上下文
/// </summary>
@ -89,5 +94,36 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
.HasForeignKey(rp => rp.PermissionId)
.OnDelete(DeleteBehavior.Cascade);
});
// 配置登录日志实体
modelBuilder.Entity<LoginLog>(entity =>
{
entity.ToTable("LoginLogs", t => t.HasComment("用户登录日志表"));
entity.HasKey(l => l.Id);
// 配置索引
entity.HasIndex(l => l.UserId).HasDatabaseName("IX_LoginLogs_UserId");
entity.HasIndex(l => l.LoginTime).HasDatabaseName("IX_LoginLogs_LoginTime");
entity.HasIndex(l => l.IpAddress).HasDatabaseName("IX_LoginLogs_IpAddress");
entity.HasIndex(l => new { l.UserId, l.LoginTime }).HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
// 配置属性
entity.Property(l => l.Id).HasComment("日志ID");
entity.Property(l => l.UserId).IsRequired().HasMaxLength(450).HasComment("用户ID");
entity.Property(l => l.LoginTime).IsRequired().HasComment("登录时间");
entity.Property(l => l.IpAddress).IsRequired().HasMaxLength(50).HasComment("登录IP");
entity.Property(l => l.UserAgent).IsRequired().HasMaxLength(500).HasComment("设备信息");
entity.Property(l => l.IsSuccess).IsRequired().HasComment("登录状态(成功/失败)");
entity.Property(l => l.FailureReason).HasMaxLength(200).HasComment("失败原因");
entity.Property(l => l.Location).HasMaxLength(200).HasComment("登录位置");
entity.Property(l => l.Browser).HasMaxLength(100).HasComment("浏览器信息");
entity.Property(l => l.OperatingSystem).HasMaxLength(100).HasComment("操作系统信息");
// 配置外键关系
entity.HasOne<AppUser>()
.WithMany()
.HasForeignKey(l => l.UserId)
.OnDelete(DeleteBehavior.Restrict);
});
}
}

6
src/CellularManagement.Infrastructure/DependencyInjection.cs

@ -110,6 +110,9 @@ public static class DependencyInjection
// 注册权限仓储
services.AddScoped<IPermissionRepository, PermissionRepository>();
// 注册登录日志仓储
services.AddScoped<ILoginLogRepository, LoginLogRepository>();
// 添加内存缓存
services.AddMemoryCache();
@ -123,6 +126,9 @@ public static class DependencyInjection
// 配置认证设置
services.Configure<AuthConfiguration>(configuration.GetSection("Auth"));
// 添加 IHttpContextAccessor 注册
services.AddHttpContextAccessor();
// 自动注册服务
services
.Scan(action =>

314
src/CellularManagement.Infrastructure/Migrations/20250520034505_AddLastLoginTime.Designer.cs

@ -0,0 +1,314 @@
// <auto-generated />
using System;
using CellularManagement.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250520034505_AddLastLoginTime")]
partial class AddLastLoginTime
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("CellularManagement.Domain.Entities.AppRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("角色ID,主键");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("角色描述");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("角色名称");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化角色名称(大写)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("IX_Roles_Name");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", null, t =>
{
t.HasComment("角色表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("用户ID,主键");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer")
.HasComment("登录失败次数");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("电子邮箱");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasComment("邮箱是否已验证");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("用户状态(true: 启用, false: 禁用)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasComment("是否已删除");
b.Property<DateTime?>("LastLoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("最后登录时间");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone")
.HasComment("账户锁定结束时间");
b.Property<DateTime?>("ModifiedTime")
.HasColumnType("timestamp with time zone")
.HasComment("修改时间");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化电子邮箱(大写)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化用户名(大写)");
b.Property<string>("PasswordHash")
.HasColumnType("text")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text")
.HasComment("电话号码");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean")
.HasComment("电话号码是否已验证");
b.Property<string>("SecurityStamp")
.HasColumnType("text")
.HasComment("安全戳,用于并发控制");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean")
.HasComment("是否启用双因素认证");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("用户名");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("IX_Users_Email");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PhoneNumber")
.IsUnique()
.HasDatabaseName("IX_Users_PhoneNumber");
b.HasIndex("UserName")
.IsUnique()
.HasDatabaseName("IX_Users_UserName");
b.ToTable("Users", null, t =>
{
t.HasComment("用户表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", null, t =>
{
t.HasComment("用户角色关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
.WithMany("RolePermissions")
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");
});
#pragma warning restore 612, 618
}
}
}

30
src/CellularManagement.Infrastructure/Migrations/20250520034505_AddLastLoginTime.cs

@ -0,0 +1,30 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddLastLoginTime : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastLoginTime",
table: "Users",
type: "timestamp with time zone",
nullable: true,
comment: "最后登录时间");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastLoginTime",
table: "Users");
}
}
}

396
src/CellularManagement.Infrastructure/Migrations/20250520044439_AddLoginLog.Designer.cs

@ -0,0 +1,396 @@
// <auto-generated />
using System;
using CellularManagement.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250520044439_AddLoginLog")]
partial class AddLoginLog
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("CellularManagement.Domain.Entities.AppRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("角色ID,主键");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("角色描述");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("角色名称");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化角色名称(大写)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("IX_Roles_Name");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", null, t =>
{
t.HasComment("角色表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("用户ID,主键");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer")
.HasComment("登录失败次数");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("电子邮箱");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasComment("邮箱是否已验证");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("用户状态(true: 启用, false: 禁用)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasComment("是否已删除");
b.Property<DateTime?>("LastLoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("最后登录时间");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone")
.HasComment("账户锁定结束时间");
b.Property<DateTime?>("ModifiedTime")
.HasColumnType("timestamp with time zone")
.HasComment("修改时间");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化电子邮箱(大写)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化用户名(大写)");
b.Property<string>("PasswordHash")
.HasColumnType("text")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text")
.HasComment("电话号码");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean")
.HasComment("电话号码是否已验证");
b.Property<string>("SecurityStamp")
.HasColumnType("text")
.HasComment("安全戳,用于并发控制");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean")
.HasComment("是否启用双因素认证");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("用户名");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("IX_Users_Email");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PhoneNumber")
.IsUnique()
.HasDatabaseName("IX_Users_PhoneNumber");
b.HasIndex("UserName")
.IsUnique()
.HasDatabaseName("IX_Users_UserName");
b.ToTable("Users", null, t =>
{
t.HasComment("用户表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("日志ID");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("浏览器信息");
b.Property<string>("FailureReason")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("失败原因");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("登录IP");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean")
.HasComment("登录状态(成功/失败)");
b.Property<string>("Location")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("登录位置");
b.Property<DateTime>("LoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("登录时间");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("操作系统信息");
b.Property<string>("UserAgent")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备信息");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("character varying(450)")
.HasComment("用户ID");
b.HasKey("Id");
b.HasIndex("IpAddress")
.HasDatabaseName("IX_LoginLogs_IpAddress");
b.HasIndex("LoginTime")
.HasDatabaseName("IX_LoginLogs_LoginTime");
b.HasIndex("UserId")
.HasDatabaseName("IX_LoginLogs_UserId");
b.HasIndex("UserId", "LoginTime")
.HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
b.ToTable("LoginLogs", null, t =>
{
t.HasComment("用户登录日志表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", null, t =>
{
t.HasComment("用户角色关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
.WithMany("RolePermissions")
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");
});
#pragma warning restore 612, 618
}
}
}

69
src/CellularManagement.Infrastructure/Migrations/20250520044439_AddLoginLog.cs

@ -0,0 +1,69 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddLoginLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "LoginLogs",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false, comment: "日志ID"),
UserId = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: false, comment: "用户ID"),
LoginTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "登录时间"),
IpAddress = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, comment: "登录IP"),
UserAgent = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false, comment: "设备信息"),
IsSuccess = table.Column<bool>(type: "boolean", nullable: false, comment: "登录状态(成功/失败)"),
FailureReason = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true, comment: "失败原因"),
Location = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true, comment: "登录位置"),
Browser = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true, comment: "浏览器信息"),
OperatingSystem = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true, comment: "操作系统信息")
},
constraints: table =>
{
table.PrimaryKey("PK_LoginLogs", x => x.Id);
table.ForeignKey(
name: "FK_LoginLogs_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
},
comment: "用户登录日志表");
migrationBuilder.CreateIndex(
name: "IX_LoginLogs_IpAddress",
table: "LoginLogs",
column: "IpAddress");
migrationBuilder.CreateIndex(
name: "IX_LoginLogs_LoginTime",
table: "LoginLogs",
column: "LoginTime");
migrationBuilder.CreateIndex(
name: "IX_LoginLogs_UserId",
table: "LoginLogs",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_LoginLogs_UserId_LoginTime",
table: "LoginLogs",
columns: new[] { "UserId", "LoginTime" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LoginLogs");
}
}
}

401
src/CellularManagement.Infrastructure/Migrations/20250520060054_AddRealNameToAppUser.Designer.cs

@ -0,0 +1,401 @@
// <auto-generated />
using System;
using CellularManagement.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250520060054_AddRealNameToAppUser")]
partial class AddRealNameToAppUser
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("CellularManagement.Domain.Entities.AppRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("角色ID,主键");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("角色描述");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("角色名称");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化角色名称(大写)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("IX_Roles_Name");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", null, t =>
{
t.HasComment("角色表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("用户ID,主键");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer")
.HasComment("登录失败次数");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("电子邮箱");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasComment("邮箱是否已验证");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("用户状态(true: 启用, false: 禁用)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasComment("是否已删除");
b.Property<DateTime?>("LastLoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("最后登录时间");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone")
.HasComment("账户锁定结束时间");
b.Property<DateTime?>("ModifiedTime")
.HasColumnType("timestamp with time zone")
.HasComment("修改时间");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化电子邮箱(大写)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化用户名(大写)");
b.Property<string>("PasswordHash")
.HasColumnType("text")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text")
.HasComment("电话号码");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean")
.HasComment("电话号码是否已验证");
b.Property<string>("RealName")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("真实姓名");
b.Property<string>("SecurityStamp")
.HasColumnType("text")
.HasComment("安全戳,用于并发控制");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean")
.HasComment("是否启用双因素认证");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("用户名");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("IX_Users_Email");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PhoneNumber")
.IsUnique()
.HasDatabaseName("IX_Users_PhoneNumber");
b.HasIndex("UserName")
.IsUnique()
.HasDatabaseName("IX_Users_UserName");
b.ToTable("Users", null, t =>
{
t.HasComment("用户表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("日志ID");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("浏览器信息");
b.Property<string>("FailureReason")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("失败原因");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("登录IP");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean")
.HasComment("登录状态(成功/失败)");
b.Property<string>("Location")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("登录位置");
b.Property<DateTime>("LoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("登录时间");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("操作系统信息");
b.Property<string>("UserAgent")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备信息");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("character varying(450)")
.HasComment("用户ID");
b.HasKey("Id");
b.HasIndex("IpAddress")
.HasDatabaseName("IX_LoginLogs_IpAddress");
b.HasIndex("LoginTime")
.HasDatabaseName("IX_LoginLogs_LoginTime");
b.HasIndex("UserId")
.HasDatabaseName("IX_LoginLogs_UserId");
b.HasIndex("UserId", "LoginTime")
.HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
b.ToTable("LoginLogs", null, t =>
{
t.HasComment("用户登录日志表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", null, t =>
{
t.HasComment("用户角色关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
.WithMany("RolePermissions")
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");
});
#pragma warning restore 612, 618
}
}
}

30
src/CellularManagement.Infrastructure/Migrations/20250520060054_AddRealNameToAppUser.cs

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddRealNameToAppUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "RealName",
table: "Users",
type: "character varying(50)",
maxLength: 50,
nullable: true,
comment: "真实姓名");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RealName",
table: "Users");
}
}
}

401
src/CellularManagement.Infrastructure/Migrations/20250520060351_UpdateUserComments.Designer.cs

@ -0,0 +1,401 @@
// <auto-generated />
using System;
using CellularManagement.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250520060351_UpdateUserComments")]
partial class UpdateUserComments
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("CellularManagement.Domain.Entities.AppRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("角色ID,主键");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("角色描述");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("角色名称");
b.Property<string>("NormalizedName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化角色名称(大写)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasDatabaseName("IX_Roles_Name");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", null, t =>
{
t.HasComment("角色表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.AppUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text")
.HasComment("用户ID,主键");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer")
.HasComment("登录失败次数");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("电子邮箱");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean")
.HasComment("邮箱是否已验证");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("用户状态(true: 启用, false: 禁用)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasComment("是否已删除");
b.Property<DateTime?>("LastLoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("最后登录时间");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone")
.HasComment("账户锁定结束时间");
b.Property<DateTime?>("ModifiedTime")
.HasColumnType("timestamp with time zone")
.HasComment("修改时间");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化电子邮箱(大写)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化账号(大写)");
b.Property<string>("PasswordHash")
.HasColumnType("text")
.HasComment("密码哈希值");
b.Property<string>("PhoneNumber")
.IsRequired()
.HasColumnType("text")
.HasComment("电话号码");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean")
.HasComment("电话号码是否已验证");
b.Property<string>("RealName")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("用户名");
b.Property<string>("SecurityStamp")
.HasColumnType("text")
.HasComment("安全戳,用于并发控制");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean")
.HasComment("是否启用双因素认证");
b.Property<string>("UserName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("账号");
b.HasKey("Id");
b.HasIndex("Email")
.IsUnique()
.HasDatabaseName("IX_Users_Email");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.HasIndex("PhoneNumber")
.IsUnique()
.HasDatabaseName("IX_Users_PhoneNumber");
b.HasIndex("UserName")
.IsUnique()
.HasDatabaseName("IX_Users_UserName");
b.ToTable("Users", null, t =>
{
t.HasComment("用户表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("日志ID");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("浏览器信息");
b.Property<string>("FailureReason")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("失败原因");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("登录IP");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean")
.HasComment("登录状态(成功/失败)");
b.Property<string>("Location")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("登录位置");
b.Property<DateTime>("LoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("登录时间");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("操作系统信息");
b.Property<string>("UserAgent")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备信息");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("character varying(450)")
.HasComment("用户ID");
b.HasKey("Id");
b.HasIndex("IpAddress")
.HasDatabaseName("IX_LoginLogs_IpAddress");
b.HasIndex("LoginTime")
.HasDatabaseName("IX_LoginLogs_LoginTime");
b.HasIndex("UserId")
.HasDatabaseName("IX_LoginLogs_UserId");
b.HasIndex("UserId", "LoginTime")
.HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
b.ToTable("LoginLogs", null, t =>
{
t.HasComment("用户登录日志表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.ToTable("Permissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", (string)null);
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", null, t =>
{
t.HasComment("用户角色关系表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")
.WithMany("RolePermissions")
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Permission");
b.Navigation("Role");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.UserRole", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppRole", "Role")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CellularManagement.Domain.Entities.AppUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Navigation("RolePermissions");
});
#pragma warning restore 612, 618
}
}
}

94
src/CellularManagement.Infrastructure/Migrations/20250520060351_UpdateUserComments.cs

@ -0,0 +1,94 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class UpdateUserComments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "UserName",
table: "Users",
type: "character varying(256)",
maxLength: 256,
nullable: false,
comment: "账号",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldComment: "用户名");
migrationBuilder.AlterColumn<string>(
name: "RealName",
table: "Users",
type: "character varying(50)",
maxLength: 50,
nullable: true,
comment: "用户名",
oldClrType: typeof(string),
oldType: "character varying(50)",
oldMaxLength: 50,
oldNullable: true,
oldComment: "真实姓名");
migrationBuilder.AlterColumn<string>(
name: "NormalizedUserName",
table: "Users",
type: "character varying(256)",
maxLength: 256,
nullable: true,
comment: "标准化账号(大写)",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true,
oldComment: "标准化用户名(大写)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "UserName",
table: "Users",
type: "character varying(256)",
maxLength: 256,
nullable: false,
comment: "用户名",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldComment: "账号");
migrationBuilder.AlterColumn<string>(
name: "RealName",
table: "Users",
type: "character varying(50)",
maxLength: 50,
nullable: true,
comment: "真实姓名",
oldClrType: typeof(string),
oldType: "character varying(50)",
oldMaxLength: 50,
oldNullable: true,
oldComment: "用户名");
migrationBuilder.AlterColumn<string>(
name: "NormalizedUserName",
table: "Users",
type: "character varying(256)",
maxLength: 256,
nullable: true,
comment: "标准化用户名(大写)",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true,
oldComment: "标准化账号(大写)");
}
}
}

95
src/CellularManagement.Infrastructure/Migrations/AppDbContextModelSnapshot.cs

@ -115,6 +115,10 @@ namespace CellularManagement.Infrastructure.Migrations
.HasDefaultValue(false)
.HasComment("是否已删除");
b.Property<DateTime?>("LastLoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("最后登录时间");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
@ -135,7 +139,7 @@ namespace CellularManagement.Infrastructure.Migrations
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("标准化用户名(大写)");
.HasComment("标准化账号(大写)");
b.Property<string>("PasswordHash")
.HasColumnType("text")
@ -150,6 +154,11 @@ namespace CellularManagement.Infrastructure.Migrations
.HasColumnType("boolean")
.HasComment("电话号码是否已验证");
b.Property<string>("RealName")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("用户名");
b.Property<string>("SecurityStamp")
.HasColumnType("text")
.HasComment("安全戳,用于并发控制");
@ -162,7 +171,7 @@ namespace CellularManagement.Infrastructure.Migrations
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("用户名");
.HasComment("账号");
b.HasKey("Id");
@ -191,6 +200,79 @@ namespace CellularManagement.Infrastructure.Migrations
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("日志ID");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("浏览器信息");
b.Property<string>("FailureReason")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("失败原因");
b.Property<string>("IpAddress")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("登录IP");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean")
.HasComment("登录状态(成功/失败)");
b.Property<string>("Location")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("登录位置");
b.Property<DateTime>("LoginTime")
.HasColumnType("timestamp with time zone")
.HasComment("登录时间");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasComment("操作系统信息");
b.Property<string>("UserAgent")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasComment("设备信息");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(450)
.HasColumnType("character varying(450)")
.HasComment("用户ID");
b.HasKey("Id");
b.HasIndex("IpAddress")
.HasDatabaseName("IX_LoginLogs_IpAddress");
b.HasIndex("LoginTime")
.HasDatabaseName("IX_LoginLogs_LoginTime");
b.HasIndex("UserId")
.HasDatabaseName("IX_LoginLogs_UserId");
b.HasIndex("UserId", "LoginTime")
.HasDatabaseName("IX_LoginLogs_UserId_LoginTime");
b.ToTable("LoginLogs", null, t =>
{
t.HasComment("用户登录日志表");
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.Permission", b =>
{
b.Property<string>("Id")
@ -259,6 +341,15 @@ namespace CellularManagement.Infrastructure.Migrations
});
});
modelBuilder.Entity("CellularManagement.Domain.Entities.LoginLog", b =>
{
b.HasOne("CellularManagement.Domain.Entities.AppUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("CellularManagement.Domain.Entities.RolePermission", b =>
{
b.HasOne("CellularManagement.Domain.Entities.Permission", "Permission")

84
src/CellularManagement.Infrastructure/Repositories/LoginLogRepository.cs

@ -0,0 +1,84 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CellularManagement.Domain.Entities;
using CellularManagement.Domain.Repositories;
using CellularManagement.Infrastructure.Context;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CellularManagement.Infrastructure.Repositories;
/// <summary>
/// 登录日志仓储实现
/// </summary>
public class LoginLogRepository : CommandRepository<LoginLog>, ILoginLogRepository
{
private readonly AppDbContext _context;
private readonly ILogger<LoginLogRepository> _logger;
/// <summary>
/// 初始化仓储
/// </summary>
/// <param name="context">数据库上下文</param>
/// <param name="unitOfWork">工作单元</param>
/// <param name="logger">日志记录器</param>
public LoginLogRepository(
AppDbContext context,
IUnitOfWork unitOfWork,
ILogger<LoginLogRepository> logger)
: base(context, unitOfWork, logger)
{
_context = context;
_logger = logger;
}
/// <summary>
/// 添加登录日志
/// </summary>
public async Task AddAsync(LoginLog log, CancellationToken cancellationToken = default)
{
await _context.LoginLogs.AddAsync(log, cancellationToken);
// 不在这里 SaveChanges,由工作单元统一提交
}
/// <summary>
/// 获取用户最近的登录日志
/// </summary>
public async Task<LoginLog[]> GetRecentLogsAsync(string userId, int count, CancellationToken cancellationToken = default)
{
return await _context.LoginLogs
.Where(l => l.UserId == userId)
.OrderByDescending(l => l.LoginTime)
.Take(count)
.ToArrayAsync(cancellationToken);
}
/// <summary>
/// 检查IP是否被限制
/// </summary>
public async Task<bool> IsIpRestrictedAsync(string ipAddress, CancellationToken cancellationToken = default)
{
var recentFailures = await _context.LoginLogs
.Where(l => l.IpAddress == ipAddress && !l.IsSuccess)
.OrderByDescending(l => l.LoginTime)
.Take(5)
.ToArrayAsync(cancellationToken);
return recentFailures.Length >= 5 &&
(DateTime.UtcNow - recentFailures[0].LoginTime).TotalMinutes < 30;
}
/// <summary>
/// 获取IP的失败登录次数
/// </summary>
public async Task<int> GetIpFailureCountAsync(string ipAddress, CancellationToken cancellationToken = default)
{
return await _context.LoginLogs
.CountAsync(l => l.IpAddress == ipAddress &&
!l.IsSuccess &&
l.LoginTime > DateTime.UtcNow.AddMinutes(-30),
cancellationToken);
}
}

16
src/CellularManagement.Infrastructure/Services/UserRegistrationService.cs

@ -39,13 +39,24 @@ public class UserRegistrationService : IUserRegistrationService
public async Task<(bool success, string? errorMessage)> RegisterUserAsync(AppUser user, string password)
{
// 验证用户名
// 验证账号
var userNameResult = UserName.Create(user.UserName);
if (userNameResult.IsFailure)
{
throw new UserRegistrationException(userNameResult.Error!);
}
// 验证用户名
if (string.IsNullOrWhiteSpace(user.RealName))
{
throw new UserRegistrationException("用户名不能为空");
}
if (user.RealName.Length > 50)
{
throw new UserRegistrationException("用户名长度不能超过50个字符");
}
// 验证邮箱
var emailResult = Email.Create(user.Email);
if (emailResult.IsFailure)
@ -60,7 +71,7 @@ public class UserRegistrationService : IUserRegistrationService
throw new UserRegistrationException("系统繁忙,请稍后重试");
}
// 检查用户名是否存在
// 检查账号是否存在
var existingUser = await _userManager.FindByNameAsync(user.UserName);
if (existingUser != null)
{
@ -110,6 +121,7 @@ public class UserRegistrationService : IUserRegistrationService
throw new RoleAssignmentException(string.Join(", ", errors));
}
}
// 创建用户角色关系
var userRole = new UserRole
{

81
update-database.ps1

@ -1,34 +1,57 @@
# 数据库迁移脚本
Write-Host "开始执行数据库迁移..." -ForegroundColor Green
# Database Migration Script
$ErrorActionPreference = "Stop"
function Write-Status {
param(
[string]$Message,
[string]$Status = "Info"
)
$color = switch ($Status) {
"Success" { "Green" }
"Error" { "Red" }
"Warning" { "Yellow" }
default { "White" }
}
Write-Host "[$Status] $Message" -ForegroundColor $color
}
try {
# 添加迁移
Write-Host "正在添加迁移..." -ForegroundColor Yellow
dotnet ef migrations add UpdateAppUserAuditFields --project src/CellularManagement.Infrastructure --startup-project src/CellularManagement.WebAPI
# Check if projects exist
if (-not (Test-Path "src/CellularManagement.Infrastructure")) {
throw "Infrastructure project not found"
}
if (-not (Test-Path "src/CellularManagement.WebAPI")) {
throw "WebAPI project not found"
}
if ($LASTEXITCODE -eq 0) {
Write-Host "迁移添加成功!" -ForegroundColor Green
# 更新数据库
Write-Host "正在更新数据库..." -ForegroundColor Yellow
dotnet ef database update --project src/CellularManagement.Infrastructure --startup-project src/CellularManagement.WebAPI
if ($LASTEXITCODE -eq 0) {
Write-Host "数据库更新成功!" -ForegroundColor Green
} else {
Write-Host "数据库更新失败!错误代码: $LASTEXITCODE" -ForegroundColor Red
exit 1
}
} else {
Write-Host "迁移添加失败!错误代码: $LASTEXITCODE" -ForegroundColor Red
exit 1
# Build project
Write-Status "Building project..." "Info"
dotnet build "src/CellularManagement.Infrastructure" -c Release
if ($LASTEXITCODE -ne 0) {
throw "Project build failed"
}
}
catch {
Write-Host "执行过程中发生错误:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
exit 1
}
Write-Status "Project built successfully" "Success"
# Add migration
Write-Status "Adding database migration..." "Info"
$migrationName = "AddRealNameToAppUser"
dotnet ef migrations add $migrationName --project src/CellularManagement.Infrastructure --startup-project src/CellularManagement.WebAPI
if ($LASTEXITCODE -ne 0) {
throw "Failed to add migration"
}
Write-Status "Migration added successfully" "Success"
Write-Host "按任意键退出..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
# Update database
Write-Status "Updating database..." "Info"
dotnet ef database update --project src/CellularManagement.Infrastructure --startup-project src/CellularManagement.WebAPI
if ($LASTEXITCODE -ne 0) {
throw "Database update failed"
}
Write-Status "Database updated successfully" "Success"
} catch {
Write-Status "Error occurred: $_" "Error"
exit 1
}
Loading…
Cancel
Save