领域驱动设计实战案例(三):实现经销商上下文领域层之POCO模型、 仓储与领域逻辑、经销商登录仓储与逻辑...

DDD实战进阶第一波:开发一般业务的大健康行业直销系统

一、实现经销商上下文领域层之POCO模型

从这篇文章开始,我们开始介绍大健康行业直销系统领域层的实现。

先简单讲下业务方面的需求:直销系统会有一个顶级的经销商,经销商的基本信息中包括经销商的名字、联系人(因为在平台购买产品后,会寄送给联系人)、总的电子币(电子币是由经销商支付产生,购买产品后会扣减电子币)、总的奖金币(系统周期性根据经销商购买的东西来确定奖金币,奖金币可以购买东西,也可以提现)、总PV(经销商购买时,会根据购买产品的PV进行累加)、卡的类型(根据经销商初次的电子币确定卡的类型)、子经销商个数(子经销商的注册由父经销商进行,父经销商的直接子经销商不超过2个)、级别(根据周期消费总额确定经销商级别);另外经销商有个层级结构,最后系统当然还要对应经销商的登录信息,默认系统会有个登录密码;经销商在注册子经销商时,会从自己扣除一部分电子币附加到子经销商上。

从整个需求的理解并通过对DDD理解来看,我们会有两个聚合,分别是经销商聚合(包括经销商、联系人、层级)和登录聚合。

clipboard.png

1.经销商聚合根:

public partial class Dealers:IAggregationRoot

{
    public Dealers() { }

    public string Code { get; set; }
    [Key]
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Tel { get; set; }
    public decimal TotalEleMoney { get; set; }
    public decimal JiangJInMoney { get; set; }
    public decimal TotalPV { get; set; }
    public CardType CardType { get; set; }
    public Level Level { get; set; }
    public int SubCount { get; set; }
    public List<Contact> Contacts { get; set; }
    public DealerTree DealerTree { get; set; }
}

public enum CardType : int
{
    普通会员=1,
    银卡会员=2,
    金卡会员=3
}
public enum Level : int
{
    片区经理=1,
    省区经理=2,
    大区经理=3,
    董事=4
}

2.联系人值对象:

public partial class Contact : IValueObject

{
    public Contact() { }
    public Guid Id { get; set; }
    public string ContactName { get; set; }
    public string ContactTel { get; set; }
    public string Province { get; set; }
    public string City { get; set; }
    public string Zero { get; set; }
    public string Street { get; set; }
    public IsDefaultContact IsDefault { get; set; }
}
public enum IsDefaultContact : int
{
    默认=1,
    非默认=2
}

3.层次结构值对象:

public partial class DealerTree : IValueObject

{
    public DealerTree() { }
    public Guid Id { get; set; }
    public Guid DealerId { get; set; }
    public Guid? ParentDealerId { get; set; }
    public int Layer { get; set; }
}

从经销商聚合大家可以看到,在创建一个经销商时,除了有经销商的基本信息外,还必须同时创建联系人与层次结构,这样一个经销商才是完整的,而且经销商也引用到了联系人与层次结构。

4.登录聚合根:

public partial class Login : IAggregationRoot

{
    public Login() { }
    //代表登录的电话号码
    public string Code { get; set; }
    public string Password { get; set; }
    public Guid DealerId { get; set; }
    [Key]
    public Guid Id { get ; set ; }
}

5.处理经销商界限上下文与数据访问上下文的映射

关于如何讲经销商界限上下文映射到数据访问上下文,请参考产品上下文的相关实现,这里就不再累述了。

下一篇文章开始讲经销商上下文仓储的实现,因为在注册子经销商的领域逻辑中,会通过仓储去判断当前经销商是否子经销商个数超过2个。

二、实现经销商上下文仓储与领域逻辑

上篇文章主要讲述了经销商上下文的需求与POCO对象,这篇文章主要讲述该界限上下文的仓储与领域逻辑的实现。

关于界限上下文与EF Core数据访问上下文参考产品上下文相应的实现,这里不再累述。因为在经销商上下文中有两个聚合,一个是经销商聚合,一个是登录聚合,所以我们需要实现两个仓储接口:

1.经销商仓储接口定义:

public interface IDealerRepository

{
    void CreateDealer<T>(T dealer) where T : class, IAggregationRoot;
    //获取上级经销商(当前代注册经销商)的层次结构
    int GetParentDealerLayer(Guid dealerid);
    //将上级经销商(代注册经销商)的子个数加一
    void AddParentSubCount(Guid? parentdealerid);
    //减去父进销商的电子币(用于注册和下单时,扣减经销商的电子币)
    void SubParentEleMoney(Guid parentdealerid, decimal subelemoney);
    //下订单时,增加经销商的PV
    void AddDealerPV(Guid dealerid, decimal orderpv);

}

2.登录仓储接口定义:

public interface ILoginRepository

{
    void CreateLogin<T>(T login) where T : class, IAggregationRoot;
    Guid UserLogin(string tel, string password);
}

3.具体对应的仓储实现在仓储实现的项目中自己实现,主要通过EF Core完成数据库的访问与操作

4.经销商聚合中联系人对象的领域逻辑实现:

public partial class Contact

{
    public Contact CreateContact(Guid dealerid,string name,string tel,string province,string city,
        string zero,string street,int isdefault)
    {
        this.Id = Guid.NewGuid();
        this.DealerId = dealerid;
        this.ContactName = name;
        this.ContactTel = tel;
        this.Province = province;
        this.City = city;
        this.Zero = zero;
        this.Street = street;
        switch (isdefault)
        {
            case 1:this.IsDefault = IsDefaultContact.默认;
                break;
            case 2:this.IsDefault = IsDefaultContact.非默认;
                break;
        }
        return this;

    }
}

5.经销商聚合中经销商层次结构对象的领域逻辑实现:

public partial class DealerTree

{
    private readonly IDealerRepository idealerrepository;
    public DealerTree(IDealerRepository idealerrepository)
    {
        this.idealerrepository = idealerrepository;
    }
    public DealerTree CreateDealerTree(Guid? parentdealerid,Guid dealerid)
    {
        this.Id = Guid.NewGuid();
        this.DealerId = dealerid;
        this.ParentDealerId = parentdealerid;
        this.Layer = parentdealerid == null ? 1 : idealerrepository.GetParentDealerLayer(Guid.Parse(parentdealerid.ToString())) + 1;
        return this;
    }
}

6.经销商聚合中经销商对象的领域逻辑实现:

public partial class Dealers

{
    private readonly IDealerRepository idealerrepository;
    public Dealers(IDealerRepository idealerrepository)
    {
        this.idealerrepository = idealerrepository;
    }
    public Dealers RegisterDealer(Guid id,string name,string tel,decimal telmoney,List<Contact>
        contacts,Guid? parentid)
    {
        this.Id = id;
        this.Code = "Code " + name;
        this.Name = name;
        this.Tel = tel;
        this.TotalEleMoney = telmoney;
        if (telmoney < 2000)
        {
            this.CardType = CardType.普通会员;
        }
        else if (telmoney >= 2000 && telmoney < 4000)
        {
            this.CardType = CardType.银卡会员;
        }
        else
        {
            this.CardType = CardType.金卡会员;
        }
        this.SubCount = 0;
        this.TotalPV = 0;
        this.JiangJInMoney = 0;
        this.Contacts = contacts;
        this.DealerTree = new DealerTree(idealerrepository).CreateDealerTree(parentid, id);
        return this;
    }
}

7.登录聚合中登录对象的领域逻辑实现:

public partial class Login

{
    public Login CreateLogin(string code,Guid dealerid)
    {
        this.Id = Guid.NewGuid();
        //手机号
        this.Code = code;
        //默认初始密码
        this.Password=MD5Encrption.GetMd5Str("111111");
        this.DealerId = dealerid;
        return this;
    }
}

这样,我们就完成了基本数据库的访问、操作和相关领域逻辑的实现。

三、实现经销商登录仓储与逻辑

上一篇文章主要讲了经销商注册的仓储和领域逻辑的实现,我们先把应用服务协调完成经销商注册这部分暂停一下,后面文章统一讲。这篇文章主要讲讲经销商登录的仓储和相关逻辑的实现。

在现代应用程序前后端分离的实现中,通常不是将用户登录的信息存储在服务器端Session,因为会存在服务器Session无法传递的情况,也存在WebApi调用时无法通过Authorize Attribute判断用户是否已经登录并获取用户身份信息的问题。所以现代应用程序都是由服务器后端返回Token给客户端,客户端将Token存储在客户端Session中,客户端在请求后端接口时,带上Token,服务器端就能够识别客户端是否经过身份验证,而且可以直接拿到客户端的身份。

要实现经销商的登录,主要由以下几个步骤组成

1.实现经销商登录时信息查询的仓储。
2.在应用服务中,单独建立一个查询文件夹放置经销商登录的查询逻辑。
3.在登录WebApi中,调用应用服务的查询逻辑并分发Token。

1.实现经销商登录时信息查询的仓储:

public interface ILoginRepository

{
        Guid UserLogin(string tel, string password);
}

public class LoginEFCoreRepository : ILoginRepository

{
    private readonly DbContext context;
    public LoginEFCoreRepository(DbContext context)
    {
        this.context = context;
    }
    public Guid UserLogin(string tel, string password)
    {
        var dealercontext = this.context as DealerEFCoreContext;
        var enpassword = MD5Encrption.GetMd5Str(password);
        var logindealer=
            dealercontext.Login.Where(p => p.Code == tel && p.Password == enpassword).FirstOrDefault();
        if (logindealer != null)
        {
            return logindealer.DealerId;
        }
        return Guid.Empty;
    }

      }

2.应用服务中调用仓储完成用户登录的查询

public class UserLoginQuery:BaseAppSrv

{
    private readonly IRepository irepository;
    private readonly ILoginRepository iloginrepository;
    public UserLoginQuery(IRepository irepository, ILoginRepository iloginrepository)
    {
        this.iloginrepository = iloginrepository;
        this.irepository = irepository;
    }
    public Guid Login(UserLoginDTO userlogindto)
    {
        try
        {
            using (irepository)
            {
                return iloginrepository.UserLogin(userlogindto.Telphone, userlogindto.Password);
            }
        }
        catch(Exception error)
        {
            throw error;
        }
    }
}

3.在登录WebApi中调用应用服务,并分发令牌

[AllowAnonymous]

    [HttpPost]
    [Route("UserLogin")]
    public ResultEntity<UserLoginResultDTO> UserLogin([FromBody] UserLoginDTO userlogindto)
    {
        var result = new ResultEntity<UserLoginResultDTO>();
        var idealercontext = servicelocator.GetService<IDealerContext>();
        var irepository =
            servicelocator.GetService<IRepository>(new ParameterOverrides { { "context", idealercontext } });
        var iloginrepository = servicelocator.GetService<ILoginRepository>(new ParameterOverrides { { "context", idealercontext } });
        UserLoginQuery userloginquery = new UserLoginQuery(irepository, iloginrepository);
        try
        {
            var dealerid = userloginquery.Login(userlogindto);
            if (dealerid != Guid.Empty)
            {
                var token = new JwtTokenBuilder()
                    .AddSecurityKey(JwtSecurityKey.Create("msshcjsecretmsshcjsecret"))
                    .AddSubject(userlogindto.Telphone)
                    .AddIssuer("DDD1ZXSystem")
                    .AddAudience("DDD1ZXSystem")
                    .AddClaim("role", "NormalUser")                        
                    .AddExpiry(600)
                    .Build();

                var userloginresultdto = new UserLoginResultDTO();
                userloginresultdto.Tel = userlogindto.Telphone;
                userloginresultdto.DealerId = dealerid;
                userloginresultdto.Token = token.Value;

                result.IsSuccess = true;
                result.Data = userloginresultdto;
                result.Msg = "登录成功!";
            }
            else
            {
                result.ErrorCode = 300;
                result.Msg = "登录失败!";
            }

        }
        catch (Exception error)
        {
            result.ErrorCode = 200;
            result.Msg = error.Message;
        }
        return result;
    }

这里的UserLoginDTO定义如下:

public class UserLoginDTO

{
    public string Telphone { get; set; }
    public string Password { get; set; }
}

这里的UserLoginResultDTO定义如下:

public class UserLoginResultDTO

{
    public string Tel { get; set; }
    public Guid DealerId { get; set; }
    public string Token { get; set; }
}

这里的JwtTokenBuilder定义如下:

public class JwtTokenBuilder

{
    private SecurityKey securityKey = null;
    private string subject = "";
    private string issuer = "";
    private string audience = "";
    private Dictionary<string, string> claims = new Dictionary<string, string>();
    private int expiryInMinutes = 5;

    public JwtTokenBuilder AddSecurityKey(SecurityKey securityKey)
    {
        this.securityKey = securityKey;
        return this;
    }
    public JwtTokenBuilder AddSubject(string subject)
    {
        this.subject = subject;
        return this;
    }
    public JwtTokenBuilder AddIssuer(string issuer)
    {
        this.issuer = issuer;
        return this;
    }
    public JwtTokenBuilder AddAudience(string audience)
    {
        this.audience = audience;
        return this;
    }
    public JwtTokenBuilder AddClaim(string type,string value)
    {
        this.claims.Add(type, value);
        return this;
    }
    public JwtTokenBuilder AddExpiry(int expiryInMinutes)
    {
        this.expiryInMinutes = expiryInMinutes;
        return this;
    }

    public JwtToken Build()
    {
        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub,this.subject),
            new Claim(JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString())

        }.Union(this.claims.Select(item => new Claim(item.Key, item.Value)));

        var token = new JwtSecurityToken(issuer: this.issuer, audience: this.audience, claims: claims,
            expires: DateTime.UtcNow.AddMinutes(this.expiryInMinutes), signingCredentials:
            new SigningCredentials(this.securityKey, SecurityAlgorithms.HmacSha256));
        return new JwtToken(token);
    }
}

这里的BearerUserInfo定义如下:

public class BearerUserInfo:Controller

{
    public string GetUserName()
    {
        var principal = HttpContext.User as ClaimsPrincipal;
        if (principal != null)
        {
            foreach(var claim in principal.Claims)
            {
                if (claim.Subject != null)
                {
                    var subjectclaims = claim.Subject.Claims as List<Claim>;
                    return subjectclaims[0].Value;
                }
            }
        }
        return null;
    }
}

这里的JwtSecurityKey定义如下:

public static class JwtSecurityKey

{
    public static SymmetricSecurityKey Create(string secret)
    {
        return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
    }
}

这里的JwtToken定义如下:

public class JwtToken

{
    private JwtSecurityToken token;
    public JwtToken(JwtSecurityToken token)
    {
        this.token = token;
    }
    public DateTime ValidTo => token.ValidTo;
    public string Value => new JwtSecurityTokenHandler().WriteToken(this.token);
}

以上采用了.net core中关于OWIN的使用,具体不清楚的属性和方法,可以参考OWIN中.net core的实现标准,这里就不累述了,具体可以参考微信公众号中的视频讲解。

QQ讨论群:309287205
DDD实战进阶视频请关注微信公众号:msshcj

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值