DDD实战进阶第一波:开发一般业务的大健康行业直销系统
一、实现经销商上下文领域层之POCO模型
从这篇文章开始,我们开始介绍大健康行业直销系统领域层的实现。
先简单讲下业务方面的需求:直销系统会有一个顶级的经销商,经销商的基本信息中包括经销商的名字、联系人(因为在平台购买产品后,会寄送给联系人)、总的电子币(电子币是由经销商支付产生,购买产品后会扣减电子币)、总的奖金币(系统周期性根据经销商购买的东西来确定奖金币,奖金币可以购买东西,也可以提现)、总PV(经销商购买时,会根据购买产品的PV进行累加)、卡的类型(根据经销商初次的电子币确定卡的类型)、子经销商个数(子经销商的注册由父经销商进行,父经销商的直接子经销商不超过2个)、级别(根据周期消费总额确定经销商级别);另外经销商有个层级结构,最后系统当然还要对应经销商的登录信息,默认系统会有个登录密码;经销商在注册子经销商时,会从自己扣除一部分电子币附加到子经销商上。
从整个需求的理解并通过对DDD理解来看,我们会有两个聚合,分别是经销商聚合(包括经销商、联系人、层级)和登录聚合。
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