56 changed files with 2860 additions and 133 deletions
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
|
|||
namespace CellularManagement.Domain.Services; |
|||
|
|||
/// <summary>
|
|||
/// 分布式锁句柄接口
|
|||
/// </summary>
|
|||
public interface ILockHandle : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// 是否成功获取锁
|
|||
/// </summary>
|
|||
bool IsAcquired { get; } |
|||
} |
|||
@ -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); |
|||
|
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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
|
|||
} |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
@ -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
|
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -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
|
|||
} |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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 } |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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> |
|||
); |
|||
} |
|||
@ -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 || '删除用户失败' |
|||
}; |
|||
} |
|||
} |
|||
}; |
|||
@ -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…
Reference in new issue