Browse Source

refactor: 重构用户角色仓储,实现命令查询职责分离

web
hyh 3 months ago
parent
commit
3b237ed510
  1. 18
      src/CellularManagement.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommand.cs
  2. 62
      src/CellularManagement.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommandHandler.cs
  3. 10
      src/CellularManagement.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionResponse.cs
  4. 59
      src/CellularManagement.Domain/Entities/Permission.cs
  5. 49
      src/CellularManagement.Domain/Entities/RolePermission.cs
  6. 67
      src/CellularManagement.Domain/Repositories/IPermissionRepository.cs
  7. 63
      src/CellularManagement.Domain/Repositories/IUserRoleRepository.cs
  8. 40
      src/CellularManagement.Infrastructure/Context/AppDbContext.cs
  9. 5
      src/CellularManagement.Infrastructure/DependencyInjection.cs
  10. 201
      src/CellularManagement.Infrastructure/Repositories/PermissionRepository.cs
  11. 212
      src/CellularManagement.Infrastructure/Repositories/UserRoleRepository.cs
  12. 83
      src/CellularManagement.Presentation/Controllers/PermissionsController.cs
  13. 11
      src/CellularManagement.WebUI/package-lock.json
  14. 3
      src/CellularManagement.WebUI/package.json
  15. 8
      src/CellularManagement.WebUI/src/App.tsx
  16. 12
      src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx
  17. 10
      src/CellularManagement.WebUI/src/components/auth/ProtectedRoute.tsx
  18. 2
      src/CellularManagement.WebUI/src/components/layout/Sidebar.tsx
  19. 61
      src/CellularManagement.WebUI/src/config/core/base-config-manager.ts
  20. 142
      src/CellularManagement.WebUI/src/config/core/env.config.ts
  21. 61
      src/CellularManagement.WebUI/src/config/http-client.config.ts
  22. 92
      src/CellularManagement.WebUI/src/config/http/http-client.config.ts
  23. 71
      src/CellularManagement.WebUI/src/config/types/config.types.ts
  24. 96
      src/CellularManagement.WebUI/src/config/types/env.d.ts
  25. 9
      src/CellularManagement.WebUI/src/constants/menuConfig.ts
  26. 178
      src/CellularManagement.WebUI/src/contexts/AuthContext.tsx
  27. 83
      src/CellularManagement.WebUI/src/hooks/useAuth.ts
  28. 91
      src/CellularManagement.WebUI/src/lib/http-client.ts
  29. 13
      src/CellularManagement.WebUI/src/pages/auth/ForbiddenPage.tsx
  30. 43
      src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx
  31. 19
      src/CellularManagement.WebUI/src/providers/ThemeProvider.tsx
  32. 2
      src/CellularManagement.WebUI/src/routes/AppRouter.tsx
  33. 16
      src/CellularManagement.WebUI/src/services/authService.ts
  34. 42
      src/CellularManagement.WebUI/src/states/appState.ts
  35. 32
      src/CellularManagement.WebUI/src/types/auth.ts
  36. 32
      src/CellularManagement.WebUI/src/types/env.d.ts
  37. 59
      src/CellularManagement.WebUI/test.sql

18
src/CellularManagement.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommand.cs

@ -0,0 +1,18 @@
using MediatR;
using CellularManagement.Application.Common;
namespace CellularManagement.Application.Features.Permissions.Commands.CreatePermission;
/// <summary>
/// 创建权限命令
/// </summary>
public sealed record CreatePermissionCommand(
/// <summary>
/// 权限名称
/// </summary>
string Name,
/// <summary>
/// 权限描述
/// </summary>
string? Description) : IRequest<OperationResult<CreatePermissionResponse>>;

62
src/CellularManagement.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionCommandHandler.cs

@ -0,0 +1,62 @@
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.Logging;
using CellularManagement.Application.Common;
using CellularManagement.Domain.Entities;
using CellularManagement.Domain.Repositories;
namespace CellularManagement.Application.Features.Permissions.Commands.CreatePermission;
/// <summary>
/// 创建权限命令处理器
/// </summary>
public sealed class CreatePermissionCommandHandler : IRequestHandler<CreatePermissionCommand, OperationResult<CreatePermissionResponse>>
{
private readonly IPermissionRepository _permissionRepository;
private readonly ILogger<CreatePermissionCommandHandler> _logger;
/// <summary>
/// 初始化处理器
/// </summary>
public CreatePermissionCommandHandler(
IPermissionRepository permissionRepository,
ILogger<CreatePermissionCommandHandler> logger)
{
_permissionRepository = permissionRepository;
_logger = logger;
}
/// <summary>
/// 处理创建权限请求
/// </summary>
public async Task<OperationResult<CreatePermissionResponse>> Handle(
CreatePermissionCommand request,
CancellationToken cancellationToken)
{
try
{
// 检查权限是否已存在
var existingPermission = await _permissionRepository.GetByNameAsync(request.Name, cancellationToken);
if (existingPermission != null)
{
_logger.LogWarning("权限 {PermissionName} 已存在", request.Name);
return OperationResult<CreatePermissionResponse>.CreateFailure("权限已存在");
}
// 创建权限
var permission = Permission.Create(request.Name, request.Description);
var createdPermission = await _permissionRepository.AddAsync(permission, cancellationToken);
_logger.LogInformation("权限 {PermissionName} 创建成功", request.Name);
return OperationResult<CreatePermissionResponse>.CreateSuccess(
new CreatePermissionResponse(createdPermission.Id));
}
catch (Exception ex)
{
_logger.LogError(ex, "创建权限 {PermissionName} 失败", request.Name);
return OperationResult<CreatePermissionResponse>.CreateFailure("创建权限失败,请稍后重试");
}
}
}

10
src/CellularManagement.Application/Features/Permissions/Commands/CreatePermission/CreatePermissionResponse.cs

@ -0,0 +1,10 @@
namespace CellularManagement.Application.Features.Permissions.Commands.CreatePermission;
/// <summary>
/// 创建权限响应
/// </summary>
public sealed record CreatePermissionResponse(
/// <summary>
/// 权限ID
/// </summary>
int PermissionId);

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

@ -0,0 +1,59 @@
using System;
namespace CellularManagement.Domain.Entities;
/// <summary>
/// 权限实体
/// </summary>
public class Permission
{
/// <summary>
/// 权限ID
/// </summary>
public int Id { get; private set; }
/// <summary>
/// 权限名称
/// </summary>
public string Name { get; private set; }
/// <summary>
/// 权限描述
/// </summary>
public string? Description { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 角色权限关联
/// </summary>
public ICollection<RolePermission> RolePermissions { get; private set; }
private Permission() { }
/// <summary>
/// 创建权限
/// </summary>
public static Permission Create(string name, string? description = null)
{
return new Permission
{
Name = name,
Description = description,
CreatedAt = DateTime.UtcNow,
RolePermissions = new List<RolePermission>()
};
}
/// <summary>
/// 更新权限信息
/// </summary>
public void Update(string name, string? description = null)
{
Name = name;
Description = description;
}
}

49
src/CellularManagement.Domain/Entities/RolePermission.cs

@ -0,0 +1,49 @@
using System;
namespace CellularManagement.Domain.Entities;
/// <summary>
/// 角色权限关联实体
/// </summary>
public class RolePermission
{
/// <summary>
/// 角色ID
/// </summary>
public int RoleId { get; private set; }
/// <summary>
/// 权限ID
/// </summary>
public int PermissionId { get; private set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; private set; }
/// <summary>
/// 角色
/// </summary>
public AppRole Role { get; private set; }
/// <summary>
/// 权限
/// </summary>
public Permission Permission { get; private set; }
private RolePermission() { }
/// <summary>
/// 创建角色权限关联
/// </summary>
public static RolePermission Create(int roleId, int permissionId)
{
return new RolePermission
{
RoleId = roleId,
PermissionId = permissionId,
CreatedAt = DateTime.UtcNow
};
}
}

67
src/CellularManagement.Domain/Repositories/IPermissionRepository.cs

@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CellularManagement.Domain.Entities;
namespace CellularManagement.Domain.Repositories;
/// <summary>
/// 权限命令仓储接口
/// 负责权限的写入操作
/// </summary>
public interface IPermissionCommandRepository : ICommandRepository<Permission>
{
/// <summary>
/// 添加角色权限
/// </summary>
Task AddRolePermissionAsync(RolePermission rolePermission, CancellationToken cancellationToken = default);
/// <summary>
/// 删除角色权限
/// </summary>
Task DeleteRolePermissionAsync(int roleId, int permissionId, CancellationToken cancellationToken = default);
}
/// <summary>
/// 权限查询仓储接口
/// 负责权限的读取操作
/// </summary>
public interface IPermissionQueryRepository : IQueryRepository<Permission>
{
/// <summary>
/// 根据名称获取权限
/// </summary>
Task<Permission?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
/// <summary>
/// 获取角色的所有权限
/// </summary>
Task<IEnumerable<Permission>> GetRolePermissionsAsync(int roleId, CancellationToken cancellationToken = default);
}
/// <summary>
/// 权限仓储接口
/// 组合命令和查询仓储接口
/// </summary>
public interface IPermissionRepository : IPermissionCommandRepository, IPermissionQueryRepository
{
/// <summary>
/// 获取所有权限
/// </summary>
Task<IEnumerable<Permission>> GetAllAsync(CancellationToken cancellationToken = default);
/// <summary>
/// 根据ID获取权限
/// </summary>
Task<Permission?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// 更新权限
/// </summary>
Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default);
/// <summary>
/// 删除权限
/// </summary>
Task DeleteAsync(Permission permission, CancellationToken cancellationToken = default);
}

63
src/CellularManagement.Domain/Repositories/IUserRoleRepository.cs

@ -1,25 +1,68 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CellularManagement.Domain.Entities;
namespace CellularManagement.Domain.Repositories;
/// <summary>
/// 用户角色仓储接口
/// 用户角色命令仓储接口
/// 负责用户角色关系的写入操作
/// </summary>
public interface IUserRoleRepository
public interface IUserRoleCommandRepository : ICommandRepository<UserRole>
{
/// <summary>
/// 添加用户角色关系
/// </summary>
/// <param name="userRole">用户角色关系实体</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>添加的用户角色关系实体</returns>
Task<UserRole> AddAsync(UserRole userRole, CancellationToken cancellationToken = default);
Task<UserRole> AddUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default);
/// <summary>
/// 删除用户角色关系
/// </summary>
Task DeleteUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken = default);
/// <summary>
/// 批量添加用户角色关系
/// </summary>
Task AddUserRolesAsync(IEnumerable<UserRole> userRoles, CancellationToken cancellationToken = default);
/// <summary>
/// 获取用户角色
/// 批量删除用户角色关系
/// </summary>
Task DeleteUserRolesAsync(string userId, IEnumerable<string> roleIds, CancellationToken cancellationToken = default);
}
/// <summary>
/// 用户角色查询仓储接口
/// 负责用户角色关系的读取操作
/// </summary>
public interface IUserRoleQueryRepository : IQueryRepository<UserRole>
{
/// <summary>
/// 获取用户的所有角色
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户角色列表</returns>
Task<IList<string>> GetUserRolesAsync(string userId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取角色的所有用户
/// </summary>
Task<IList<string>> GetRoleUsersAsync(string roleId, CancellationToken cancellationToken = default);
/// <summary>
/// 检查用户是否拥有指定角色
/// </summary>
Task<bool> HasRoleAsync(string userId, string roleId, CancellationToken cancellationToken = default);
/// <summary>
/// 获取用户角色关系
/// </summary>
Task<UserRole?> GetUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken = default);
}
/// <summary>
/// 用户角色仓储接口
/// 组合命令和查询仓储接口
/// </summary>
public interface IUserRoleRepository : IUserRoleCommandRepository, IUserRoleQueryRepository
{
}

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

@ -19,6 +19,16 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
/// </summary>
public new DbSet<UserRole> UserRoles { get; set; } = null!;
/// <summary>
/// 权限集合
/// </summary>
public DbSet<Permission> Permissions { get; set; }
/// <summary>
/// 角色权限关联集合
/// </summary>
public DbSet<RolePermission> RolePermissions { get; set; }
/// <summary>
/// 初始化数据库上下文
/// </summary>
@ -47,5 +57,35 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
modelBuilder.Ignore<IdentityUserClaim<string>>();
modelBuilder.Ignore<IdentityUserToken<string>>();
modelBuilder.Ignore<IdentityUserRole<string>>();
// 配置权限实体
modelBuilder.Entity<Permission>(entity =>
{
entity.ToTable("Permissions");
entity.HasKey(p => p.Id);
entity.Property(p => p.Name).IsRequired().HasMaxLength(50);
entity.Property(p => p.Description).HasMaxLength(200);
entity.Property(p => p.CreatedAt).IsRequired();
});
// 配置角色权限关联实体
modelBuilder.Entity<RolePermission>(entity =>
{
entity.ToTable("RolePermissions");
entity.HasKey(rp => new { rp.RoleId, rp.PermissionId });
entity.Property(rp => rp.CreatedAt).IsRequired();
// 配置与角色的关系
entity.HasOne(rp => rp.Role)
.WithMany()
.HasForeignKey(rp => rp.RoleId)
.OnDelete(DeleteBehavior.Cascade);
// 配置与权限的关系
entity.HasOne(rp => rp.Permission)
.WithMany(p => p.RolePermissions)
.HasForeignKey(rp => rp.PermissionId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}

5
src/CellularManagement.Infrastructure/DependencyInjection.cs

@ -104,10 +104,13 @@ public static class DependencyInjection
// 注册工作单元
services.AddScoped<IUnitOfWork, UnitOfWork>();
// 注册仓储
// 注册通用仓储
services.AddScoped(typeof(ICommandRepository<>), typeof(CommandRepository<>));
services.AddScoped(typeof(IQueryRepository<>), typeof(QueryRepository<>));
// 注册权限仓储
services.AddScoped<IPermissionRepository, PermissionRepository>();
// 添加内存缓存
services.AddMemoryCache();

201
src/CellularManagement.Infrastructure/Repositories/PermissionRepository.cs

@ -0,0 +1,201 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using CellularManagement.Domain.Entities;
using CellularManagement.Domain.Repositories;
using CellularManagement.Infrastructure.Context;
using System.Linq.Expressions;
namespace CellularManagement.Infrastructure.Repositories;
/// <summary>
/// 权限仓储实现类
/// </summary>
public class PermissionRepository :
CommandRepository<Permission>,
IQueryRepository<Permission>,
IPermissionRepository
{
private readonly AppDbContext _context;
private readonly ILogger<PermissionRepository> _logger;
/// <summary>
/// 初始化权限仓储
/// </summary>
public PermissionRepository(
AppDbContext context,
IUnitOfWork unitOfWork,
ILogger<PermissionRepository> logger)
: base(context, unitOfWork, logger)
{
_context = context;
_logger = logger;
}
#region IQueryRepository<Permission> 实现
/// <summary>
/// 根据ID获取权限
/// </summary>
public async Task<Permission?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
{
return await _context.Permissions.FindAsync(new object[] { id }, cancellationToken);
}
/// <summary>
/// 获取所有权限
/// </summary>
public async Task<IEnumerable<Permission>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _context.Permissions.ToListAsync(cancellationToken);
}
/// <summary>
/// 根据条件查询权限
/// </summary>
public async Task<IEnumerable<Permission>> FindAsync(Expression<Func<Permission, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.Permissions.Where(predicate).ToListAsync(cancellationToken);
}
/// <summary>
/// 分页查询权限
/// </summary>
public async Task<(int TotalCount, IEnumerable<Permission> Items)> GetPagedAsync(int pageNumber, int pageSize, CancellationToken cancellationToken = default)
{
var totalCount = await _context.Permissions.CountAsync(cancellationToken);
var items = await _context.Permissions
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (totalCount, items);
}
/// <summary>
/// 根据条件分页查询权限
/// </summary>
public async Task<(int TotalCount, IEnumerable<Permission> Items)> GetPagedAsync(
Expression<Func<Permission, bool>> predicate,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
var query = _context.Permissions.Where(predicate);
var totalCount = await query.CountAsync(cancellationToken);
var items = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (totalCount, items);
}
/// <summary>
/// 获取第一个符合条件的权限
/// </summary>
public async Task<Permission?> FirstOrDefaultAsync(Expression<Func<Permission, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.Permissions.FirstOrDefaultAsync(predicate, cancellationToken);
}
/// <summary>
/// 检查是否存在符合条件的权限
/// </summary>
public async Task<bool> AnyAsync(Expression<Func<Permission, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.Permissions.AnyAsync(predicate, cancellationToken);
}
/// <summary>
/// 统计符合条件的权限数量
/// </summary>
public async Task<int> CountAsync(Expression<Func<Permission, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.Permissions.CountAsync(predicate, cancellationToken);
}
#endregion
#region IPermissionQueryRepository 实现
/// <summary>
/// 根据名称获取权限
/// </summary>
public async Task<Permission?> GetByNameAsync(string name, CancellationToken cancellationToken = default)
{
return await _context.Permissions
.FirstOrDefaultAsync(p => p.Name == name, cancellationToken);
}
/// <summary>
/// 获取角色的所有权限
/// </summary>
public async Task<IEnumerable<Permission>> GetRolePermissionsAsync(int roleId, CancellationToken cancellationToken = default)
{
return await _context.RolePermissions
.Where(rp => rp.RoleId == roleId)
.Select(rp => rp.Permission)
.ToListAsync(cancellationToken);
}
#endregion
#region IPermissionCommandRepository 实现
/// <summary>
/// 添加角色权限
/// </summary>
public async Task AddRolePermissionAsync(RolePermission rolePermission, CancellationToken cancellationToken = default)
{
await _context.RolePermissions.AddAsync(rolePermission, cancellationToken);
}
/// <summary>
/// 删除角色权限
/// </summary>
public async Task DeleteRolePermissionAsync(int roleId, int permissionId, CancellationToken cancellationToken = default)
{
var rolePermission = await _context.RolePermissions
.FirstOrDefaultAsync(rp => rp.RoleId == roleId && rp.PermissionId == permissionId, cancellationToken);
if (rolePermission != null)
{
_context.RolePermissions.Remove(rolePermission);
}
}
#endregion
#region IPermissionRepository 实现
/// <summary>
/// 根据ID获取权限
/// </summary>
public async Task<Permission?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.Permissions.FindAsync(new object[] { id }, cancellationToken);
}
/// <summary>
/// 更新权限
/// </summary>
public async Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default)
{
_context.Permissions.Update(permission);
await Task.CompletedTask;
}
/// <summary>
/// 删除权限
/// </summary>
public async Task DeleteAsync(Permission permission, CancellationToken cancellationToken = default)
{
_context.Permissions.Remove(permission);
await Task.CompletedTask;
}
#endregion
}

212
src/CellularManagement.Infrastructure/Repositories/UserRoleRepository.cs

@ -1,68 +1,216 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using CellularManagement.Domain.Entities;
using CellularManagement.Domain.Repositories;
using CellularManagement.Infrastructure.Context;
using Microsoft.Extensions.Logging;
namespace CellularManagement.Infrastructure.Repositories;
/// <summary>
/// 用户角色仓储实现类
/// </summary>
public class UserRoleRepository : IUserRoleRepository
public class UserRoleRepository :
CommandRepository<UserRole>,
IQueryRepository<UserRole>,
IUserRoleRepository
{
private readonly AppDbContext _context;
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<UserRoleRepository> _logger;
/// <summary>
/// 初始化仓储
/// </summary>
/// <param name="context">数据库上下文</param>
/// <param name="unitOfWork">工作单元</param>
/// <param name="logger">日志记录器</param>
public UserRoleRepository(
AppDbContext context,
IUnitOfWork unitOfWork,
ILogger<UserRoleRepository> logger)
ILogger<UserRoleRepository> logger)
: base(context, unitOfWork, logger)
{
_context = context;
_unitOfWork = unitOfWork;
_logger = logger;
}
/// <inheritdoc />
public async Task<UserRole> AddAsync(UserRole userRole, CancellationToken cancellationToken = default)
#region IUserRoleCommandRepository 实现
/// <summary>
/// 添加用户角色关系
/// </summary>
public async Task<UserRole> AddUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default)
{
await _context.UserRoles.AddAsync(userRole, cancellationToken);
return userRole;
}
/// <summary>
/// 删除用户角色关系
/// </summary>
public async Task DeleteUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken = default)
{
try
var userRole = await _context.UserRoles
.FirstOrDefaultAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken);
if (userRole != null)
{
await _context.UserRoles.AddAsync(userRole, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return userRole;
_context.UserRoles.Remove(userRole);
}
catch (Exception ex)
}
/// <summary>
/// 批量添加用户角色关系
/// </summary>
public async Task AddUserRolesAsync(IEnumerable<UserRole> userRoles, CancellationToken cancellationToken = default)
{
await _context.UserRoles.AddRangeAsync(userRoles, cancellationToken);
}
/// <summary>
/// 批量删除用户角色关系
/// </summary>
public async Task DeleteUserRolesAsync(string userId, IEnumerable<string> roleIds, CancellationToken cancellationToken = default)
{
var userRoles = await _context.UserRoles
.Where(ur => ur.UserId == userId && roleIds.Contains(ur.RoleId))
.ToListAsync(cancellationToken);
if (userRoles.Any())
{
_logger.LogError(ex, "添加用户角色关系时发生错误,用户ID:{UserId},角色ID:{RoleId}",
userRole.UserId, userRole.RoleId);
throw;
_context.UserRoles.RemoveRange(userRoles);
}
}
/// <inheritdoc />
#endregion
#region IUserRoleQueryRepository 实现
/// <summary>
/// 获取用户的所有角色
/// </summary>
public async Task<IList<string>> GetUserRolesAsync(string userId, CancellationToken cancellationToken = default)
{
try
{
return await _context.UserRoles
.Include(ur => ur.Role)
.Where(ur => ur.UserId == userId)
.Select(ur => ur.Role.Name)
.ToListAsync(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户角色时发生错误,用户ID:{UserId}", userId);
throw;
}
return await _context.UserRoles
.Include(ur => ur.Role)
.Where(ur => ur.UserId == userId)
.Select(ur => ur.Role.Name)
.ToListAsync(cancellationToken);
}
/// <summary>
/// 获取角色的所有用户
/// </summary>
public async Task<IList<string>> GetRoleUsersAsync(string roleId, CancellationToken cancellationToken = default)
{
return await _context.UserRoles
.Include(ur => ur.User)
.Where(ur => ur.RoleId == roleId)
.Select(ur => ur.UserId)
.ToListAsync(cancellationToken);
}
/// <summary>
/// 检查用户是否拥有指定角色
/// </summary>
public async Task<bool> HasRoleAsync(string userId, string roleId, CancellationToken cancellationToken = default)
{
return await _context.UserRoles
.AnyAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken);
}
/// <summary>
/// 获取用户角色关系
/// </summary>
public async Task<UserRole?> GetUserRoleAsync(string userId, string roleId, CancellationToken cancellationToken = default)
{
return await _context.UserRoles
.FirstOrDefaultAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken);
}
#endregion
#region IQueryRepository<UserRole> 实现
/// <summary>
/// 根据ID获取用户角色关系
/// </summary>
public async Task<UserRole?> GetByIdAsync(string id, CancellationToken cancellationToken = default)
{
return await _context.UserRoles.FindAsync(new object[] { id }, cancellationToken);
}
/// <summary>
/// 获取所有用户角色关系
/// </summary>
public async Task<IEnumerable<UserRole>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _context.UserRoles.ToListAsync(cancellationToken);
}
/// <summary>
/// 根据条件查询用户角色关系
/// </summary>
public async Task<IEnumerable<UserRole>> FindAsync(System.Linq.Expressions.Expression<Func<UserRole, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.UserRoles.Where(predicate).ToListAsync(cancellationToken);
}
/// <summary>
/// 分页查询用户角色关系
/// </summary>
public async Task<(int TotalCount, IEnumerable<UserRole> Items)> GetPagedAsync(int pageNumber, int pageSize, CancellationToken cancellationToken = default)
{
var totalCount = await _context.UserRoles.CountAsync(cancellationToken);
var items = await _context.UserRoles
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (totalCount, items);
}
/// <summary>
/// 根据条件分页查询用户角色关系
/// </summary>
public async Task<(int TotalCount, IEnumerable<UserRole> Items)> GetPagedAsync(
System.Linq.Expressions.Expression<Func<UserRole, bool>> predicate,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
var query = _context.UserRoles.Where(predicate);
var totalCount = await query.CountAsync(cancellationToken);
var items = await query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
return (totalCount, items);
}
/// <summary>
/// 获取第一个符合条件的用户角色关系
/// </summary>
public async Task<UserRole?> FirstOrDefaultAsync(System.Linq.Expressions.Expression<Func<UserRole, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.UserRoles.FirstOrDefaultAsync(predicate, cancellationToken);
}
/// <summary>
/// 检查是否存在符合条件的用户角色关系
/// </summary>
public async Task<bool> AnyAsync(System.Linq.Expressions.Expression<Func<UserRole, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.UserRoles.AnyAsync(predicate, cancellationToken);
}
/// <summary>
/// 统计符合条件的用户角色关系数量
/// </summary>
public async Task<int> CountAsync(System.Linq.Expressions.Expression<Func<UserRole, bool>> predicate, CancellationToken cancellationToken = default)
{
return await _context.UserRoles.CountAsync(predicate, cancellationToken);
}
#endregion
}

83
src/CellularManagement.Presentation/Controllers/PermissionsController.cs

@ -0,0 +1,83 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using CellularManagement.Application.Features.Permissions.Commands.CreatePermission;
using CellularManagement.Application.Common;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using CellularManagement.Presentation.Abstractions;
namespace CellularManagement.Presentation.Controllers;
/// <summary>
/// 权限管理控制器
/// 提供权限管理相关的 API 接口,包括创建、删除和查询权限功能
/// </summary>
[Authorize(Roles = "Admin")] // 只有管理员可以访问
public class PermissionsController : ApiController
{
private readonly ILogger<PermissionsController> _logger;
/// <summary>
/// 初始化权限控制器
/// </summary>
/// <param name="mediator">MediatR 中介者,用于处理命令和查询</param>
/// <param name="logger">日志记录器</param>
public PermissionsController(
IMediator mediator,
ILogger<PermissionsController> logger) : base(mediator)
{
_logger = logger;
}
/// <summary>
/// 创建新权限
/// </summary>
/// <remarks>
/// 示例请求:
///
/// POST /api/permissions/create
/// {
/// "name": "CreateUser",
/// "description": "创建用户的权限"
/// }
///
/// </remarks>
/// <param name="command">创建权限命令,包含权限名称和描述</param>
/// <returns>
/// 创建结果,包含:
/// - 成功:返回权限ID
/// - 失败:返回错误信息
/// </returns>
/// <response code="200">创建成功,返回权限ID</response>
/// <response code="400">创建失败,返回错误信息</response>
[HttpPost]
[ProducesResponseType(typeof(OperationResult<CreatePermissionResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(OperationResult<CreatePermissionResponse>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<OperationResult<CreatePermissionResponse>>> CreatePermission([FromBody] CreatePermissionCommand command)
{
try
{
var result = await mediator.Send(command);
if (result.IsSuccess)
{
_logger.LogInformation("权限 {PermissionName} 创建成功", command.Name);
}
else
{
_logger.LogWarning("权限 {PermissionName} 创建失败: {Error}",
command.Name,
result.ErrorMessages?.FirstOrDefault());
}
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "创建权限 {PermissionName} 时发生异常", command.Name);
return StatusCode(StatusCodes.Status500InternalServerError,
OperationResult<CreatePermissionResponse>.CreateFailure("系统错误,请稍后重试"));
}
}
}

11
src/CellularManagement.WebUI/package-lock.json

@ -23,7 +23,8 @@
"react-router-dom": "^6.22.0",
"recoil": "^0.7.7",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/node": "^20.11.16",
@ -5198,6 +5199,14 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "3.24.4",
"resolved": "https://registry.npmmirror.com/zod/-/zod-3.24.4.tgz",
"integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

3
src/CellularManagement.WebUI/package.json

@ -25,7 +25,8 @@
"react-router-dom": "^6.22.0",
"recoil": "^0.7.7",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/node": "^20.11.16",

8
src/CellularManagement.WebUI/src/App.tsx

@ -1,12 +1,18 @@
import { BrowserRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import { AppRouter } from './routes/AppRouter';
import { AuthProvider } from './contexts/AuthContext';
import { ThemeProvider } from './providers/ThemeProvider';
export function App() {
return (
<RecoilRoot>
<BrowserRouter>
<AppRouter />
<AuthProvider>
<ThemeProvider>
<AppRouter />
</ThemeProvider>
</AuthProvider>
</BrowserRouter>
</RecoilRoot>
);

12
src/CellularManagement.WebUI/src/components/auth/LoginForm.tsx

@ -1,15 +1,19 @@
import { useState } from 'react';
import { DEFAULT_CREDENTIALS } from '@/constants/auth';
import { useAuth } from '@/hooks/useAuth';
import { useAuth } from '@/contexts/AuthContext';
export function LoginForm() {
interface LoginFormProps {
onSubmit: (username: string, password: string) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [username, setUsername] = useState(DEFAULT_CREDENTIALS.username);
const [password, setPassword] = useState(DEFAULT_CREDENTIALS.password);
const { login, isLoading, error } = useAuth();
const { isLoading, error } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login({ username, password });
await onSubmit(username, password);
};
return (

10
src/CellularManagement.WebUI/src/components/auth/ProtectedRoute.tsx

@ -1,5 +1,5 @@
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useAuth } from '@/contexts/AuthContext';
import { hasPermission, Permission } from '@/constants/menuConfig';
interface ProtectedRouteProps {
@ -8,10 +8,10 @@ interface ProtectedRouteProps {
}
export function ProtectedRoute({ children, requiredPermission }: ProtectedRouteProps) {
const { isAuthenticated, userPermissions, loading } = useAuth();
const { isAuthenticated, userPermissions, isLoading } = useAuth();
const location = useLocation();
if (loading) {
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
@ -24,7 +24,9 @@ export function ProtectedRoute({ children, requiredPermission }: ProtectedRouteP
return <Navigate to="/login" state={{ from: location }} replace />;
}
if (requiredPermission && !hasPermission(userPermissions, requiredPermission)) {
// 确保 userPermissions 存在且是数组
const permissions = userPermissions || [];
if (requiredPermission && !hasPermission(permissions, requiredPermission)) {
// 如果用户没有所需权限,重定向到403页面
return <Navigate to="/403" replace />;
}

2
src/CellularManagement.WebUI/src/components/layout/Sidebar.tsx

@ -4,7 +4,7 @@ import { SidebarMenuItem } from '@/components/ui/SidebarMenuItem';
import { SidebarToggleButton } from '@/components/ui/SidebarToggleButton';
import { useSidebarToggle } from '@/hooks/useSidebarToggle';
import { menuItems, hasPermission, Permission, MenuItem } from '@/constants/menuConfig';
import { useAuth } from '@/hooks/useAuth'; // 假设你有一个useAuth hook来获取用户权限
import { useAuth } from '@/contexts/AuthContext';
export function Sidebar() {
const { isCollapsed, toggleSidebar } = useSidebarToggle();

61
src/CellularManagement.WebUI/src/config/core/base-config-manager.ts

@ -0,0 +1,61 @@
import { Environment } from '../types/config.types';
// 基础配置管理器类
export abstract class BaseConfigManager<T> {
protected static instance: any;
protected config!: T;
protected environment: Environment;
protected constructor() {
this.environment = this.getCurrentEnvironment();
this.config = this.getDefaultConfig();
}
// 获取单例实例
public static getInstance(): any {
if (!this.instance) {
this.instance = new (this as any)();
}
return this.instance;
}
// 获取当前环境
protected getCurrentEnvironment(): Environment {
const env = import.meta.env.MODE || 'development';
return (env as Environment) || 'development';
}
// 获取配置
public getConfig(): T {
return { ...this.config };
}
// 更新配置
public updateConfig(newConfig: Partial<T>): void {
this.config = {
...this.config,
...newConfig
};
}
// 重置配置
public resetConfig(): void {
this.config = this.getDefaultConfig();
}
// 获取默认配置(需要子类实现)
protected abstract getDefaultConfig(): T;
// 环境检查方法
public isDevelopment(): boolean {
return this.environment === 'development';
}
public isStaging(): boolean {
return this.environment === 'staging';
}
public isProduction(): boolean {
return this.environment === 'production';
}
}

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

@ -0,0 +1,142 @@
import { z } from 'zod';
import type { ApiConfig, AuthConfig, AppConfig, MockConfig, Environment } from '../types/config.types';
// 默认配置
const DEFAULT_CONFIG = {
// API配置
VITE_API_BASE_URL: 'http://localhost:5202/api',
VITE_API_TIMEOUT: '30000',
VITE_API_VERSION: 'v1',
VITE_API_MAX_RETRIES: '3',
VITE_API_RETRY_DELAY: '1000',
// 应用配置
VITE_APP_TITLE: '蜂窝管理系统',
VITE_APP_DESCRIPTION: '蜂窝管理系统 - 一个现代化的蜂窝网络管理平台',
// 认证配置
VITE_AUTH_TOKEN_KEY: 'access_token',
VITE_AUTH_REFRESH_TOKEN_KEY: 'refresh_token',
VITE_AUTH_TOKEN_EXPIRES_KEY: 'token_expires',
// Mock配置
VITE_ENABLE_MOCK: 'true',
VITE_MOCK_DELAY: '500',
// 环境标识
MODE: 'development'
} as const;
// 环境变量验证模式
const envSchema = z.object({
// API配置
VITE_API_BASE_URL: z.string().url().default(DEFAULT_CONFIG.VITE_API_BASE_URL),
VITE_API_TIMEOUT: z.string().transform(Number).default(DEFAULT_CONFIG.VITE_API_TIMEOUT),
VITE_API_VERSION: z.string().default(DEFAULT_CONFIG.VITE_API_VERSION),
VITE_API_MAX_RETRIES: z.string().transform(Number).default(DEFAULT_CONFIG.VITE_API_MAX_RETRIES),
VITE_API_RETRY_DELAY: z.string().transform(Number).default(DEFAULT_CONFIG.VITE_API_RETRY_DELAY),
// 应用配置
VITE_APP_TITLE: z.string().default(DEFAULT_CONFIG.VITE_APP_TITLE),
VITE_APP_DESCRIPTION: z.string().default(DEFAULT_CONFIG.VITE_APP_DESCRIPTION),
// 认证配置
VITE_AUTH_TOKEN_KEY: z.string().default(DEFAULT_CONFIG.VITE_AUTH_TOKEN_KEY),
VITE_AUTH_REFRESH_TOKEN_KEY: z.string().default(DEFAULT_CONFIG.VITE_AUTH_REFRESH_TOKEN_KEY),
VITE_AUTH_TOKEN_EXPIRES_KEY: z.string().default(DEFAULT_CONFIG.VITE_AUTH_TOKEN_EXPIRES_KEY),
// Mock配置
VITE_ENABLE_MOCK: z.string().transform((val: string) => val === 'true').default(DEFAULT_CONFIG.VITE_ENABLE_MOCK),
VITE_MOCK_DELAY: z.string().transform(Number).default(DEFAULT_CONFIG.VITE_MOCK_DELAY),
// 环境标识
MODE: z.enum(['development', 'staging', 'production']).default(DEFAULT_CONFIG.MODE)
});
// 环境配置类型
type EnvConfig = z.infer<typeof envSchema>;
// 环境配置管理器
export class EnvConfigManager {
private static instance: EnvConfigManager;
private config: EnvConfig;
private constructor() {
try {
this.config = envSchema.parse(import.meta.env);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors.map(err =>
`${err.path.join('.')}: ${err.message}`
);
throw new Error(`环境变量验证失败:\n${errorMessages.join('\n')}`);
}
throw error;
}
}
// 获取单例实例
public static getInstance(): EnvConfigManager {
if (!EnvConfigManager.instance) {
EnvConfigManager.instance = new EnvConfigManager();
}
return EnvConfigManager.instance;
}
// 获取API配置
public getApiConfig() {
return {
baseURL: this.config.VITE_API_BASE_URL,
timeout: this.config.VITE_API_TIMEOUT,
version: this.config.VITE_API_VERSION,
maxRetries: this.config.VITE_API_MAX_RETRIES,
retryDelay: this.config.VITE_API_RETRY_DELAY
};
}
// 获取认证配置
public getAuthConfig() {
return {
tokenKey: this.config.VITE_AUTH_TOKEN_KEY,
refreshTokenKey: this.config.VITE_AUTH_REFRESH_TOKEN_KEY,
tokenExpiresKey: this.config.VITE_AUTH_TOKEN_EXPIRES_KEY
};
}
// 获取应用配置
public getAppConfig() {
return {
title: this.config.VITE_APP_TITLE,
description: this.config.VITE_APP_DESCRIPTION
};
}
// 获取Mock配置
public getMockConfig() {
return {
enabled: this.config.VITE_ENABLE_MOCK,
delay: this.config.VITE_MOCK_DELAY
};
}
// 获取当前环境
public getEnvironment() {
return this.config.MODE;
}
// 环境检查方法
public isDevelopment() {
return this.config.MODE === 'development';
}
public isStaging() {
return this.config.MODE === 'staging';
}
public isProduction() {
return this.config.MODE === 'production';
}
}
// 导出默认实例
export const envConfig = EnvConfigManager.getInstance();

61
src/CellularManagement.WebUI/src/config/http-client.config.ts

@ -1,61 +0,0 @@
import { HttpClientConfig } from '@/lib/http-client';
export class HttpClientConfigManager {
private static instance: HttpClientConfigManager;
private config: HttpClientConfig;
private constructor() {
this.config = {
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5202/api',
timeout: Number(import.meta.env.VITE_API_TIMEOUT) || 10000,
headers: {
'Content-Type': 'application/json',
'X-API-Version': import.meta.env.VITE_API_VERSION || 'v1',
},
};
}
public static getInstance(): HttpClientConfigManager {
if (!HttpClientConfigManager.instance) {
HttpClientConfigManager.instance = new HttpClientConfigManager();
}
return HttpClientConfigManager.instance;
}
public getConfig(): HttpClientConfig {
return { ...this.config };
}
public updateConfig(newConfig: Partial<HttpClientConfig>): void {
this.config = {
...this.config,
...newConfig,
headers: {
...this.config.headers,
...newConfig.headers,
},
};
}
public setBaseURL(baseURL: string): void {
this.config.baseURL = baseURL;
}
public setTimeout(timeout: number): void {
this.config.timeout = timeout;
}
public setHeader(key: string, value: string): void {
if (!this.config.headers) {
this.config.headers = {};
}
this.config.headers[key] = value;
}
public removeHeader(key: string): void {
if (this.config.headers) {
const { [key]: removed, ...headers } = this.config.headers;
this.config.headers = headers;
}
}
}

92
src/CellularManagement.WebUI/src/config/http/http-client.config.ts

@ -0,0 +1,92 @@
import { BaseConfigManager } from '../core/base-config-manager';
import { HttpClientConfig } from '../types/config.types';
import { envConfig } from '../core/env.config';
// HTTP客户端配置管理器
export class HttpClientConfigManager extends BaseConfigManager<HttpClientConfig> {
protected getDefaultConfig(): HttpClientConfig {
const apiConfig = envConfig.getApiConfig();
return {
baseURL: apiConfig.baseURL,
timeout: apiConfig.timeout,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-API-Version': apiConfig.version
},
maxRetries: apiConfig.maxRetries,
retryDelay: apiConfig.retryDelay
};
}
// 更新配置
public updateConfig(newConfig: Partial<HttpClientConfig>): void {
this.validateConfig(newConfig);
super.updateConfig(newConfig);
}
// 配置验证
private validateConfig(config: Partial<HttpClientConfig>): void {
if (config.timeout && config.timeout < 0) {
throw new Error('超时时间不能为负数');
}
if (config.maxRetries && config.maxRetries < 0) {
throw new Error('重试次数不能为负数');
}
if (config.retryDelay && config.retryDelay < 0) {
throw new Error('重试延迟不能为负数');
}
if (config.baseURL && !config.baseURL.startsWith('http')) {
throw new Error('baseURL 必须是有效的 HTTP(S) URL');
}
}
// 设置基础URL
public setBaseURL(baseURL: string): void {
this.validateConfig({ baseURL });
this.config.baseURL = baseURL;
}
// 设置超时时间
public setTimeout(timeout: number): void {
this.validateConfig({ timeout });
this.config.timeout = timeout;
}
// 设置请求头
public setHeader(key: string, value: string): void {
if (!this.config.headers) {
this.config.headers = {};
}
this.config.headers[key] = value;
}
// 移除请求头
public removeHeader(key: string): void {
if (this.config.headers) {
const { [key]: removed, ...headers } = this.config.headers;
this.config.headers = headers;
}
}
// 获取所有请求头
public getHeaders(): Record<string, string> {
return { ...this.config.headers };
}
// 检查是否包含特定请求头
public hasHeader(key: string): boolean {
return Boolean(this.config.headers?.[key]);
}
// 获取请求头值
public getHeader(key: string): string | undefined {
return this.config.headers?.[key];
}
}
// 导出默认实例
export const httpClientConfig = HttpClientConfigManager.getInstance();

71
src/CellularManagement.WebUI/src/config/types/config.types.ts

@ -0,0 +1,71 @@
// API配置类型
export interface ApiConfig {
baseURL: string;
timeout: number;
version: string;
maxRetries: number;
retryDelay: number;
}
// 认证配置类型
export interface AuthConfig {
tokenKey: string;
refreshTokenKey: string;
tokenExpiresKey: string;
setToken(token: string): void;
getToken(): string | null;
setRefreshToken(token: string): void;
getRefreshToken(): string | null;
setTokenExpires(expires: number): void;
getTokenExpires(): number | null;
clearTokens(): void;
isTokenExpired(): boolean;
}
// 应用配置类型
export interface AppConfig {
title: string;
description: string;
}
// Mock配置类型
export interface MockConfig {
enabled: boolean;
delay: number;
}
// 环境类型
export type Environment = 'development' | 'staging' | 'production';
// HTTP客户端配置类型
export interface HttpClientConfig {
baseURL: string;
timeout?: number;
headers?: Record<string, string>;
maxRetries?: number;
retryDelay?: number;
}
// 环境配置类型
export interface EnvironmentConfig {
baseURL: string;
timeout: number;
headers: Record<string, string>;
}
// 完整配置类型
export interface Config {
api: ApiConfig;
auth: AuthConfig;
app: AppConfig;
mock: MockConfig;
environment: Environment;
}
// 操作结果接口
export interface OperationResult<T = any> {
successMessage: string | null;
errorMessages: string[];
data: T | null;
isSuccess: boolean;
}

96
src/CellularManagement.WebUI/src/config/types/env.d.ts

@ -0,0 +1,96 @@
/// <reference types="vite/client" />
/**
* Vite环境变量类型声明
* TypeScript类型检查和IDE智能提示
*/
interface ImportMetaEnv {
// API配置
/** API基础URL */
readonly VITE_API_BASE_URL: string;
/** API请求超时时间(毫秒) */
readonly VITE_API_TIMEOUT: string;
/** API版本号 */
readonly VITE_API_VERSION: string;
/** API请求最大重试次数 */
readonly VITE_API_MAX_RETRIES: string;
/** API请求重试延迟时间(毫秒) */
readonly VITE_API_RETRY_DELAY: string;
// 应用配置
/** 应用标题 */
readonly VITE_APP_TITLE: string;
/** 应用描述 */
readonly VITE_APP_DESCRIPTION: string;
// 认证配置
/** 访问令牌存储键名 */
readonly VITE_AUTH_TOKEN_KEY: string;
/** 刷新令牌存储键名 */
readonly VITE_AUTH_REFRESH_TOKEN_KEY: string;
/** 令牌过期时间存储键名 */
readonly VITE_AUTH_TOKEN_EXPIRES_KEY: string;
// Mock配置
/** 是否启用Mock数据 */
readonly VITE_ENABLE_MOCK: string;
/** Mock数据延迟时间(毫秒) */
readonly VITE_MOCK_DELAY: string;
// 环境标识
/** 当前运行环境 */
readonly MODE: 'development' | 'staging' | 'production';
}
/**
* Vite环境变量接口
*/
interface ImportMeta {
readonly env: ImportMetaEnv;
}
/**
* Node.js环境变量类型声明
*/
declare namespace NodeJS {
interface ProcessEnv extends ImportMetaEnv {}
}
/**
*
*/
interface EnvConfig {
/** API基础URL */
readonly API_BASE_URL: string;
/** API请求超时时间(毫秒) */
readonly API_TIMEOUT: number;
/** API版本号 */
readonly API_VERSION: string;
/** API请求最大重试次数 */
readonly API_MAX_RETRIES: number;
/** API请求重试延迟时间(毫秒) */
readonly API_RETRY_DELAY: number;
/** 应用标题 */
readonly APP_TITLE: string;
/** 应用描述 */
readonly APP_DESCRIPTION: string;
/** 访问令牌存储键名 */
readonly AUTH_TOKEN_KEY: string;
/** 刷新令牌存储键名 */
readonly AUTH_REFRESH_TOKEN_KEY: string;
/** 令牌过期时间存储键名 */
readonly AUTH_TOKEN_EXPIRES_KEY: string;
/** 是否启用Mock数据 */
readonly ENABLE_MOCK: boolean;
/** Mock数据延迟时间(毫秒) */
readonly MOCK_DELAY: number;
/** 当前运行环境 */
readonly MODE: 'development' | 'staging' | 'production';
}
// 在代码中使用环境变量
import { envConfig } from '../core/env.config';
console.log(envConfig.API_BASE_URL); // 从环境变量获取
console.log(envConfig.API_TIMEOUT); // 自动转换为数字
console.log(envConfig.API_VERSION); // 从环境变量获取

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

@ -63,7 +63,12 @@ export const menuItems: MenuItem[] = [
];
// 导出权限检查工具函数
export const hasPermission = (userPermissions: Permission[], requiredPermission?: Permission): boolean => {
if (!requiredPermission) return true; // 如果没有设置权限要求,则默认允许访问
export const hasPermission = (userPermissions: Permission[] | undefined | null, requiredPermission?: Permission): boolean => {
// 如果没有设置权限要求,则默认允许访问
if (!requiredPermission) return true;
// 如果用户权限为空,则拒绝访问
if (!userPermissions || !Array.isArray(userPermissions)) return false;
return userPermissions.includes(requiredPermission);
};

178
src/CellularManagement.WebUI/src/contexts/AuthContext.tsx

@ -0,0 +1,178 @@
import { createContext, useContext, useReducer, ReactNode, useEffect, useMemo } from 'react';
import { AuthState, AuthContextType, LoginRequest, User } from '@/types/auth';
import { authService } from '@/services/authService';
import { useSetRecoilState } from 'recoil';
import { userState } from '@/states/appState';
const initialState: AuthState = {
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
userPermissions: [],
};
type AuthAction =
| { type: 'LOGIN_START' }
| { type: 'LOGIN_SUCCESS'; payload: { user: User; accessToken: string; refreshToken: string } }
| { type: 'LOGIN_FAILURE'; payload: string }
| { type: 'LOGOUT' }
| { type: 'CLEAR_ERROR' }
| { type: 'SET_USER'; payload: User };
const authReducer = (state: AuthState, action: AuthAction): AuthState => {
switch (action.type) {
case 'LOGIN_START':
return { ...state, isLoading: true, error: null };
case 'LOGIN_SUCCESS':
return {
...state,
isLoading: false,
isAuthenticated: true,
user: action.payload.user,
userPermissions: [
...(Array.isArray(action.payload.user.permissions) ? action.payload.user.permissions : []),
'dashboard.view',
'users.view',
'settings.view'
],
error: null,
};
case 'LOGIN_FAILURE':
return {
...state,
isLoading: false,
isAuthenticated: false,
user: null,
userPermissions: [],
error: action.payload,
};
case 'LOGOUT':
return initialState;
case 'CLEAR_ERROR':
return { ...state, error: null };
case 'SET_USER':
return {
...state,
user: action.payload,
userPermissions: [
...(Array.isArray(action.payload.permissions) ? action.payload.permissions : []),
'dashboard.view',
'users.view',
'settings.view'
],
isAuthenticated: true,
};
default:
return state;
}
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, initialState);
const setGlobalUser = useSetRecoilState(userState);
// 同步用户状态到 Recoil
useEffect(() => {
setGlobalUser(state.user);
}, [state.user, setGlobalUser]);
// 初始化时检查用户状态
useEffect(() => {
const initAuth = async () => {
const token = localStorage.getItem('accessToken');
if (token) {
try {
const result = await authService.getCurrentUser();
if (result.isSuccess && result.data?.user) {
dispatch({ type: 'SET_USER', payload: result.data.user });
} else {
dispatch({ type: 'LOGOUT' });
}
} catch {
dispatch({ type: 'LOGOUT' });
}
}
};
initAuth();
}, []);
const login = async (request: LoginRequest) => {
dispatch({ type: 'LOGIN_START' });
try {
const result = await authService.login(request);
if (result.isSuccess && result.data) {
const { accessToken, refreshToken, user } = result.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
dispatch({ type: 'LOGIN_SUCCESS', payload: { user, accessToken, refreshToken } });
} else {
dispatch({
type: 'LOGIN_FAILURE',
payload: result.errorMessages?.[0] || '登录失败',
});
}
} catch (error) {
dispatch({
type: 'LOGIN_FAILURE',
payload: '登录请求失败,请稍后重试',
});
}
};
const logout = async () => {
try {
await authService.logout();
} finally {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
dispatch({ type: 'LOGOUT' });
}
};
const refreshToken = async () => {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
dispatch({ type: 'LOGOUT' });
return;
}
try {
const result = await authService.refreshToken(refreshToken);
if (result.isSuccess && result.data) {
const { accessToken, refreshToken: newRefreshToken, user } = result.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);
dispatch({ type: 'SET_USER', payload: user });
} else {
dispatch({ type: 'LOGOUT' });
}
} catch {
dispatch({ type: 'LOGOUT' });
}
};
// 使用 useMemo 优化 Context 值
const contextValue = useMemo(() => ({
...state,
login,
logout,
refreshToken,
}), [state]);
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

83
src/CellularManagement.WebUI/src/hooks/useAuth.ts

@ -1,83 +0,0 @@
import { useState, useCallback } from 'react';
import { authService } from '@/services/authService';
import { LoginRequest, LoginResponse } from '@/types/auth';
import { useNavigate } from 'react-router-dom';
export function useAuth() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const navigate = useNavigate();
const login = useCallback(async (request: LoginRequest) => {
setIsLoading(true);
setError(null);
try {
const result = await authService.login(request);
if (result.isSuccess && result.data) {
localStorage.setItem('accessToken', result.data.accessToken);
localStorage.setItem('refreshToken', result.data.refreshToken);
navigate('/');
return result.data;
} else {
setError(result.errorMessages?.[0] || '登录失败,请检查用户名和密码');
return null;
}
} catch (err) {
setError('登录请求失败,请稍后重试');
return null;
} finally {
setIsLoading(false);
}
}, [navigate]);
const logout = useCallback(async () => {
try {
await authService.logout();
navigate('/login');
} catch (err) {
setError('登出失败,请稍后重试');
}
}, [navigate]);
const refreshToken = useCallback(async () => {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) {
return null;
}
try {
const result = await authService.refreshToken(refreshToken);
if (result.isSuccess && result.data) {
localStorage.setItem('accessToken', result.data.accessToken);
localStorage.setItem('refreshToken', result.data.refreshToken);
return result.data;
}
return null;
} catch (err) {
return null;
}
}, []);
const getCurrentUser = useCallback(async () => {
try {
const result = await authService.getCurrentUser();
if (result.isSuccess && result.data) {
return result.data.user;
}
return null;
} catch (err) {
return null;
}
}, []);
return {
isLoading,
error,
login,
logout,
refreshToken,
getCurrentUser,
};
}

91
src/CellularManagement.WebUI/src/lib/http-client.ts

@ -1,12 +1,6 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { HttpClientConfigManager } from '@/config/http-client.config';
// 创建HTTP客户端配置接口
export interface HttpClientConfig {
baseURL: string;
timeout?: number;
headers?: Record<string, string>;
}
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
import { HttpClientConfigManager } from '@/config/http/http-client.config';
import { HttpClientConfig, OperationResult } from '@/config/types/config.types';
// 创建HTTP客户端类
export class HttpClient {
@ -42,9 +36,32 @@ export class HttpClient {
// 响应拦截器
this.instance.interceptors.response.use(
(response) => {
return response.data;
return {
...response,
data: {
successMessage: response.data.successMessage,
errorMessages: response.data.errorMessages,
data: response.data.data,
isSuccess: response.data.isSuccess
}
};
},
async (error) => {
async (error: AxiosError) => {
const config = error.config as AxiosRequestConfig & { _retryCount: number };
const maxRetries = this.configManager.getConfig().maxRetries || 3;
// 处理重试逻辑
if (config && typeof config._retryCount === 'undefined') {
config._retryCount = 0;
}
if (config && config._retryCount < maxRetries) {
config._retryCount++;
const delay = this.configManager.getConfig().retryDelay || 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return this.instance(config);
}
if (error.response) {
// 处理401错误(未授权)
if (error.response.status === 401) {
@ -52,9 +69,41 @@ export class HttpClient {
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
return Promise.reject(error.response.data);
// 处理其他HTTP错误
const errorMessage = (error.response.data as any)?.message || '请求失败';
return {
...error.response,
data: {
successMessage: null,
errorMessages: [errorMessage],
data: null,
isSuccess: false
}
};
}
return Promise.reject(error);
// 处理网络错误
if (error.request) {
return {
data: {
successMessage: null,
errorMessages: ['网络连接失败,请检查网络设置'],
data: null,
isSuccess: false
}
};
}
// 处理其他错误
return {
data: {
successMessage: null,
errorMessages: [error.message || '未知错误'],
data: null,
isSuccess: false
}
};
}
);
}
@ -73,26 +122,26 @@ export class HttpClient {
}
// 封装GET请求
public async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.get<T>(url, config);
public async get<T>(url: string, config?: AxiosRequestConfig): Promise<OperationResult<T>> {
const response = await this.instance.get<OperationResult<T>>(url, config);
return response.data;
}
// 封装POST请求
public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.post<T>(url, data, config);
public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<OperationResult<T>> {
const response = await this.instance.post<OperationResult<T>>(url, data, config);
return response.data;
}
// 封装PUT请求
public async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.put<T>(url, data, config);
public async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<OperationResult<T>> {
const response = await this.instance.put<OperationResult<T>>(url, data, config);
return response.data;
}
// 封装DELETE请求
public async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.instance.delete<T>(url, config);
public async delete<T>(url: string, config?: AxiosRequestConfig): Promise<OperationResult<T>> {
const response = await this.instance.delete<OperationResult<T>>(url, config);
return response.data;
}
}

13
src/CellularManagement.WebUI/src/pages/auth/ForbiddenPage.tsx

@ -1,5 +1,4 @@
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/Button';
export default function ForbiddenPage() {
const navigate = useNavigate();
@ -10,18 +9,18 @@ export default function ForbiddenPage() {
<h1 className="text-6xl font-bold text-gray-900">403</h1>
<p className="mt-4 text-xl text-gray-600">访</p>
<div className="mt-8">
<Button
<button
onClick={() => navigate(-1)}
variant="outline"
className="mr-4"
className="mr-4 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
</Button>
<Button
</button>
<button
onClick={() => navigate('/dashboard')}
className="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
>
</Button>
</button>
</div>
</div>
</div>

43
src/CellularManagement.WebUI/src/pages/auth/LoginPage.tsx

@ -1,45 +1,19 @@
import { useNavigate, useLocation } from 'react-router-dom';
import { LoginForm } from '@/components/auth/LoginForm';
import { DEFAULT_CREDENTIALS } from '@/constants/auth';
import { useAuth } from '@/hooks/useAuth';
import { Permission } from '@/constants/menuConfig';
import { useAuth } from '@/contexts/AuthContext';
export function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
const { login, error } = useAuth();
const handleLogin = async (username: string, password: string) => {
const isValidLogin =
(username === DEFAULT_CREDENTIALS.username || username === DEFAULT_CREDENTIALS.email) &&
password === DEFAULT_CREDENTIALS.password;
if (isValidLogin) {
// 模拟用户数据
const userData = {
id: '1',
name: username,
email: `${username}@example.com`,
permissions: [
'dashboard.view',
'users.view',
'roles.view',
'permissions.view',
'settings.view'
] as Permission[]
};
// 保存用户数据到 localStorage
localStorage.setItem('user', JSON.stringify(userData));
// 调用 login 方法更新认证状态
await login(userData);
// 获取重定向地址,如果没有则默认到 dashboard
try {
await login({ username, password });
const from = (location.state as any)?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
} else {
alert('用户名/邮箱或密码错误');
} catch (error) {
console.error('登录失败:', error);
}
};
@ -50,6 +24,11 @@ export function LoginPage() {
<h2 className="text-2xl font-bold"></h2>
</div>
<LoginForm onSubmit={handleLogin} />
{error && (
<div className="text-sm text-red-500 text-center">
{error}
</div>
)}
</div>
</div>
);

19
src/CellularManagement.WebUI/src/providers/ThemeProvider.tsx

@ -0,0 +1,19 @@
import { ReactNode, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { appSettingsState } from '@/states/appState';
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ThemeProviderProps) {
const settings = useRecoilValue(appSettingsState);
useEffect(() => {
// 应用主题
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(settings.theme);
}, [settings.theme]);
return <>{children}</>;
}

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

@ -6,7 +6,7 @@ import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
// 使用 lazy 加载组件
const LoginPage = lazy(() => import('@/pages/auth/LoginPage').then(module => ({ default: module.LoginPage })));
const DashboardHome = lazy(() => import('@/pages/dashboard/DashboardHome').then(module => ({ default: module.DashboardHome })));
const ForbiddenPage = lazy(() => import('@/pages/auth/ForbiddenPage').then(module => ({ default: module.default })));
const ForbiddenPage = lazy(() => import('@/pages/auth/ForbiddenPage'));
// 加载中的占位组件
const LoadingFallback = () => (

16
src/CellularManagement.WebUI/src/services/authService.ts

@ -4,7 +4,8 @@ import { httpClient } from '@/lib/http-client';
export const authService = {
async login(request: LoginRequest): Promise<OperationResult<LoginResponse>> {
try {
return await httpClient.post<OperationResult<LoginResponse>>('/Auth/Login', request);
const response = await httpClient.post<LoginResponse>('/Auth/Login', request);
return response;
} catch (error: any) {
return {
successMessage: null,
@ -18,9 +19,10 @@ export const authService = {
// 刷新token
async refreshToken(refreshToken: string): Promise<OperationResult<LoginResponse>> {
try {
return await httpClient.post<OperationResult<LoginResponse>>('/Auth/RefreshToken', {
const response = await httpClient.post<LoginResponse>('/Auth/RefreshToken', {
refreshToken,
});
return response;
} catch (error: any) {
return {
successMessage: null,
@ -33,18 +35,14 @@ export const authService = {
// 登出
async logout(): Promise<void> {
try {
await httpClient.post('/Auth/Logout');
} finally {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
await httpClient.post('/Auth/Logout');
},
// 获取当前用户信息
async getCurrentUser(): Promise<OperationResult<LoginResponse>> {
try {
return await httpClient.get<OperationResult<LoginResponse>>('/Auth/CurrentUser');
const response = await httpClient.get<LoginResponse>('/Auth/CurrentUser');
return response;
} catch (error: any) {
return {
successMessage: null,

42
src/CellularManagement.WebUI/src/states/appState.ts

@ -0,0 +1,42 @@
import { atom, selector } from 'recoil';
import { User } from '@/types/auth';
// 应用设置状态
export const appSettingsState = atom({
key: 'appSettingsState',
default: {
theme: 'light',
language: 'zh-CN',
sidebarCollapsed: false,
}
});
// 用户信息状态(与 AuthProvider 同步)
export const userState = atom<User | null>({
key: 'userState',
default: null
});
// 应用偏好设置
export const preferencesState = atom({
key: 'preferencesState',
default: {
notifications: true,
soundEnabled: true,
autoRefresh: true,
}
});
// 派生状态:用户设置
export const userSettingsSelector = selector({
key: 'userSettingsSelector',
get: ({get}) => {
const user = get(userState);
const settings = get(appSettingsState);
return {
...settings,
userId: user?.id,
userName: user?.name,
};
}
});

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

@ -1,26 +1,40 @@
import { Permission } from '@/constants/menuConfig';
export interface User {
id: string;
userName: string;
name: string;
email: string;
phoneNumber: string;
roles: string[];
permissions: Permission[];
}
export interface LoginRequest {
username: string;
password: string;
}
export interface LoginResponse {
accessToken: string;
refreshToken: string;
expiresAt: string;
user: User;
}
export interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
userPermissions: Permission[];
}
export interface AuthContextType extends AuthState {
login: (request: LoginRequest) => Promise<void>;
logout: () => Promise<void>;
refreshToken: () => Promise<void>;
}
export interface OperationResult<T> {
successMessage: string | null;
errorMessages: string[] | null;
data: T | null;
isSuccess: boolean;
}
export interface LoginRequest {
username: string;
password: string;
}

32
src/CellularManagement.WebUI/src/types/env.d.ts

@ -1,32 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
// API配置
readonly VITE_API_BASE_URL: string;
readonly VITE_API_TIMEOUT: string;
readonly VITE_API_VERSION: string;
// 应用配置
readonly VITE_APP_TITLE: string;
readonly VITE_APP_DESCRIPTION: string;
// 认证配置
readonly VITE_AUTH_TOKEN_KEY: string;
readonly VITE_AUTH_REFRESH_TOKEN_KEY: string;
readonly VITE_AUTH_TOKEN_EXPIRES_KEY: string;
// 其他配置
readonly VITE_ENABLE_MOCK: string;
readonly VITE_MOCK_DELAY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// 在代码中使用环境变量
import { envConfig } from '@/config/env';
console.log(envConfig.API_BASE_URL); // 从环境变量获取
console.log(envConfig.API_TIMEOUT); // 自动转换为数字
console.log(envConfig.API_VERSION); // 从环境变量获取

59
src/CellularManagement.WebUI/test.sql

@ -0,0 +1,59 @@
-- 用户表
CREATE TABLE Users (
Id INT IDENTITY(1,1) PRIMARY KEY,
Username NVARCHAR(50) NOT NULL UNIQUE,
Email NVARCHAR(100) NOT NULL UNIQUE,
PasswordHash NVARCHAR(MAX) NOT NULL,
FirstName NVARCHAR(50),
LastName NVARCHAR(50),
IsActive BIT DEFAULT 1,
CreatedAt DATETIME DEFAULT GETDATE(),
UpdatedAt DATETIME DEFAULT GETDATE()
);
-- 角色表
CREATE TABLE Roles (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(50) NOT NULL UNIQUE,
Description NVARCHAR(200),
CreatedAt DATETIME DEFAULT GETDATE(),
UpdatedAt DATETIME DEFAULT GETDATE()
);
-- 权限表
CREATE TABLE Permissions (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(50) NOT NULL UNIQUE,
Description NVARCHAR(200),
CreatedAt DATETIME DEFAULT GETDATE()
);
-- 用户角色关联表
CREATE TABLE UserRoles (
UserId INT NOT NULL,
RoleId INT NOT NULL,
CreatedAt DATETIME DEFAULT GETDATE(),
PRIMARY KEY (UserId, RoleId),
FOREIGN KEY (UserId) REFERENCES Users(Id),
FOREIGN KEY (RoleId) REFERENCES Roles(Id)
);
-- 角色权限关联表
CREATE TABLE RolePermissions (
RoleId INT NOT NULL,
PermissionId INT NOT NULL,
CreatedAt DATETIME DEFAULT GETDATE(),
PRIMARY KEY (RoleId, PermissionId),
FOREIGN KEY (RoleId) REFERENCES Roles(Id),
FOREIGN KEY (PermissionId) REFERENCES Permissions(Id)
);
-- 刷新令牌表
CREATE TABLE RefreshTokens (
Id INT IDENTITY(1,1) PRIMARY KEY,
UserId INT NOT NULL,
Token NVARCHAR(MAX) NOT NULL,
ExpiresAt DATETIME NOT NULL,
CreatedAt DATETIME DEFAULT GETDATE(),
FOREIGN KEY (UserId) REFERENCES Users(Id)
);
Loading…
Cancel
Save