fengling-member-service/src/Fengling.Member.Domain/Aggregates/Users/Member.cs
movingsam eb1d4ac4f7 refactor: apply CleanDDD strongly typed ID and add Deleted/RowVersion
- Convert CampaignId to partial record implementing IGuidStronglyTypedId
- Add PointsAccountId as IInt64StronglyTypedId with Snowflake ID generation
- Add Deleted and RowVersion to MemberEntity and PointsAccount
- Update PointsAccountEntityTypeConfiguration to use SnowFlakeValueGenerator

BREAKING CHANGE: PointsAccount now uses PointsAccountId (long) instead of plain long
2026-02-16 22:03:04 -08:00

159 lines
4.9 KiB
C#

using System.Text.RegularExpressions;
using Fengling.Member.Domain.Events.Member;
using NetCorePal.Extensions.Domain;
namespace Fengling.Member.Domain.Aggregates.Users;
public partial record MemberId : IGuidStronglyTypedId
{
public static MemberId New() => new MemberId(Guid.NewGuid());
public Guid Value => this;
public static implicit operator Guid(MemberId id) => id.Value;
public static implicit operator MemberId(Guid value) => new MemberId(value);
}
public class MemberEntity : Entity<MemberId>, IAggregateRoot
{
public long TenantId { get; private set; }
public string? PhoneNumber { get; private set; }
public string? OpenId { get; private set; }
public string? UnionId { get; private set; }
public MemberStatus Status { get; private set; } = MemberStatus.Active;
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; private set; }
public int Version { get; private set; } = 1;
public Deleted Deleted { get; private set; } = new();
public RowVersion RowVersion { get; private set; } = new(0);
private readonly List<MemberTag> _tags = new();
public IReadOnlyCollection<MemberTag> Tags => _tags.AsReadOnly();
private readonly List<OAuthAuthorization> _oauthAuthorizations = new();
public IReadOnlyCollection<OAuthAuthorization> OAuthAuthorizations => _oauthAuthorizations.AsReadOnly();
private MemberEntity()
{
}
public static MemberEntity Create(long tenantId, string? phoneNumber = null)
{
if (tenantId <= 0)
throw new ArgumentException("租户ID必须大于0", nameof(tenantId));
var member = new MemberEntity
{
TenantId = tenantId,
PhoneNumber = phoneNumber,
Status = MemberStatus.Active,
CreatedAt = DateTime.UtcNow
};
member.AddDomainEvent(new MemberRegisteredEvent(member.Id, member.TenantId, phoneNumber, member.CreatedAt));
return member;
}
public void BindPhoneNumber(string phoneNumber)
{
if (string.IsNullOrWhiteSpace(phoneNumber))
throw new ArgumentException("手机号不能为空", nameof(phoneNumber));
if (!Regex.IsMatch(phoneNumber, @"^1[3-9]\d{9}$"))
throw new ArgumentException("手机号格式不正确", nameof(phoneNumber));
PhoneNumber = phoneNumber;
UpdatedAt = DateTime.UtcNow;
}
public void BindOAuth(OAuthProvider provider, string openId, string? unionId = null)
{
if (string.IsNullOrWhiteSpace(openId))
throw new ArgumentException("OpenId不能为空", nameof(openId));
UpdatePrimaryOAuthId(provider, openId, unionId);
UpdatedAt = DateTime.UtcNow;
var authorization = _oauthAuthorizations.FirstOrDefault(x => x.Provider == provider && x.OpenId == openId);
if (authorization == null)
{
authorization = OAuthAuthorization.Create(Id, provider, openId, unionId);
_oauthAuthorizations.Add(authorization);
}
else
{
authorization.UpdateUnionId(unionId);
}
}
public void BindWechat(string openId, string? unionId = null)
{
BindOAuth(OAuthProvider.Wechat, openId, unionId);
}
private void UpdatePrimaryOAuthId(OAuthProvider provider, string openId, string? unionId)
{
if (provider == OAuthProvider.Wechat)
{
OpenId = openId;
UnionId = unionId ?? UnionId;
}
}
public OAuthAuthorization? GetOAuthAuthorization(OAuthProvider provider)
{
return _oauthAuthorizations.FirstOrDefault(x => x.Provider == provider);
}
public bool HasOAuthBound(OAuthProvider provider, string openId)
{
return _oauthAuthorizations.Any(x => x.Provider == provider && x.OpenId == openId);
}
public void RecordOAuthLogin(OAuthProvider provider, string openId)
{
var authorization = _oauthAuthorizations.FirstOrDefault(x => x.Provider == provider && x.OpenId == openId);
authorization?.RecordLogin();
}
public void AddTag(string tagId, string? tagName = null)
{
var existingTag = _tags.FirstOrDefault(t => t.TagId == tagId);
if (existingTag != null)
return;
_tags.Add(MemberTag.Create(Id, tagId, tagName));
}
public void RemoveTag(string tagId)
{
var tag = _tags.FirstOrDefault(t => t.TagId == tagId);
if (tag != null)
_tags.Remove(tag);
}
public void Freeze()
{
if (Status == MemberStatus.Frozen)
return;
Status = MemberStatus.Frozen;
UpdatedAt = DateTime.UtcNow;
}
public void Unfreeze()
{
Status = MemberStatus.Active;
UpdatedAt = DateTime.UtcNow;
}
public void Deactivate()
{
if (Status == MemberStatus.Inactive)
return;
Status = MemberStatus.Inactive;
UpdatedAt = DateTime.UtcNow;
}
}