Browse Source

refactor: 规范化代码,将 CreatedTime 改为 createdAt

refactor/repository-structure
hyh 7 months ago
parent
commit
128f3be343
  1. 4
      src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequestExample.cs
  2. 12
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommand.cs
  3. 116
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandHandler.cs
  4. 12
      src/CellularManagement.Application/Features/Auth/Commands/RegisterUser/RegisterUserCommandValidator.cs
  5. 2
      src/CellularManagement.Application/Features/Users/Queries/Dtos/UserDto.cs
  6. 2
      src/CellularManagement.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs
  7. 2
      src/CellularManagement.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs
  8. 50
      src/CellularManagement.Domain/Common/BaseAuditableEntity.cs
  9. 37
      src/CellularManagement.Domain/Common/BaseIdentityUser.cs
  10. 18
      src/CellularManagement.Domain/Common/IAuditableEntity.cs
  11. 41
      src/CellularManagement.Domain/Common/Result.cs
  12. 44
      src/CellularManagement.Domain/Common/ValueObject.cs
  13. 24
      src/CellularManagement.Domain/Entities/AppUser.cs
  14. 18
      src/CellularManagement.Domain/Exceptions/DomainException.cs
  15. 50
      src/CellularManagement.Domain/Exceptions/UserRegistrationException.cs
  16. 10
      src/CellularManagement.Domain/Options/CorsOptions.cs
  17. 18
      src/CellularManagement.Domain/Services/IDistributedLockService.cs
  18. 14
      src/CellularManagement.Domain/Services/ILockHandle.cs
  19. 27
      src/CellularManagement.Domain/Services/IUserRegistrationService.cs
  20. 7
      src/CellularManagement.Domain/Specifications/AuthRequestSamples.json
  21. 12
      src/CellularManagement.Domain/Specifications/DefaultData.sql
  22. 44
      src/CellularManagement.Domain/ValueObjects/Email.cs
  23. 43
      src/CellularManagement.Domain/ValueObjects/UserName.cs
  24. 21
      src/CellularManagement.Infrastructure/Configurations/AppUserConfiguration.cs
  25. 7
      src/CellularManagement.Infrastructure/DependencyInjection.cs
  26. 310
      src/CellularManagement.Infrastructure/Migrations/20250519092205_AddAuditFieldsToAppUser.Designer.cs
  27. 66
      src/CellularManagement.Infrastructure/Migrations/20250519092205_AddAuditFieldsToAppUser.cs
  28. 310
      src/CellularManagement.Infrastructure/Migrations/20250519093405_RemoveDuplicateAuditFields.Designer.cs
  29. 22
      src/CellularManagement.Infrastructure/Migrations/20250519093405_RemoveDuplicateAuditFields.cs
  30. 310
      src/CellularManagement.Infrastructure/Migrations/20250519093919_UpdateAppUserAuditFields.Designer.cs
  31. 22
      src/CellularManagement.Infrastructure/Migrations/20250519093919_UpdateAppUserAuditFields.cs
  32. 20
      src/CellularManagement.Infrastructure/Migrations/AppDbContextModelSnapshot.cs
  33. 3
      src/CellularManagement.Infrastructure/Services/CaptchaService.cs
  34. 128
      src/CellularManagement.Infrastructure/Services/DistributedLockService.cs
  35. 130
      src/CellularManagement.Infrastructure/Services/UserRegistrationService.cs
  36. 17
      src/CellularManagement.Presentation/appsettings.json
  37. 41
      src/CellularManagement.WebAPI/Program.cs
  38. 2
      src/CellularManagement.WebAPI/Properties/launchSettings.json
  39. 30
      src/CellularManagement.WebAPI/appsettings.json
  40. 69
      src/CellularManagement.WebUI/src/components/auth/RegisterForm.tsx
  41. 28
      src/CellularManagement.WebUI/src/components/ui/checkbox.tsx
  42. 8
      src/CellularManagement.WebUI/src/components/ui/table.tsx
  43. 5
      src/CellularManagement.WebUI/src/config/core/env.config.ts
  44. 2
      src/CellularManagement.WebUI/src/constants/auth.ts
  45. 2
      src/CellularManagement.WebUI/src/constants/menuConfig.ts
  46. 5
      src/CellularManagement.WebUI/src/pages/auth/RegisterPage.tsx
  47. 70
      src/CellularManagement.WebUI/src/pages/users/UserForm.tsx
  48. 110
      src/CellularManagement.WebUI/src/pages/users/UserRolesForm.tsx
  49. 215
      src/CellularManagement.WebUI/src/pages/users/UserTable.tsx
  50. 208
      src/CellularManagement.WebUI/src/pages/users/UsersView.tsx
  51. 17
      src/CellularManagement.WebUI/src/routes/AppRouter.tsx
  52. 6
      src/CellularManagement.WebUI/src/services/axiosConfig.ts
  53. 157
      src/CellularManagement.WebUI/src/services/userService.ts
  54. 6
      src/CellularManagement.WebUI/src/types/auth.ts
  55. 5
      src/CellularManagement.WebUI/vite.config.ts
  56. 34
      update-database.ps1

4
src/CellularManagement.Application/Features/Auth/Commands/Login/LoginRequestExample.cs

@ -16,7 +16,7 @@ public class LoginRequestExample : IExamplesProvider<LoginRequest>
return new LoginRequest
{
UserName = "zhangsan",
Password = "P@ssw0rd!"
Password = "Zhangsan0001@qq.com"
};
}
}
@ -38,7 +38,7 @@ public class LoginRequestExamples : IMultipleExamplesProvider<LoginRequest>
new LoginRequest
{
UserName = "zhangsan",
Password = "P@ssw0rd!"
Password = "Zhangsan0001@qq.com"
}
);
}

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

@ -30,4 +30,14 @@ public sealed record RegisterUserCommand(
/// <summary>
/// 电话号码
/// </summary>
string? PhoneNumber) : IRequest<OperationResult<RegisterUserResponse>>;
string? PhoneNumber,
/// <summary>
/// 验证码ID
/// </summary>
string CaptchaId,
/// <summary>
/// 用户输入的验证码
/// </summary>
string CaptchaCode) : IRequest<OperationResult<RegisterUserResponse>>;

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

@ -3,12 +3,12 @@ using Microsoft.Extensions.Logging;
using MediatR;
using CellularManagement.Domain.Entities;
using CellularManagement.Domain.Repositories;
using Microsoft.Extensions.Options;
using CellularManagement.Domain.Services;
using CellularManagement.Domain.Common;
using System.Linq;
using System;
using CellularManagement.Domain.Exceptions;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace CellularManagement.Application.Features.Auth.Commands.RegisterUser;
@ -17,49 +17,61 @@ namespace CellularManagement.Application.Features.Auth.Commands.RegisterUser;
/// </summary>
public sealed class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, OperationResult<RegisterUserResponse>>
{
private readonly UserManager<AppUser> _userManager;
private readonly RoleManager<AppRole> _roleManager;
private readonly IUserRegistrationService _userRegistrationService;
private readonly ILogger<RegisterUserCommandHandler> _logger;
private readonly IUserRoleRepository _userRoleRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly ICacheService _cacheService;
private readonly IDistributedLockService _lockService;
public RegisterUserCommandHandler(
UserManager<AppUser> userManager,
RoleManager<AppRole> roleManager,
IUserRegistrationService userRegistrationService,
ILogger<RegisterUserCommandHandler> logger,
IUserRoleRepository userRoleRepository,
IUnitOfWork unitOfWork)
IUnitOfWork unitOfWork,
ICacheService cacheService,
IDistributedLockService lockService)
{
_userManager = userManager;
_roleManager = roleManager;
_userRegistrationService = userRegistrationService;
_logger = logger;
_userRoleRepository = userRoleRepository;
_unitOfWork = unitOfWork;
_cacheService = cacheService;
_lockService = lockService;
}
public async Task<OperationResult<RegisterUserResponse>> Handle(
RegisterUserCommand request,
CancellationToken cancellationToken)
{
// 使用分布式锁保护验证码验证过程
using var captchaLock = await _lockService.AcquireLockAsync(
$"captcha_verification_{request.CaptchaId}",
TimeSpan.FromSeconds(5));
if (!captchaLock.IsAcquired)
{
_logger.LogWarning("验证码验证过程被锁定,请稍后重试");
return OperationResult<RegisterUserResponse>.CreateFailure("系统繁忙,请稍后重试");
}
try
{
// 检查用户名是否已存在
var existingUser = await _userManager.FindByNameAsync(request.UserName);
if (existingUser != null)
// 验证验证码
var cachedCaptcha = _cacheService.Get<string>($"captcha:{request.CaptchaId}");
if (string.IsNullOrEmpty(cachedCaptcha))
{
_logger.LogWarning("用户名 {UserName} 已存在", request.UserName);
return OperationResult<RegisterUserResponse>.CreateFailure("用户名已被使用");
_logger.LogWarning("验证码已过期或不存在");
return OperationResult<RegisterUserResponse>.CreateFailure("验证码已过期或不存在");
}
// 检查邮箱是否已存在
existingUser = await _userManager.FindByEmailAsync(request.Email);
if (existingUser != null)
if (!string.Equals(cachedCaptcha, request.CaptchaCode, StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("邮箱 {Email} 已存在", request.Email);
return OperationResult<RegisterUserResponse>.CreateFailure("邮箱已被使用");
_logger.LogWarning("验证码错误");
return OperationResult<RegisterUserResponse>.CreateFailure("验证码错误");
}
// 创建用户
// 验证通过后原子性地删除验证码
_cacheService.Remove($"captcha:{request.CaptchaId}");
// 创建用户实体
var user = new AppUser
{
UserName = request.UserName,
@ -67,46 +79,50 @@ public sealed class RegisterUserCommandHandler : IRequestHandler<RegisterUserCom
PhoneNumber = request.PhoneNumber
};
// 在事务中执行用户创建和角色分配
// 在事务中执行用户注册和角色分配
await _unitOfWork.ExecuteTransactionAsync(async () =>
{
// 创建用户
var result = await _userManager.CreateAsync(user, request.Password);
if (!result.Succeeded)
// 注册用户
var (success, errorMessage) = await _userRegistrationService.RegisterUserAsync(user, request.Password);
if (!success)
{
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("创建用户失败: {Errors}", string.Join(", ", errors));
throw new InvalidOperationException(string.Join(", ", errors));
_logger.LogWarning("注册用户失败: {Error}", errorMessage);
throw new UserRegistrationException(errorMessage!);
}
// 获取默认角色
var defaultRole = await _roleManager.FindByNameAsync("User");
if (defaultRole == null)
// 分配角色
(success, errorMessage) = await _userRegistrationService.AssignUserRoleAsync(user);
if (!success)
{
throw new InvalidOperationException("默认用户角色不存在");
_logger.LogWarning("分配角色失败: {Error}", errorMessage);
throw new RoleAssignmentException(errorMessage!);
}
// 创建用户角色关系
var userRole = new UserRole
{
UserId = user.Id,
RoleId = defaultRole.Id,
User = user
};
// 添加用户角色关系
await _userRoleRepository.AddAsync(userRole, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken); // 必须有
}, cancellationToken: cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
});
_logger.LogInformation("用户 {UserName} 注册成功", request.UserName);
return OperationResult<RegisterUserResponse>.CreateSuccess(
new RegisterUserResponse(user.Id));
}
catch (InvalidOperationException ex)
catch (UserNameAlreadyExistsException ex)
{
_logger.LogWarning(ex, "用户名已存在");
return OperationResult<RegisterUserResponse>.CreateFailure(ex.Message);
}
catch (EmailAlreadyExistsException ex)
{
_logger.LogWarning(ex, "邮箱已存在");
return OperationResult<RegisterUserResponse>.CreateFailure(ex.Message);
}
catch (RoleAssignmentException ex)
{
_logger.LogError(ex, "角色分配失败");
return OperationResult<RegisterUserResponse>.CreateFailure(ex.Message);
}
catch (UserRegistrationException ex)
{
_logger.LogWarning(ex, "用户 {UserName} 注册失败: {Message}", request.UserName, ex.Message);
_logger.LogError(ex, "用户注册失败");
return OperationResult<RegisterUserResponse>.CreateFailure(ex.Message);
}
catch (Exception ex)

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

@ -9,7 +9,7 @@ namespace CellularManagement.Application.Features.Auth.Commands.RegisterUser;
public sealed class RegisterUserCommandValidator : AbstractValidator<RegisterUserCommand>
{
/// <summary>
/// 初始化验证器 rugqfbgcnpkdbhee
/// 初始化验证器
/// </summary>
public RegisterUserCommandValidator()
{
@ -41,6 +41,16 @@ public sealed class RegisterUserCommandValidator : AbstractValidator<RegisterUse
.NotEmpty().WithMessage("确认密码不能为空")
.Equal(x => x.Password).WithMessage("两次输入的密码不一致");
// 验证验证码ID
RuleFor(x => x.CaptchaId)
.NotEmpty().WithMessage("验证码ID不能为空");
// 验证验证码
RuleFor(x => x.CaptchaCode)
.NotEmpty().WithMessage("验证码不能为空")
.Length(4, 6).WithMessage("验证码长度必须在4-6个字符之间")
.Matches("^[A-Z0-9]+$").WithMessage("验证码只能包含大写字母和数字");
// 验证电话号码
RuleFor(x => x.PhoneNumber)
.Matches(@"^1[3-9]\d{9}$").When(x => !string.IsNullOrEmpty(x.PhoneNumber))

2
src/CellularManagement.Application/Features/Users/Queries/Dtos/UserDto.cs

@ -12,4 +12,4 @@ public sealed record UserDto(
string UserId,
string UserName,
string Email,
string PhoneNumber);
string PhoneNumber, DateTime CreatedAt, bool IsActive);

2
src/CellularManagement.Application/Features/Users/Queries/GetAllUsers/GetAllUsersQueryHandler.cs

@ -79,7 +79,7 @@ public sealed class GetAllUsersQueryHandler : IRequestHandler<GetAllUsersQuery,
user.Id,
user.UserName,
user.Email,
user.PhoneNumber)).ToList();
user.PhoneNumber,user.CreatedTime,user.IsActive)).ToList();
// 构建响应对象
var response = new GetAllUsersResponse(

2
src/CellularManagement.Application/Features/Users/Queries/GetUserById/GetUserByIdQueryHandler.cs

@ -54,7 +54,7 @@ public sealed class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery,
var dto = new UserDto(user.Id,
user.UserName,
user.Email,
user.PhoneNumber);
user.PhoneNumber,user.CreatedTime, user.IsActive);
// 构建响应对象
var response = new GetUserByIdResponse(dto);

50
src/CellularManagement.Domain/Common/BaseAuditableEntity.cs

@ -0,0 +1,50 @@
namespace CellularManagement.Domain.Common;
/// <summary>
/// 基础审计实体类
/// 包含创建时间和修改时间字段,以及相关的操作方法
/// </summary>
public abstract class BaseAuditableEntity : IAuditableEntity
{
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
/// <summary>
/// 修改时间
/// </summary>
public DateTime? ModifiedTime { get; set; }
/// <summary>
/// 设置创建时间
/// </summary>
protected void SetCreatedTime()
{
CreatedTime = DateTime.UtcNow;
}
/// <summary>
/// 设置修改时间
/// </summary>
protected void SetModifiedTime()
{
ModifiedTime = DateTime.UtcNow;
}
/// <summary>
/// 初始化审计字段
/// </summary>
protected void InitializeAuditFields()
{
SetCreatedTime();
}
/// <summary>
/// 更新审计字段
/// </summary>
protected void UpdateAuditFields()
{
SetModifiedTime();
}
}

37
src/CellularManagement.Domain/Common/BaseIdentityUser.cs

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Identity;
using CellularManagement.Domain.Common;
namespace CellularManagement.Domain.Common;
/// <summary>
/// 基础身份用户类
/// 同时继承 IdentityUser 和 BaseAuditableEntity
/// </summary>
public abstract class BaseIdentityUser : IdentityUser, IAuditableEntity
{
/// <summary>
/// 初始化基础身份用户
/// </summary>
protected BaseIdentityUser()
{
CreatedTime = DateTime.UtcNow;
}
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedTime { get; set; }
/// <summary>
/// 修改时间
/// </summary>
public DateTime? ModifiedTime { get; set; }
/// <summary>
/// 更新审计字段
/// </summary>
protected void UpdateAuditFields()
{
ModifiedTime = DateTime.UtcNow;
}
}

18
src/CellularManagement.Domain/Common/IAuditableEntity.cs

@ -0,0 +1,18 @@
namespace CellularManagement.Domain.Common;
/// <summary>
/// 审计实体接口
/// 定义审计实体必须实现的属性
/// </summary>
public interface IAuditableEntity
{
/// <summary>
/// 创建时间
/// </summary>
DateTime CreatedTime { get; set; }
/// <summary>
/// 修改时间
/// </summary>
DateTime? ModifiedTime { get; set; }
}

41
src/CellularManagement.Domain/Common/Result.cs

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
namespace CellularManagement.Domain.Common;
/// <summary>
/// 结果类
/// </summary>
public class Result
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public string? Error { get; }
protected Result(bool isSuccess, string? error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new(true, null);
public static Result Failure(string error) => new(false, error);
}
/// <summary>
/// 泛型结果类
/// </summary>
public class Result<T> : Result
{
private readonly T? _value;
public T Value => IsSuccess ? _value! : throw new InvalidOperationException("Cannot access Value when Result is a failure.");
protected internal Result(T value, bool isSuccess, string? error)
: base(isSuccess, error)
{
_value = value;
}
public static Result<T> Success(T value) => new(value, true, null);
public static new Result<T> Failure(string error) => new(default!, false, error);
}

44
src/CellularManagement.Domain/Common/ValueObject.cs

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Linq;
namespace CellularManagement.Domain.Common;
/// <summary>
/// 值对象基类
/// </summary>
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (left is null ^ right is null)
{
return false;
}
return left?.Equals(right) != false;
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !EqualOperator(left, right);
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
}

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

@ -1,13 +1,13 @@
using Microsoft.AspNetCore.Identity;
using CellularManagement.Domain.Common;
namespace CellularManagement.Domain.Entities;
/// <summary>
/// 应用程序用户
/// 继承自 IdentityUser 以支持身份认证
/// 提供用户管理、认证和授权的基础功能
/// 继承自 BaseIdentityUser 以支持身份认证和审计功能
/// </summary>
public sealed class AppUser : IdentityUser
public sealed class AppUser : BaseIdentityUser
{
/// <summary>
/// 初始化用户
@ -17,4 +17,22 @@ public sealed class AppUser : IdentityUser
{
Id = Guid.NewGuid().ToString();
}
/// <summary>
/// 用户状态(true: 启用, false: 禁用)
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// 是否已删除
/// </summary>
public bool IsDeleted { get; set; }
/// <summary>
/// 更新用户信息
/// </summary>
public void UpdateUser()
{
UpdateAuditFields();
}
}

18
src/CellularManagement.Domain/Exceptions/DomainException.cs

@ -0,0 +1,18 @@
using System;
namespace CellularManagement.Domain.Exceptions;
/// <summary>
/// 领域异常基类
/// </summary>
public abstract class DomainException : Exception
{
protected DomainException(string message) : base(message)
{
}
protected DomainException(string message, Exception innerException)
: base(message, innerException)
{
}
}

50
src/CellularManagement.Domain/Exceptions/UserRegistrationException.cs

@ -0,0 +1,50 @@
using System;
namespace CellularManagement.Domain.Exceptions;
/// <summary>
/// 用户注册异常
/// </summary>
public class UserRegistrationException : DomainException
{
public UserRegistrationException(string message) : base(message)
{
}
public UserRegistrationException(string message, Exception innerException)
: base(message, innerException)
{
}
}
/// <summary>
/// 用户名已存在异常
/// </summary>
public class UserNameAlreadyExistsException : UserRegistrationException
{
public UserNameAlreadyExistsException(string userName)
: base($"用户名 '{userName}' 已被使用")
{
}
}
/// <summary>
/// 邮箱已存在异常
/// </summary>
public class EmailAlreadyExistsException : UserRegistrationException
{
public EmailAlreadyExistsException(string email)
: base($"邮箱 '{email}' 已被使用")
{
}
}
/// <summary>
/// 角色分配异常
/// </summary>
public class RoleAssignmentException : UserRegistrationException
{
public RoleAssignmentException(string message) : base(message)
{
}
}

10
src/CellularManagement.Domain/Options/CorsOptions.cs

@ -0,0 +1,10 @@
namespace CellularManagement.Domain.Options;
public class CorsOptions
{
public const string SectionName = "Cors";
public string[] AllowedOrigins { get; set; } = Array.Empty<string>();
public string[] AllowedMethods { get; set; } = Array.Empty<string>();
public string[] AllowedHeaders { get; set; } = Array.Empty<string>();
public bool AllowCredentials { get; set; }
}

18
src/CellularManagement.Domain/Services/IDistributedLockService.cs

@ -0,0 +1,18 @@
using System;
using System.Threading.Tasks;
namespace CellularManagement.Domain.Services;
/// <summary>
/// 分布式锁服务接口
/// </summary>
public interface IDistributedLockService
{
/// <summary>
/// 获取分布式锁
/// </summary>
/// <param name="key">锁的键</param>
/// <param name="timeout">超时时间</param>
/// <returns>锁句柄</returns>
Task<ILockHandle> AcquireLockAsync(string key, TimeSpan timeout);
}

14
src/CellularManagement.Domain/Services/ILockHandle.cs

@ -0,0 +1,14 @@
using System;
namespace CellularManagement.Domain.Services;
/// <summary>
/// 分布式锁句柄接口
/// </summary>
public interface ILockHandle : IDisposable
{
/// <summary>
/// 是否成功获取锁
/// </summary>
bool IsAcquired { get; }
}

27
src/CellularManagement.Domain/Services/IUserRegistrationService.cs

@ -0,0 +1,27 @@
using CellularManagement.Domain.Entities;
using System.Threading.Tasks;
namespace CellularManagement.Domain.Services;
/// <summary>
/// 用户注册领域服务接口
/// </summary>
public interface IUserRegistrationService
{
/// <summary>
/// 注册新用户
/// </summary>
/// <param name="user">用户信息</param>
/// <param name="password">密码</param>
/// <returns>注册结果</returns>
Task<(bool success, string? errorMessage)> RegisterUserAsync(AppUser user, string password);
/// <summary>
/// 分配用户角色
/// </summary>
/// <param name="user">用户</param>
/// <returns>分配结果</returns>
Task<(bool success, string? errorMessage)> AssignUserRoleAsync(AppUser user);
}

7
src/CellularManagement.Domain/Specifications/AuthRequestSamples.json

@ -6,8 +6,11 @@
"confirmPassword": "P@ssw0rd!",
"phoneNumber": "13800138000"
},
"AuthenticateUserCommand": {
"AuthenticateUserCommand": [{
"userNameOrEmail": "zhangsan",
"password": "P@ssw0rd!"
}
},{
"userNameOrEmail": "list",
"password": " List1201@qq.com"
}]
}

12
src/CellularManagement.Domain/Specifications/DefaultData.sql

@ -85,4 +85,14 @@ INSERT INTO "UserRoles" ("UserId", "RoleId") VALUES
('u007', 'r007'),
('u008', 'r008'),
('u009', 'r009'),
('u010', 'r010');
('u010', 'r010');
-- 菜单表初始化数据
INSERT INTO menus (id, title, icon, href, parent_id, permission, sort_order, is_active, created_at, updated_at) VALUES
(1, '系统管理', 'settings', '/system', NULL, NULL, 1, TRUE, NOW(), NOW()),
(2, '用户管理', 'user', '/system/users', 1, 'p001', 1, TRUE, NOW(), NOW()),
(3, '角色管理', 'team', '/system/roles', 1, 'p002', 2, TRUE, NOW(), NOW()),
(4, '权限管理', 'lock', '/system/permissions', 1, 'p003', 3, TRUE, NOW(), NOW()),
(5, '菜单管理', 'menu', '/system/menus', 1, NULL, 4, TRUE, NOW(), NOW()),
(6, '数据报表', 'bar-chart', '/report', NULL, NULL, 2, TRUE, NOW(), NOW()),
(7, '报表查看', 'eye', '/report/view', 6, 'p007', 1, TRUE, NOW(), NOW());

44
src/CellularManagement.Domain/ValueObjects/Email.cs

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using CellularManagement.Domain.Common;
namespace CellularManagement.Domain.ValueObjects;
/// <summary>
/// 邮箱值对象
/// </summary>
public class Email : ValueObject
{
private const string EmailPattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
private static readonly Regex EmailRegex = new(EmailPattern, RegexOptions.Compiled);
public string Value { get; }
private Email(string value)
{
Value = value;
}
public static Result<Email> Create(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
return Result<Email>.Failure("邮箱不能为空");
}
if (!EmailRegex.IsMatch(email))
{
return Result<Email>.Failure("邮箱格式不正确");
}
return Result<Email>.Success(new Email(email));
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public static implicit operator string(Email email) => email.Value;
}

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

@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using CellularManagement.Domain.Common;
namespace CellularManagement.Domain.ValueObjects;
/// <summary>
/// 用户名值对象
/// </summary>
public class UserName : ValueObject
{
private const string UserNamePattern = @"^[a-zA-Z0-9_]{3,50}$";
private static readonly Regex UserNameRegex = new(UserNamePattern, RegexOptions.Compiled);
public string Value { get; }
private UserName(string value)
{
Value = value;
}
public static Result<UserName> Create(string userName)
{
if (string.IsNullOrWhiteSpace(userName))
{
return Result<UserName>.Failure("用户名不能为空");
}
if (!UserNameRegex.IsMatch(userName))
{
return Result<UserName>.Failure("用户名只能包含字母、数字和下划线,长度在3-50个字符之间");
}
return Result<UserName>.Success(new UserName(userName));
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public static implicit operator string(UserName userName) => userName.Value;
}

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

@ -79,5 +79,26 @@ public sealed class AppUserConfiguration : IEntityTypeConfiguration<AppUser>
builder.Property(u => u.AccessFailedCount)
.HasComment("登录失败次数");
// 配置审计字段
builder.Property(u => u.CreatedTime)
.IsRequired()
.HasComment("创建时间");
builder.Property(u => u.ModifiedTime)
.HasComment("修改时间");
builder.Property(u => u.IsActive)
.IsRequired()
.HasDefaultValue(true)
.HasComment("用户状态(true: 启用, false: 禁用)");
builder.Property(u => u.IsDeleted)
.IsRequired()
.HasDefaultValue(false)
.HasComment("是否已删除");
// 添加软删除过滤器
builder.HasQueryFilter(u => !u.IsDeleted);
}
}

7
src/CellularManagement.Infrastructure/DependencyInjection.cs

@ -113,6 +113,9 @@ public static class DependencyInjection
// 添加内存缓存
services.AddMemoryCache();
// 添加分布式缓存(使用内存实现)
services.AddDistributedMemoryCache();
// 注册缓存服务
services.AddScoped<ICacheService, CacheService>();
services.AddScoped<ICaptchaService, CaptchaService>();
@ -126,7 +129,9 @@ public static class DependencyInjection
{
action
.FromAssemblies(assembly)
.AddClasses(false)
.AddClasses(classes => classes.Where(type =>
!type.Name.Contains("LockHandle") &&
!type.IsNested))
.UsingRegistrationStrategy(RegistrationStrategy.Skip)
.AsImplementedInterfaces()
.WithScopedLifetime();

310
src/CellularManagement.Infrastructure/Migrations/20250519092205_AddAuditFieldsToAppUser.Designer.cs

@ -0,0 +1,310 @@
// <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("20250519092205_AddAuditFieldsToAppUser")]
partial class AddAuditFieldsToAppUser
{
/// <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<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
}
}
}

66
src/CellularManagement.Infrastructure/Migrations/20250519092205_AddAuditFieldsToAppUser.cs

@ -0,0 +1,66 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class AddAuditFieldsToAppUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "CreatedTime",
table: "Users",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
comment: "创建时间");
migrationBuilder.AddColumn<bool>(
name: "IsActive",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: true,
comment: "用户状态(true: 启用, false: 禁用)");
migrationBuilder.AddColumn<bool>(
name: "IsDeleted",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false,
comment: "是否已删除");
migrationBuilder.AddColumn<DateTime>(
name: "ModifiedTime",
table: "Users",
type: "timestamp with time zone",
nullable: true,
comment: "修改时间");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedTime",
table: "Users");
migrationBuilder.DropColumn(
name: "IsActive",
table: "Users");
migrationBuilder.DropColumn(
name: "IsDeleted",
table: "Users");
migrationBuilder.DropColumn(
name: "ModifiedTime",
table: "Users");
}
}
}

310
src/CellularManagement.Infrastructure/Migrations/20250519093405_RemoveDuplicateAuditFields.Designer.cs

@ -0,0 +1,310 @@
// <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("20250519093405_RemoveDuplicateAuditFields")]
partial class RemoveDuplicateAuditFields
{
/// <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<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
}
}
}

22
src/CellularManagement.Infrastructure/Migrations/20250519093405_RemoveDuplicateAuditFields.cs

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class RemoveDuplicateAuditFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

310
src/CellularManagement.Infrastructure/Migrations/20250519093919_UpdateAppUserAuditFields.Designer.cs

@ -0,0 +1,310 @@
// <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("20250519093919_UpdateAppUserAuditFields")]
partial class UpdateAppUserAuditFields
{
/// <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<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
}
}
}

22
src/CellularManagement.Infrastructure/Migrations/20250519093919_UpdateAppUserAuditFields.cs

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CellularManagement.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class UpdateAppUserAuditFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

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

@ -89,6 +89,10 @@ namespace CellularManagement.Infrastructure.Migrations
.HasColumnType("text")
.HasComment("并发控制戳");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
@ -99,6 +103,18 @@ namespace CellularManagement.Infrastructure.Migrations
.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<bool>("LockoutEnabled")
.HasColumnType("boolean")
.HasComment("是否启用账户锁定");
@ -107,6 +123,10 @@ namespace CellularManagement.Infrastructure.Migrations
.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)")

3
src/CellularManagement.Infrastructure/Services/CaptchaService.cs

@ -14,7 +14,8 @@ public class CaptchaService : ICaptchaService
public (string Code, byte[] ImageBytes) GenerateCaptcha(int length = 4)
{
string code = GenerateRandomCode(length);
int width = 120, height = 40;
int width = Math.Max(120, 10 + length * 25);
int height = 40;
using var bitmap = new SKBitmap(width, height);
using var canvas = new SKCanvas(bitmap);

128
src/CellularManagement.Infrastructure/Services/DistributedLockService.cs

@ -0,0 +1,128 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using CellularManagement.Domain.Services;
namespace CellularManagement.Infrastructure.Services;
/// <summary>
/// 分布式锁服务实现
/// </summary>
public sealed class DistributedLockService : IDistributedLockService
{
private readonly IDistributedCache _cache;
private readonly ILogger<DistributedLockService> _logger;
private readonly int _maxRetryAttempts = 3;
private readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(100);
public DistributedLockService(
IDistributedCache cache,
ILogger<DistributedLockService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<ILockHandle> AcquireLockAsync(string key, TimeSpan timeout)
{
var lockKey = $"lock:{key}";
var lockValue = Guid.NewGuid().ToString();
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = timeout
};
for (int attempt = 1; attempt <= _maxRetryAttempts; attempt++)
{
try
{
// 尝试获取锁
var existingValue = await _cache.GetStringAsync(lockKey);
if (existingValue == null)
{
// 尝试设置锁
await _cache.SetStringAsync(lockKey, lockValue, options);
// 再次检查以确保锁被成功获取
var currentValue = await _cache.GetStringAsync(lockKey);
if (currentValue == lockValue)
{
_logger.LogDebug("成功获取锁 {LockKey}", lockKey);
return new LockHandle(_cache, lockKey, lockValue, true, _logger);
}
}
if (attempt < _maxRetryAttempts)
{
_logger.LogDebug("获取锁 {LockKey} 失败,第 {Attempt} 次重试", lockKey, attempt);
await Task.Delay(_retryDelay);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "获取锁 {LockKey} 时发生错误", lockKey);
if (attempt == _maxRetryAttempts)
{
throw;
}
}
}
_logger.LogWarning("无法获取锁 {LockKey},已达到最大重试次数", lockKey);
return new LockHandle(_cache, lockKey, lockValue, false, _logger);
}
private sealed class LockHandle : ILockHandle
{
private readonly IDistributedCache _cache;
private readonly string _key;
private readonly string _value;
private readonly bool _isAcquired;
private readonly ILogger _logger;
private bool _disposed;
public bool IsAcquired => _isAcquired;
internal LockHandle(
IDistributedCache cache,
string key,
string value,
bool isAcquired,
ILogger logger)
{
_cache = cache;
_key = key;
_value = value;
_isAcquired = isAcquired;
_logger = logger;
}
public void Dispose()
{
if (_disposed)
{
return;
}
if (_isAcquired)
{
try
{
var currentValue = _cache.GetString(_key);
if (currentValue == _value)
{
_cache.Remove(_key);
_logger.LogDebug("成功释放锁 {LockKey}", _key);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "释放锁 {LockKey} 时发生错误", _key);
}
}
_disposed = true;
}
}
}

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

@ -0,0 +1,130 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using CellularManagement.Domain.Entities;
using CellularManagement.Domain.Services;
using CellularManagement.Domain.ValueObjects;
using CellularManagement.Domain.Exceptions;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using CellularManagement.Domain.Repositories;
using System.Threading;
namespace CellularManagement.Infrastructure.Services;
/// <summary>
/// 用户注册领域服务实现
/// </summary>
public class UserRegistrationService : IUserRegistrationService
{
private readonly UserManager<AppUser> _userManager;
private readonly RoleManager<AppRole> _roleManager;
private readonly ILogger<UserRegistrationService> _logger;
private readonly IDistributedLockService _lockService;
private readonly IUserRoleRepository _userRoleRepository;
public UserRegistrationService(
UserManager<AppUser> userManager,
RoleManager<AppRole> roleManager,
ILogger<UserRegistrationService> logger,
IUserRoleRepository userRoleRepository,
IDistributedLockService lockService)
{
_userManager = userManager;
_roleManager = roleManager;
_logger = logger;
_lockService = lockService;
_userRoleRepository = userRoleRepository;
}
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!);
}
// 验证邮箱
var emailResult = Email.Create(user.Email);
if (emailResult.IsFailure)
{
throw new UserRegistrationException(emailResult.Error!);
}
// 使用分布式锁确保用户名和邮箱的唯一性
using var lockHandle = await _lockService.AcquireLockAsync($"user_registration_{user.UserName}", TimeSpan.FromSeconds(10));
if (!lockHandle.IsAcquired)
{
throw new UserRegistrationException("系统繁忙,请稍后重试");
}
// 检查用户名是否存在
var existingUser = await _userManager.FindByNameAsync(user.UserName);
if (existingUser != null)
{
throw new UserNameAlreadyExistsException(user.UserName);
}
// 检查邮箱是否存在
existingUser = await _userManager.FindByEmailAsync(user.Email);
if (existingUser != null)
{
throw new EmailAlreadyExistsException(user.Email);
}
// 创建用户
var result = await _userManager.CreateAsync(user, password);
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description);
throw new UserRegistrationException(string.Join(", ", errors));
}
return (true, null);
}
public async Task<(bool success, string? errorMessage)> AssignUserRoleAsync(AppUser user)
{
// 使用分布式锁确保只有一个用户能被分配为Admin角色
using var lockHandle = await _lockService.AcquireLockAsync("first_user_role_assignment", TimeSpan.FromSeconds(10));
if (!lockHandle.IsAcquired)
{
throw new RoleAssignmentException("系统繁忙,请稍后重试");
}
// 检查是否是第一个用户
var isFirstUser = !await _userManager.Users.AnyAsync();
string roleName = isFirstUser ? "Admin" : "User";
// 获取或创建角色
var role = await _roleManager.FindByNameAsync(roleName);
if (role == null)
{
role = new AppRole { Name = roleName };
var roleResult = await _roleManager.CreateAsync(role);
if (!roleResult.Succeeded)
{
var errors = roleResult.Errors.Select(e => e.Description);
throw new RoleAssignmentException(string.Join(", ", errors));
}
}
// 创建用户角色关系
var userRole = new UserRole
{
UserId = user.Id,
RoleId = role.Id,
User = user
};
//分配角色
var result = await _userRoleRepository.AddAsync(userRole);
if (isFirstUser)
{
_logger.LogInformation("创建了第一个用户 {UserName},已分配管理员角色", user.UserName);
}
return (true, null);
}
}

17
src/CellularManagement.Presentation/appsettings.json

@ -1,17 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Email": {
"SmtpServer": "smtp.example.com",
"SmtpPort": 587,
"FromEmail": "noreply@example.com",
"FromName": "系统通知",
"Password": "your-email-password",
"EnableSsl": true
}
}

41
src/CellularManagement.WebAPI/Program.cs

@ -119,7 +119,28 @@ builder.Services.AddMediatR(cfg =>
// 添加 CORS 服务
// 配置跨域资源共享支持
builder.Services.AddCors();
builder.Services.Configure<CorsOptions>(builder.Configuration.GetSection(CorsOptions.SectionName));
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", policy =>
{
var corsOptions = builder.Configuration.GetSection(CorsOptions.SectionName).Get<CorsOptions>();
if (corsOptions == null)
{
throw new InvalidOperationException("CORS配置未找到");
}
var policyBuilder = policy
.WithOrigins(corsOptions.AllowedOrigins)
.WithMethods(corsOptions.AllowedMethods)
.WithHeaders(corsOptions.AllowedHeaders);
if (corsOptions.AllowCredentials)
{
policyBuilder.AllowCredentials();
}
});
});
// 配置认证服务
// 从配置文件中读取认证相关配置
@ -155,22 +176,21 @@ builder.Services.AddDirectoryBrowser();
var app = builder.Build();
// 配置 HTTP 请求管道
// 在开发环境中启用 Swagger 和 SwaggerUI
if (app.Environment.IsDevelopment())
{
// 启用 Swagger 中间件
app.UseSwagger();
// 启用 Swagger UI 中间件,提供交互式 API 文档界面
app.UseSwaggerUI();
// 在开发环境中禁用 HTTPS 重定向
app.UseHttpsRedirection();
}
else
{
// 在生产环境中强制使用 HTTPS
app.UseHttpsRedirection();
}
// 启用 HTTPS 重定向
// 将 HTTP 请求重定向到 HTTPS
app.UseHttpsRedirection();
// 配置 CORS 策略
// 允许所有来源、所有请求头、所有 HTTP 方法和凭证
app.UseCors(x => x.AllowAnyHeader().SetIsOriginAllowed(p => true).AllowAnyMethod().AllowCredentials());
app.UseCors("CorsPolicy");
// 启用认证中间件
app.UseAuthentication();
@ -183,7 +203,6 @@ app.UseWebSockets(new Microsoft.AspNetCore.Builder.WebSocketOptions
app.UseWebSocketMiddleware();
// 启用授权中间件
// 处理用户认证和授权
app.UseAuthorization();
// 配置静态文件中间件

2
src/CellularManagement.WebAPI/Properties/launchSettings.json

@ -24,7 +24,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7268;http://localhost:5202",
"applicationUrl": "https://localhost:7268;http://localhost:5000;https://192.168.3.147:7268;http://192.168.3.147:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

30
src/CellularManagement.WebAPI/appsettings.json

@ -47,5 +47,35 @@
"CacheKeyPrefix": "EmailVerification_",
"VerificationCodeLength": 6,
"VerificationCodeExpirationMinutes": 2
},
"Cors": {
"AllowedOrigins": [
"http://localhost:5173",
"https://localhost:5173",
"http://192.168.3.147:5173",
"https://192.168.3.147:5173",
"http://192.168.10.2:5173",
"https://192.168.10.2:5173",
"http://192.168.11.2:5173",
"https://192.168.11.2:5173",
"http://192.168.12.3:5173",
"https://192.168.12.3:5173",
"http://localhost:5000",
"https://localhost:7268"
],
"AllowedMethods": [
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
],
"AllowedHeaders": [
"Authorization",
"Content-Type",
"Accept",
"x-api-version"
],
"AllowCredentials": true
}
}

69
src/CellularManagement.WebUI/src/components/auth/RegisterForm.tsx

@ -33,8 +33,18 @@ const registerSchema = z.object({
path: ['confirmPassword'],
});
export interface RegisterRequest {
userName: string;
email: string;
password: string;
confirmPassword: string;
phoneNumber?: string; // 可选
captchaId: string; // 必填
captchaCode: string; // 必填
}
interface RegisterFormProps {
onSubmit: (username: string, email: string, password: string, phoneNumber?: string, captcha?: string, captchaId?: string) => Promise<void>;
onSubmit: (request: RegisterRequest) => Promise<void>;
}
export function RegisterForm({ onSubmit }: RegisterFormProps) {
@ -51,23 +61,26 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
const [error, setError] = useState<string | null>(null);
const [captchaImage, setCaptchaImage] = useState<string>('');
const [captchaId, setCaptchaId] = useState<string>('');
const [captchaAvailable, setCaptchaAvailable] = useState(true);
const [captchaAvailable, setCaptchaAvailable] = useState(false);
// 获取验证码
const fetchCaptcha = async () => {
try {
setCaptchaAvailable(false);
const result = await apiService.getCaptcha();
if (result.success && result.data) {
setCaptchaImage(result.data.imageBase64);
setCaptchaId(result.data.captchaId);
setCaptchaAvailable(true);
setError(null);
} else {
setCaptchaImage('');
setCaptchaId('');
setCaptchaAvailable(false);
setError('获取验证码失败,请重试');
toast({
title: '获取验证码失败',
description: result.message || '请刷新页面重试',
description: result.message || '请点击验证码图片重试',
variant: 'destructive',
duration: 3000,
});
@ -76,9 +89,10 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
setCaptchaImage('');
setCaptchaId('');
setCaptchaAvailable(false);
setError('获取验证码失败,请重试');
toast({
title: '获取验证码失败',
description: '请刷新页面重试',
description: '请点击验证码图片重试',
variant: 'destructive',
duration: 3000,
});
@ -171,14 +185,17 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
setIsLoading(true);
try {
await onSubmit(
formData.username,
formData.email,
formData.password,
formData.phoneNumber || undefined,
formData.captcha,
captchaId
);
const req: RegisterRequest = {
userName: formData.username,
email: formData.email,
password: formData.password,
confirmPassword: formData.confirmPassword,
captchaId,
captchaCode: formData.captcha,
};
if (formData.phoneNumber) req.phoneNumber = formData.phoneNumber;
await onSubmit(req);
} catch (err) {
setError(err instanceof Error ? err.message : '注册失败');
} finally {
@ -323,7 +340,7 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
className={`mt-1 block w-full rounded-md border ${
errors.captcha ? 'border-red-500' : 'border-input'
} bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2`}
placeholder="请输入验证码"
placeholder={captchaAvailable ? "请输入验证码" : "请等待验证码加载"}
disabled={isLoading || !captchaAvailable}
/>
<div className="mt-1">
@ -331,23 +348,21 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
<img
src={`data:image/png;base64,${captchaImage}`}
alt="验证码"
className="h-10 cursor-pointer"
className="h-10 min-w-[80px] max-w-full cursor-pointer"
onClick={fetchCaptcha}
title="点击刷新验证码"
/>
) : (
!captchaAvailable && (
<div className="flex items-center h-10 text-xs text-red-500">
<button
type="button"
className="ml-2 text-blue-500 underline"
onClick={fetchCaptcha}
>
</button>
</div>
)
<div className="flex items-center h-10 text-xs text-red-500">
<button
type="button"
className="ml-2 text-blue-500 underline"
onClick={fetchCaptcha}
>
</button>
</div>
)}
</div>
</div>
@ -366,7 +381,7 @@ export function RegisterForm({ onSubmit }: RegisterFormProps) {
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50"
disabled={isLoading || !captchaAvailable}
>
{isLoading ? '注册中...' : '注册'}
{isLoading ? '注册中...' : !captchaAvailable ? '等待验证码...' : '注册'}
</button>
</form>
);

28
src/CellularManagement.WebUI/src/components/ui/checkbox.tsx

@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

8
src/CellularManagement.WebUI/src/components/ui/table.tsx

@ -4,13 +4,17 @@ import { cn } from '@/lib/utils';
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
>(({ className, children, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
>
{React.Children.map(children, (child, index) =>
React.isValidElement(child) ? React.cloneElement(child, { key: index }) : child
)}
</table>
</div>
));
Table.displayName = 'Table';

5
src/CellularManagement.WebUI/src/config/core/env.config.ts

@ -94,6 +94,11 @@ export class EnvConfigManager {
};
}
// 获取基础 URL(不包含 /api 后缀)
public getBaseUrl(): string {
return this.config.VITE_API_BASE_URL.replace(/\/api$/, '');
}
// 获取认证配置
public getAuthConfig() {
return {

2
src/CellularManagement.WebUI/src/constants/auth.ts

@ -46,5 +46,5 @@ export const AUTH_CONSTANTS = {
export const DEFAULT_CREDENTIALS = {
username: 'zhangsan',
password: 'P@ssw0rd!'
password: 'Zhangsan0001@qq.com'
};

2
src/CellularManagement.WebUI/src/constants/menuConfig.ts

@ -44,7 +44,7 @@ export const menuItems: MenuItem[] = [
},
{
title: '角色管理',
href: '/dashboard/user/roles',
href: '/dashboard/users/roles',
permission: 'roles.view',
},
{

5
src/CellularManagement.WebUI/src/pages/auth/RegisterPage.tsx

@ -2,14 +2,15 @@ import { useNavigate } from 'react-router-dom';
import { RegisterForm } from '@/components/auth/RegisterForm';
import { useAuth } from '@/contexts/AuthContext';
import { toast } from '@/components/ui/use-toast';
import { RegisterRequest } from '@/types/auth';
export function RegisterPage() {
const navigate = useNavigate();
const { register, error } = useAuth();
const handleRegister = async (username: string, email: string, password: string, phoneNumber?: string) => {
const handleRegister = async (request: RegisterRequest) => {
try {
await register({ username, email, password, phoneNumber });
await register(request);
toast({
title: '注册成功',
description: '请使用您的账号登录',

70
src/CellularManagement.WebUI/src/pages/users/UserForm.tsx

@ -0,0 +1,70 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { CreateUserRequest } from '@/services/userService';
interface UserFormProps {
onSubmit: (data: CreateUserRequest) => void;
initialData?: Partial<CreateUserRequest>;
}
export default function UserForm({ onSubmit, initialData }: UserFormProps) {
const [formData, setFormData] = React.useState<CreateUserRequest>({
userName: initialData?.userName || '',
email: initialData?.email || '',
phoneNumber: initialData?.phoneNumber || '',
password: '',
roles: initialData?.roles || []
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="userName"></Label>
<Input
id="userName"
value={formData.userName}
onChange={e => setFormData({ ...formData, userName: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={e => setFormData({ ...formData, email: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="phoneNumber"></Label>
<Input
id="phoneNumber"
value={formData.phoneNumber}
onChange={e => setFormData({ ...formData, phoneNumber: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={e => setFormData({ ...formData, password: e.target.value })}
required={!initialData}
/>
</div>
<Button type="submit" className="w-full">
{initialData ? '更新用户' : '创建用户'}
</Button>
</form>
);
}

110
src/CellularManagement.WebUI/src/pages/users/UserRolesForm.tsx

@ -0,0 +1,110 @@
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Checkbox } from '@/components/ui/checkbox';
import { roleService } from '@/services/roleService';
import { Role } from '@/services/roleService';
import { User } from '@/services/userService';
interface UserRolesFormProps {
user: User;
onSubmit: (roles: string[]) => void;
}
export default function UserRolesForm({ user, onSubmit }: UserRolesFormProps) {
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(false);
const form = useForm({
defaultValues: {
roles: user.roles || [],
},
});
useEffect(() => {
const fetchRoles = async () => {
setLoading(true);
const result = await roleService.getAllRoles();
if (result.success && result.data) {
setRoles(result.data.roles || []);
}
setLoading(false);
};
fetchRoles();
}, []);
const handleSubmit = (data: { roles: string[] }) => {
onSubmit(data.roles);
form.reset();
};
if (loading) {
return <div className="text-center text-muted-foreground">...</div>;
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="roles"
render={() => (
<FormItem>
<FormLabel className="text-foreground"></FormLabel>
<div className="grid grid-cols-2 gap-4">
{roles.map((role) => (
<FormField
key={role.id}
control={form.control}
name="roles"
render={({ field }) => {
return (
<FormItem
key={role.id}
className="flex flex-row items-start space-x-3 space-y-0"
>
<FormControl>
<Checkbox
checked={field.value?.includes(role.name)}
onCheckedChange={(checked: boolean) => {
const currentRoles = field.value || [];
if (checked) {
field.onChange([...currentRoles, role.name]);
} else {
field.onChange(
currentRoles.filter((r) => r !== role.name)
);
}
}}
/>
</FormControl>
<FormLabel className="font-normal">
{role.name}
</FormLabel>
</FormItem>
);
}}
/>
))}
</div>
<FormMessage className="text-destructive" />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90">
</Button>
</div>
</form>
</Form>
);
}

215
src/CellularManagement.WebUI/src/pages/users/UserTable.tsx

@ -0,0 +1,215 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { User } from '@/services/userService';
import { formatToBeijingTime } from '@/lib/utils';
import { DensityType } from '@/components/ui/TableToolbar';
import * as Switch from '@radix-ui/react-switch';
import { userService } from '@/services/userService';
interface ColumnConfig {
key: string;
title: string;
visible: boolean;
fixed?: boolean;
}
interface UserTableProps {
users: User[];
loading: boolean;
onDelete: (userId: string) => void;
onEdit?: (user: User) => void;
onSetRoles?: (user: User) => void;
page: number;
pageSize: number;
total: number;
onPageChange: (page: number) => void;
hideCard?: boolean;
density?: DensityType;
columns?: ColumnConfig[];
}
export default function UserTable({
users,
loading,
onDelete,
onEdit,
onSetRoles,
page,
pageSize,
total,
onPageChange,
hideCard = false,
density = 'default',
columns = [],
}: UserTableProps) {
const totalPages = Math.ceil(total / pageSize);
const Wrapper = hideCard ? React.Fragment : 'div';
const wrapperProps = hideCard ? {} : { className: 'rounded-md border bg-background' };
const rowClass = density === 'relaxed' ? 'h-20' : density === 'compact' ? 'h-8' : 'h-12';
const cellPadding = density === 'relaxed' ? 'py-5' : density === 'compact' ? 'py-1' : 'py-3';
// 过滤可见列
const visibleColumns = columns.length > 0 ? columns.filter(col => col.visible) : [
{ key: 'userName', title: '用户名', visible: true },
{ key: 'email', title: '邮箱', visible: true },
{ key: 'phoneNumber', title: '手机号', visible: true },
{ key: 'isActive', title: '状态', visible: true },
{ key: 'roles', title: '角色', visible: true },
{ key: 'createdAt', title: '创建时间', visible: true },
{ key: 'actions', title: '操作', visible: true }
];
return (
<Wrapper {...wrapperProps}>
<Table>
<TableHeader key="header">
<TableRow className={rowClass}>
{visibleColumns.map(col => (
<TableHead
key={col.key}
className={`text-foreground ${col.key === 'actions' ? 'text-right' : ''} ${cellPadding}`}
>
{col.title}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody key="body">
{loading ? (
<TableRow key="loading" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
...
</TableCell>
</TableRow>
) : users.length === 0 ? (
<TableRow key="empty" className={rowClass}>
<TableCell colSpan={visibleColumns.length} className={`text-center text-muted-foreground ${cellPadding}`}>
</TableCell>
</TableRow>
) : (
users.map((user) => (
<TableRow key={user.id} className={rowClass}>
{visibleColumns.map(col => {
switch (col.key) {
case 'userName':
return <TableCell key={col.key} className={`text-foreground ${cellPadding}`}>{user.userName}</TableCell>;
case 'email':
return <TableCell key={col.key} className={`text-foreground ${cellPadding}`}>{user.email}</TableCell>;
case 'phoneNumber':
return <TableCell key={col.key} className={`text-foreground ${cellPadding}`}>{user.phoneNumber}</TableCell>;
case 'isActive':
return (
<TableCell key={col.key} className={`text-foreground ${cellPadding}`}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<Switch.Root
checked={!!user.isActive}
style={{
width: 44,
height: 24,
background: user.isActive
? 'linear-gradient(90deg, var(--primary, #b620e0), var(--primary, #b620e0))'
: '#e5e7eb',
borderRadius: 999,
border: user.isActive ? '1.5px solid var(--primary, #b620e0)' : '1.5px solid #e5e7eb',
position: 'relative',
transition: 'background 0.2s, border 0.2s',
boxShadow: user.isActive
? '0 0 0 2px var(--primary, #b620e0), 0 2px 8px rgba(0,0,0,0.04)'
: '0 1px 4px rgba(0,0,0,0.04)',
cursor: 'pointer',
}}
onCheckedChange={async (checked) => {
await userService.updateUser(user.id, { ...user, isActive: checked });
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('refresh-users'));
}
}}
>
<Switch.Thumb
style={{
display: 'block',
width: 20,
height: 20,
background: '#fff',
borderRadius: '50%',
boxShadow: '0 2px 8px rgba(0,0,0,0.10)',
transition: 'transform 0.2s, background 0.2s',
transform: user.isActive ? 'translateX(20px)' : 'translateX(2px)',
border: user.isActive ? '1.5px solid var(--primary, #b620e0)' : '1.5px solid #e5e7eb',
}}
/>
</Switch.Root>
<span
style={{
color: user.isActive ? 'var(--primary, #b620e0)' : '#bdbdbd',
fontWeight: 600,
fontSize: 14,
minWidth: 32,
textAlign: 'center',
letterSpacing: 2,
transition: 'color 0.2s',
}}
>
{user.isActive ? '正常' : '禁用'}
</span>
</div>
</TableCell>
);
case 'roles':
return <TableCell key={col.key} className={`text-foreground ${cellPadding}`}>
{Array.isArray(user.roles) && user.roles.length > 0 ? user.roles.join(', ') : '-'}
</TableCell>;
case 'createdAt':
return <TableCell key={col.key} className={`text-foreground ${cellPadding}`}>
{user.createdAt ? formatToBeijingTime(user.createdAt) : '-'}
</TableCell>;
case 'actions':
return (
<TableCell key={col.key} className={`text-right ${cellPadding}`}>
<div className="flex justify-end gap-4">
{onEdit && (
<span
className="cursor-pointer text-blue-600 hover:underline select-none"
onClick={() => onEdit(user)}
>
</span>
)}
{onSetRoles && (
<span
className="cursor-pointer text-blue-600 hover:underline select-none"
onClick={() => onSetRoles(user)}
>
</span>
)}
<span
className="cursor-pointer text-red-500 hover:underline select-none"
onClick={() => onDelete(user.id)}
>
</span>
</div>
</TableCell>
);
default:
return null;
}
})}
</TableRow>
))
)}
</TableBody>
</Table>
</Wrapper>
);
}

208
src/CellularManagement.WebUI/src/pages/users/UsersView.tsx

@ -0,0 +1,208 @@
import React, { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { userService } from '@/services/userService';
import UserTable from './UserTable';
import UserForm from './UserForm';
import { User } from '@/services/userService';
import { Input } from '@/components/ui/input';
import PaginationBar from '@/components/ui/PaginationBar';
import TableToolbar, { DensityType } from '@/components/ui/TableToolbar';
import UserRolesForm from './UserRolesForm';
const defaultColumns = [
{ key: 'userName', title: '用户名', visible: true },
{ key: 'email', title: '邮箱', visible: true },
{ key: 'phoneNumber', title: '手机号', visible: true },
{ key: 'isActive', title: '状态', visible: true },
{ key: 'roles', title: '角色', visible: true },
{ key: 'createdAt', title: '创建时间', visible: true },
{ key: 'actions', title: '操作', visible: true }
];
export default function UsersView() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const [rolesOpen, setRolesOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [total, setTotal] = useState(0);
const [userName, setUserName] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [density, setDensity] = useState<DensityType>('default');
const [columns, setColumns] = useState(defaultColumns);
const fetchUsers = async (params = {}) => {
setLoading(true);
const result = await userService.getAllUsers({ userName, page, pageSize, ...params });
if (result.success && result.data) {
setUsers(result.data.users || []);
setTotal(result.data.totalCount || 0);
}
setLoading(false);
};
useEffect(() => {
fetchUsers();
// eslint-disable-next-line
}, [page, pageSize]);
const handleCreate = async (data: { userName: string; email: string; phoneNumber?: string; password: string; roles?: string[] }) => {
const result = await userService.createUser(data);
if (result.success) {
setOpen(false);
fetchUsers();
}
};
const handleEdit = async (data: { userName: string; email: string; phoneNumber?: string; roles?: string[] }) => {
if (!selectedUser) return;
const result = await userService.updateUser(selectedUser.id, data);
if (result.success) {
setEditOpen(false);
setSelectedUser(null);
fetchUsers();
}
};
const handleSetRoles = async (roles: string[]) => {
if (!selectedUser) return;
const result = await userService.updateUserRoles(selectedUser.id, roles);
if (result.success) {
setRolesOpen(false);
setSelectedUser(null);
fetchUsers();
}
};
const handleDelete = async (userId: string) => {
const result = await userService.deleteUser(userId);
if (result.success) {
fetchUsers();
}
};
// 查询按钮
const handleQuery = () => {
setPage(1);
fetchUsers({ page: 1 });
};
// 重置按钮
const handleReset = () => {
setUserName('');
setPage(1);
fetchUsers({ userName: '', page: 1 });
};
// 每页条数选择
const handlePageSizeChange = (size: number) => {
setPageSize(size);
setPage(1);
};
const totalPages = Math.ceil(total / pageSize);
return (
<main className="flex-1 p-4 transition-all duration-300 ease-in-out sm:p-6">
<div className="w-full space-y-4">
{/* 顶部搜索栏 */}
<div className="flex items-center bg-background p-4 rounded-md border mb-2 gap-4">
<Input
placeholder="请输入用户名"
value={userName}
onChange={e => setUserName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleQuery(); }}
className="w-64 bg-background text-foreground placeholder:text-muted-foreground border border-border focus:outline-none focus:ring-0 focus:border-border transition-all mx-2"
/>
<div className="ml-auto flex gap-2">
<Button variant="outline" onClick={handleReset}></Button>
<Button onClick={handleQuery}></Button>
</div>
</div>
{/* 表格整体卡片区域,包括添加按钮、表格、分页 */}
<div className="rounded-md border bg-background p-4">
{/* 顶部操作栏:添加用户+工具栏 */}
<div className="flex items-center justify-between mb-2">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="bg-primary text-primary-foreground hover:bg-primary/90">+ </Button>
</DialogTrigger>
<DialogContent className="bg-background">
<UserForm onSubmit={handleCreate} />
</DialogContent>
</Dialog>
<TableToolbar
onRefresh={() => fetchUsers()}
onDensityChange={setDensity}
onColumnsChange={setColumns}
onColumnsReset={() => setColumns(defaultColumns)}
columns={columns}
density={density}
/>
</div>
{/* 表格区域 */}
<UserTable
users={users}
loading={loading}
onDelete={handleDelete}
onEdit={(user) => {
setSelectedUser(user);
setEditOpen(true);
}}
onSetRoles={(user) => {
setSelectedUser(user);
setRolesOpen(true);
}}
page={page}
pageSize={pageSize}
total={total}
onPageChange={setPage}
hideCard={true}
density={density}
columns={columns}
/>
{/* 分页 */}
<PaginationBar
page={page}
pageSize={pageSize}
total={total}
onPageChange={setPage}
onPageSizeChange={handlePageSizeChange}
/>
</div>
</div>
{/* 编辑用户对话框 */}
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="bg-background">
{selectedUser && (
<UserForm
onSubmit={handleEdit}
initialData={{
userName: selectedUser.userName,
email: selectedUser.email,
phoneNumber: selectedUser.phoneNumber,
roles: selectedUser.roles
}}
/>
)}
</DialogContent>
</Dialog>
{/* 设置角色对话框 */}
<Dialog open={rolesOpen} onOpenChange={setRolesOpen}>
<DialogContent className="bg-background">
{selectedUser && (
<UserRolesForm
user={selectedUser}
onSubmit={handleSetRoles}
/>
)}
</DialogContent>
</Dialog>
</main>
);
}

17
src/CellularManagement.WebUI/src/routes/AppRouter.tsx

@ -9,7 +9,8 @@ const RegisterPage = lazy(() => import('@/pages/auth/RegisterPage').then(module
const DashboardHome = lazy(() => import('@/pages/dashboard/DashboardHome').then(module => ({ default: module.DashboardHome })));
const ForbiddenPage = lazy(() => import('@/pages/auth/ForbiddenPage'));
const UserManagePage = lazy(() => import('@/pages/dashboard/UserManagePage'));
import RolesView from '@/pages/roles/RolesView';
const RolesView = lazy(() => import('@/pages/roles/RolesView'));
const UsersView = lazy(() => import('@/pages/users/UsersView'));
// 加载中的占位组件
const LoadingFallback = () => (
@ -51,8 +52,18 @@ export function AppRouter() {
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route index element={<DashboardHome />} />
<Route path="user" element={<UserManagePage />}>
<Route path="roles" element={<RolesView />} />
<Route path="users">
<Route index element={<Navigate to="list" replace />} />
<Route path="list" element={
<ProtectedRoute requiredPermission="users.view">
<UsersView />
</ProtectedRoute>
} />
<Route path="roles" element={
<ProtectedRoute requiredPermission="roles.view">
<RolesView />
</ProtectedRoute>
} />
</Route>
{/* 添加更多路由 */}
</Routes>

6
src/CellularManagement.WebUI/src/services/axiosConfig.ts

@ -1,5 +1,6 @@
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { authService } from './authService';
import { envConfig } from '@/config/core/env.config';
const TOKEN_EXPIRY_KEY = 'tokenExpiry';
const TOKEN_REFRESH_THRESHOLD = 5 * 60 * 1000; // 5分钟
@ -8,9 +9,10 @@ class AxiosConfig {
private instance: AxiosInstance;
constructor() {
const apiConfig = envConfig.getApiConfig();
this.instance = axios.create({
baseURL: process.env.REACT_APP_API_URL,
timeout: 10000,
baseURL: apiConfig.baseURL,
timeout: apiConfig.timeout,
headers: {
'Content-Type': 'application/json',
},

157
src/CellularManagement.WebUI/src/services/userService.ts

@ -0,0 +1,157 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
export interface User {
id: string;
userName: string;
email: string;
phoneNumber?: string;
roles: string[];
createdAt: string;
updatedAt: string;
isActive?: boolean;
}
export interface GetAllUsersResponse {
users: User[];
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
hasPreviousPage: boolean;
hasNextPage: boolean;
}
export interface CreateUserRequest {
userName: string;
email: string;
phoneNumber?: string;
password: string;
roles?: string[];
}
export interface UpdateUserRequest {
userName: string;
email: string;
phoneNumber?: string;
roles?: string[];
}
export interface UserService {
getAllUsers: (params?: { userName?: string; page?: number; pageSize?: number }) => Promise<OperationResult<GetAllUsersResponse>>;
getUser: (userId: string) => Promise<OperationResult<User>>;
createUser: (data: CreateUserRequest) => Promise<OperationResult<User>>;
updateUser: (userId: string, data: UpdateUserRequest) => Promise<OperationResult<User>>;
updateUserRoles: (userId: string, roles: string[]) => Promise<OperationResult<User>>;
deleteUser: (userId: string) => Promise<OperationResult<void>>;
}
export const userService: UserService = {
getAllUsers: async (params = {}) => {
try {
const mappedParams = {
PageNumber: params.page ?? 1,
PageSize: params.pageSize ?? 10,
UserName: params.userName ?? undefined
};
const response = await httpClient.get<{ data: GetAllUsersResponse }>('/users', { params: mappedParams });
const resultData = (response.data && 'users' in response.data) ? response.data : response.data?.data;
if (resultData && Array.isArray(resultData.users)) {
resultData.users = resultData.users.map(u => ({
...u,
id: u.id ?? u.userId,
isActive: u.isActive ?? false
}));
}
return {
success: true,
data: resultData as GetAllUsersResponse,
message: '获取用户列表成功'
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || '获取用户列表失败'
};
}
},
getUser: async (userId: string): Promise<OperationResult<User>> => {
try {
const response = await httpClient.get<User>(`/users/${userId}`);
return {
success: true,
data: response.data || undefined,
message: '获取用户详情成功'
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || '获取用户详情失败'
};
}
},
createUser: async (data: CreateUserRequest): Promise<OperationResult<User>> => {
try {
const response = await httpClient.post<User>('/users', data);
return {
success: true,
data: response.data || undefined,
message: '创建用户成功'
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || '创建用户失败'
};
}
},
updateUser: async (userId: string, data: UpdateUserRequest): Promise<OperationResult<User>> => {
try {
const response = await httpClient.put<User>(`/users/${userId}`, data);
return {
success: true,
data: response.data || undefined,
message: '更新用户成功'
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || '更新用户失败'
};
}
},
updateUserRoles: async (userId: string, roles: string[]): Promise<OperationResult<User>> => {
try {
const response = await httpClient.put<User>(`/users/${userId}/roles`, { roles });
return {
success: true,
data: response.data || undefined,
message: '更新用户角色成功'
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || '更新用户角色失败'
};
}
},
deleteUser: async (userId: string): Promise<OperationResult<void>> => {
try {
await httpClient.delete(`/users/${userId}`);
return {
success: true,
message: '删除用户成功'
};
} catch (error: any) {
return {
success: false,
message: error.response?.data?.message || '删除用户失败'
};
}
}
};

6
src/CellularManagement.WebUI/src/types/auth.ts

@ -38,9 +38,13 @@ export type AuthAction =
| { type: 'SET_REMEMBER_ME'; payload: boolean };
export interface RegisterRequest {
username: string;
userName: string;
email: string;
password: string;
confirmPassword: string;
phoneNumber?: string;
captchaId: string;
captchaCode: string;
}
export interface AuthContextType extends AuthState {

5
src/CellularManagement.WebUI/vite.config.ts

@ -9,4 +9,9 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
server: {
host: '0.0.0.0', // ← 支持本地 IP 访问,如 192.168.x.x
port: 5173,
open: true
},
});

34
update-database.ps1

@ -0,0 +1,34 @@
# 数据库迁移脚本
Write-Host "开始执行数据库迁移..." -ForegroundColor Green
try {
# 添加迁移
Write-Host "正在添加迁移..." -ForegroundColor Yellow
dotnet ef migrations add UpdateAppUserAuditFields --project src/CellularManagement.Infrastructure --startup-project src/CellularManagement.WebAPI
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
}
}
catch {
Write-Host "执行过程中发生错误:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
exit 1
}
Write-Host "按任意键退出..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Loading…
Cancel
Save