【ABP】项目示例(2)——聚合根和实体

聚合根和实体

在上一章节中,已经完成了项目搭建的前置准备,在这一章节中,实现领域层的聚合根和实体

创建名称为General.Backend.Domain的标准类库,分别新建名称为Entities、Services、IRepositories和Specifications的文件夹,用于存放实体和聚合根、领域服务、仓储接口和规约。

本项目使用ABP相关的Nuget包的版本为8.3.0,为保持版本一致,后续其他ABP相关的Nuget包都使用该版本。

在程序包管理控制台选中General.Backend.Domain,执行以下命令安装ABP领域相关的Nuget包。

Install-Package Volo.Abp.Ddd.Domain -v 8.3.0

新建名称为GeneralDomainModule的领域模块类

public class GeneralDomainModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        
    }
}

创建名称为General.Backend.Domain.Shared的标准类库,分别新建名称为Enums和Consts的文件夹,用于存放枚举和常量。

在程序包管理控制台选中General.Backend.Domain.Shared,执行以下命令安装ABP领域共享相关的Nuget包。

Install-Package Volo.Abp.Ddd.Domain.Shared -v 8.3.0

新建名称为GeneralDomainSharedModule的领域共享模块类

public class GeneralDomainSharedModule : AbpModule
{

}

General.Backend.Domain添加项目引用General.Backend.Domain.Shared

用户聚合

在Enums文件夹下新建名称为FrozenStatus的用户冻结状态枚举

/// <summary>
/// 冻结状态
/// </summary>
public enum FrozenStatus
{
    /// <summary>
    /// 未冻结
    /// </summary>
    UnFrozen = 1,

    /// <summary>
    /// 已冻结
    /// </summary>
    Frozen = 2
}

在Consts文件夹下新建名称为UserConsts的用户常量类,定义用户领域使用到的常量,用于数据库表配置和常规校验

public static class UserConsts
{
    public const string UserTableName = "user";

    public const string UserTableComment = "用户表";

    public const string UserRoleTableName = "user_role";

    public const string UserRoleTableComment = "用户角色表";

    public const string AdminAccount = "admin";

    public const string AdminName = "Admin";

    public const int MaxLoginErrorCount = 5;

    public const int MinAccountLength = 2;

    public const int MaxAccountLength = 32;

    public const int MinPasswordLength = 6;

    public const int MaxPasswordLength = 32;

    public const int MinNameLength = 1;

    public const int MaxNameLength = 32;

    public const int MaxContactLength = 64;

    public const int MaxAddressLength = 64;
}

在Entities文件夹下新建名称为User的用户聚合根类和UserRole的用户角色实体类,业务逻辑为一个用户拥有一个或者多个角色,用户的角色需要通过用户聚合来管理

/// <summary>
/// 用户
/// </summary>
public class User : FullAuditedAggregateRoot<Guid>
{
    /// <summary>
    /// 账号
    /// </summary>
    public virtual string Account { get; private set; } = string.Empty;

    /// <summary>
    /// 密码
    /// </summary>
    public virtual string Password { get; private set; } = string.Empty;

    /// <summary>
    /// 用户名称
    /// </summary>
    public virtual string Name { get; private set; } = string.Empty;

    /// <summary>
    /// 联系方式
    /// </summary>
    public virtual string? Contact { get; set; }

    /// <summary>
    /// 地址
    /// </summary>
    public virtual string? Address { get; set; }

    /// <summary>
    /// 登录错误次数
    /// </summary>
    public virtual byte LoginErrorCount { get; private set; }

    /// <summary>
    /// 是否冻结(1:未冻结,2:已冻结)
    /// </summary>
    public virtual FrozenStatus IsFrozen { get; private set; }

    /// <summary>
    /// 用户关联角色列表
    /// </summary>
    public virtual ICollection<UserRole> UserRoles { get; private set; } = [];

    protected User()
    {
        
    }

    public User(
        Guid id,
        [NotNull] string account,
        [NotNull] string password,
        [NotNull] string name,
        [CanBeNull] string? contact = null,
        [CanBeNull] string? address = null)
    {
        Id = id;
        SetAccount(account);
        SetPassword(password);
        SetName(name);
        IsFrozen = FrozenStatus.UnFrozen;
        Contact = Check.Length(contact, nameof(contact), UserConsts.MaxContactLength);
        Address = Check.Length(address, nameof(address), UserConsts.MaxAddressLength);
    }

    private void SetAccount([NotNull] string account)
    {
        Account = Check.Length(account, nameof(account), UserConsts.MaxAccountLength, UserConsts.MinAccountLength)!;
    }

    internal virtual void SetPassword([NotNull] string password)
    {
        Password = Check.Length(password, nameof(password), UserConsts.MaxPasswordLength, UserConsts.MinPasswordLength)!;
    }

    public virtual void SetName([NotNull] string name)
    {
        Name = Check.Length(name, nameof(name), UserConsts.MaxNameLength, UserConsts.MinNameLength)!;
    }

    internal virtual bool CheckPassword([NotNull] string password)
    {
        Check.NotNullOrEmpty(password, nameof(password));
        if (Password != password) 
        {
            TryFrozen();
            return false;
        }
        UnFrozen();
        return true;
    }

    public virtual void UnFrozen()
    {
        LoginErrorCount = 0;
        IsFrozen = FrozenStatus.UnFrozen;
    }

    private bool TryFrozen()
    {
        var isSuccess = false;
        if (Account != UserConsts.AdminAccount)
        {
            LoginErrorCount += 1;
            if (LoginErrorCount >= UserConsts.MaxLoginErrorCount)
            {
                IsFrozen = FrozenStatus.Frozen;
                isSuccess = true;
            }
        }
        return isSuccess;
    }

    public virtual void SetRoles(ICollection<UserRole> userRoles)
    {
        UserRoles = userRoles;
    }
}

/// <summary>
/// 用户角色
/// </summary>
public class UserRole : Entity<Guid>, IHasCreationTime
{
    /// <summary>
    /// 用户Id
    /// </summary>
    public virtual Guid UserId { get; private set; }

    /// <summary>
    /// 角色Id
    /// </summary>
    public virtual Guid RoleId { get; private set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public virtual DateTime CreationTime { get; private set; }

    protected UserRole()
    {

    }

    internal UserRole(
        Guid id,
        Guid userId,
        Guid roleId)
    {
        Id = id;
        UserId = userId;
        RoleId = roleId;
    }
}

角色聚合

在Entities文件夹下新建名称为Role的角色聚合根类和RoleMenus的角色菜单实体类,业务逻辑为一个角色拥有一个或者多个菜单,用户拥有的菜单需要通过角色聚合来管理

/// <summary>
/// 角色
/// </summary>
public class Role : FullAuditedAggregateRoot<Guid>
{
    /// <summary>
    /// 编码
    /// </summary>
    public virtual string Code { get; private set; } = string.Empty;

    /// <summary>
    /// 名称
    /// </summary>
    public virtual string Name { get; private set; } = string.Empty;

    /// <summary>
    /// 描述
    /// </summary>
    public virtual string? Remark { get; set; }

    /// <summary>
    /// 角色关联菜单列表
    /// </summary>
    public virtual ICollection<RoleMenu> RoleMenus { get; private set; } = [];


    protected Role()
    {

    }

    public Role(
        Guid id,
        [NotNull] string code,
        [NotNull] string name,
        [CanBeNull] string? remark = null)
    {
        Id = id;
        Code = Check.Length(code, nameof(code), RoleConsts.MaxCodeLength, RoleConsts.MinCodeLength)!;
        SetName(name);
        Remark = Check.Length(remark, nameof(remark), RoleConsts.MaxRemarkLength);
    }

    internal virtual void SetName(
        [NotNull] string name)
    {
        Name = Check.Length(name, nameof(name), UserConsts.MaxNameLength, UserConsts.MinNameLength)!;
    }

    public virtual void SetMenus(ICollection<RoleMenu> roleMenus)
    {
        RoleMenus = roleMenus;
    }
}

/// <summary>
/// 角色菜单
/// </summary>
public class RoleMenu : Entity<Guid>, IHasCreationTime
{
    /// <summary>
    /// 角色Id
    /// </summary>
    public virtual Guid RoleId { get; private set; }

    /// <summary>
    /// 菜单编码
    /// </summary>
    public virtual string MenuCode { get; private set; } = string.Empty;

    /// <summary>
    /// 创建时间
    /// </summary>
    public virtual DateTime CreationTime { get; private set; }

    protected RoleMenu()
    {

    }

    internal RoleMenu(
        Guid id,
        Guid roleId,
        [NotNull] string menuCode)
    {
        Id = id;
        RoleId = roleId;
        MenuCode = Check.Length(menuCode, nameof(menuCode), RoleConsts.MaxMenuCodeLength, RoleConsts.MinMenuCodeLength)!;
    }
}

同样地在Consts文件夹下新建名称为RoleConsts的角色常量类,定义角色领域使用到的常量,用于数据库表配置和常规校验

public class RoleConsts
{
    public const string RoleTableName = "role";

    public const string RoleTableComment = "角色表";

    public const string RoleMenuTableName = "role_menu";

    public const string RoleMenuTableComment = "角色菜单表";

    public const string AdminRoleCode = "Admin";

    public const string AdminRoleName = "Admin";

    public const int MaxCodeLength = 32;

    public const int MinCodeLength = 1;

    public const int MaxNameLength = 32;

    public const int MinNameLength = 1;

    public const int MaxMenuCodeLength = 64;

    public const int MinMenuCodeLength = 1;

    public const int MaxRemarkLength = 255;
}

菜单聚合

在Entities文件夹下新建名称为Menu的菜单聚合根类

/// <summary>
/// 菜单
/// </summary>
public class Menu : BasicAggregateRoot<Guid>, IHasCreationTime
{
    /// <summary>
    /// 编码
    /// </summary>
    public virtual string Code { get; private set; } = string.Empty;

    /// <summary>
    /// 父编码
    /// </summary>
    public virtual string ParentCode { get; private set; } = string.Empty;

    /// <summary>
    /// 名称
    /// </summary>
    public virtual string Name { get; private set; } = string.Empty;

    /// <summary>
    /// 类型
    /// </summary>
    public virtual string Type { get; private set; } = string.Empty;

    /// <summary>
    /// 层级
    /// </summary>
    public virtual int Level { get; private set; }

    /// <summary>
    /// 图标
    /// </summary>
    public virtual string? Icon { get; private set; }

    /// <summary>
    /// 路由地址
    /// </summary>
    public virtual string? UrlAddress { get; private set; }

    /// <summary>
    /// 组件地址
    /// </summary>
    public virtual string? ComponentAddress { get; private set; }

    /// <summary>
    /// 排序
    /// </summary>
    public virtual int Sort { get; private set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    public virtual DateTime CreationTime { get; private set; }

    /// <summary>
    /// 子菜单
    /// </summary>
    public List<Menu> SubMenu { get; private set; } = [];

    protected Menu()
    {

    }

    public Menu(
        [NotNull] string code,
        [NotNull] string parentCode,
        [NotNull] string name,
        [NotNull] string type,
        int level,
        int sort,
        [CanBeNull] string? icon = null,
        [CanBeNull] string? urlAddress = null,
        [CanBeNull] string? componentAddress = null)
    {
        Code = Check.Length(code, nameof(code), MenuConsts.MaxCodeLength, MenuConsts.MinCodeLength)!;
        ParentCode = Check.Length(parentCode, nameof(parentCode), MenuConsts.MaxParentCodeLength, MenuConsts.MinParentCodeLength)!;
        Name = Check.Length(name, nameof(name), MenuConsts.MaxNameLength, MenuConsts.MinNameLength)!;
        Type = Check.Length(type, nameof(type), MenuConsts.MaxTypeLength, MenuConsts.MinTypeLength)!;
        Level = level;
        Sort = sort;
        Icon = Check.Length(icon, nameof(icon), MenuConsts.MaxIconLength);
        UrlAddress = Check.Length(urlAddress, nameof(urlAddress), MenuConsts.MaxUrlAddressLength);
        ComponentAddress = Check.Length(componentAddress, nameof(componentAddress), MenuConsts.MaxComponentAddressLength);
    }

    private Menu(
        [NotNull] string code)
    {
        Code = Check.Length(code, nameof(code), MenuConsts.MaxCodeLength, MenuConsts.MinCodeLength)!;
    }

    internal static Menu BuildRoot()
    {
        return new Menu(
            MenuConsts.MenuRootCode);
    }

    internal static Menu BuildCatalog(
        [NotNull] string code,
        [NotNull] string name,
        [NotNull] string icon,
        [NotNull] string urlAddress,
        int sort)
    {
        return new Menu(
            code,
            MenuConsts.MenuRootCode,
            name,
            MenuConsts.CatalogTypeName,
            1,
            sort,
            icon,
            urlAddress);
    }

    internal static Menu BuildMenu(
        [NotNull] string code,
        [NotNull] string parentCode,
        [NotNull] string name,
        [NotNull] string icon,
        [NotNull] string urlAddress,
        [NotNull] string componentAddress,
        int sort)
    {
        return new Menu(
            code,
            parentCode,
            name,
            MenuConsts.MenuTypeName,
            2,
            sort,
            icon,
            urlAddress,
            componentAddress);
    }

    internal static Menu BuildFunc(
        [NotNull] string code,
        [NotNull] string parentCode,
        [NotNull] string name,
        int sort)
    {
        return new Menu(
            code,
            parentCode,
            name,
            MenuConsts.FuncTypeName,
            3,
            sort);
    }

    public void SetSubMenu(
        [NotNull] List<Menu> subMenus)
    {
        Check.NotNull(subMenus, nameof(subMenus));
        foreach (var parentCode in subMenus.Select(menu => menu.ParentCode))
        {
            Check.Equals(Code, parentCode);
        }
        SubMenu = subMenus;
    }
}

同样地在Consts文件夹下新建名称为MenuConsts的菜单常量类

public class MenuConsts
{
    public const string MenuTableName = "menu";

    public const string MenuTableComment = "菜单表";

    public const string MenuRootCode = "root";

    public const string CatalogTypeName = "C";

    public const string MenuTypeName = "M";

    public const string FuncTypeName = "F";

    public const int MaxCodeLength = 64;

    public const int MinCodeLength = 1;

    public const int MaxParentCodeLength = 64;

    public const int MinParentCodeLength = 1;

    public const int MaxNameLength = 32;

    public const int MinNameLength = 1;

    public const int MaxTypeLength = 8;

    public const int MinTypeLength = 1;

    public const int MaxIconLength = 32;

    public const int MaxUrlAddressLength = 64;

    public const int MaxComponentAddressLength = 64;
}

上面已经对用户、角色和菜单领域创建了相应的聚合根,相关的业务逻辑以聚合根和实体中方法来实现

解决方案的目录结构现如下

在下一章节中,使用仓储作为领域模型和数据模型的桥梁,将领域模型持久化到数据库中的数据模型中