feat(member): change MemberId to Guid strongly typed id

- Convert MemberId from long to Guid strongly typed ID
- Update all Member commands to use record pattern with MemberId
- Update all Member endpoints to use record pattern with MemberId
- Update entity configurations to use GuidVersion7ValueGenerator
- Add implicit conversion operators for MemberId

Migration: ChangeMemberIdToGuid
This commit is contained in:
Sam 2026-02-10 00:41:35 +08:00
parent 3b8b86a258
commit 96503a593d
18 changed files with 823 additions and 416 deletions

View File

@ -5,64 +5,34 @@ using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member; namespace Fengling.Member.Application.Commands.Member;
public class AddMemberTagCommand : IRequest<AddMemberTagResponse> public record AddMemberTagCommand(MemberId MemberId, string TagId, string? TagName)
{ : IRequest<AddMemberTagResponse>;
public long MemberId { get; set; }
public string TagId { get; set; } = string.Empty;
public string? TagName { get; set; }
}
public class AddMemberTagResponse public record AddMemberTagResponse(MemberId MemberId, string TagId, string? TagName, DateTime AddedAt);
{
public long MemberId { get; set; }
public string TagId { get; set; } = string.Empty;
public string? TagName { get; set; }
public DateTime AddedAt { get; set; }
}
public class AddMemberTagCommandValidator : AbstractValidator<AddMemberTagCommand> public class AddMemberTagCommandValidator : AbstractValidator<AddMemberTagCommand>
{ {
public AddMemberTagCommandValidator() public AddMemberTagCommandValidator()
{ {
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.TagId).NotEmpty().MaximumLength(50); RuleFor(x => x.TagId).NotEmpty().MaximumLength(50);
RuleFor(x => x.TagName).MaximumLength(100); RuleFor(x => x.TagName).MaximumLength(100);
} }
} }
public class AddMemberTagCommandHandler : IRequestHandler<AddMemberTagCommand, AddMemberTagResponse> public class AddMemberTagCommandHandler(IMemberRepository memberRepository, ILogger<AddMemberTagCommandHandler> logger)
: IRequestHandler<AddMemberTagCommand, AddMemberTagResponse>
{ {
private readonly IMemberRepository _memberRepository;
private readonly ILogger<AddMemberTagCommandHandler> _logger;
public AddMemberTagCommandHandler(
IMemberRepository memberRepository,
ILogger<AddMemberTagCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<AddMemberTagResponse> Handle(AddMemberTagCommand request, CancellationToken cancellationToken) public async Task<AddMemberTagResponse> Handle(AddMemberTagCommand request, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Adding tag {TagId} to member {MemberId}", request.TagId, request.MemberId); logger.LogInformation("Adding tag {TagId} to member {MemberId}", request.TagId, request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken); var member = await memberRepository.GetAsync(request.MemberId, cancellationToken)
if (member == null) ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.AddTag(request.TagId, request.TagName); member.AddTag(request.TagId, request.TagName);
_logger.LogInformation("Tag {TagId} added to member {MemberId}", request.TagId, request.MemberId); logger.LogInformation("Tag {TagId} added to member {MemberId}", request.TagId, request.MemberId);
return new AddMemberTagResponse return new AddMemberTagResponse(member.Id, request.TagId, request.TagName, DateTime.UtcNow);
{
MemberId = member.Id,
TagId = request.TagId,
TagName = request.TagName,
AddedAt = DateTime.UtcNow
};
} }
} }

View File

@ -5,64 +5,34 @@ using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member; namespace Fengling.Member.Application.Commands.Member;
public class BindAlipayCommand : IRequest<BindAlipayResponse> public record BindAlipayCommand(MemberId MemberId, string AlipayOpenId, string? AlipayUserId)
{ : IRequest<BindAlipayResponse>;
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
}
public class BindAlipayResponse public record BindAlipayResponse(MemberId MemberId, string AlipayOpenId, string? AlipayUserId, DateTime BoundAt);
{
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
public DateTime BoundAt { get; set; }
}
public class BindAlipayCommandValidator : AbstractValidator<BindAlipayCommand> public class BindAlipayCommandValidator : AbstractValidator<BindAlipayCommand>
{ {
public BindAlipayCommandValidator() public BindAlipayCommandValidator()
{ {
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.AlipayOpenId).NotEmpty().MaximumLength(128); RuleFor(x => x.AlipayOpenId).NotEmpty().MaximumLength(128);
RuleFor(x => x.AlipayUserId).MaximumLength(128); RuleFor(x => x.AlipayUserId).MaximumLength(128);
} }
} }
public class BindAlipayCommandHandler : IRequestHandler<BindAlipayCommand, BindAlipayResponse> public class BindAlipayCommandHandler(IMemberRepository memberRepository, ILogger<BindAlipayCommandHandler> logger)
: IRequestHandler<BindAlipayCommand, BindAlipayResponse>
{ {
private readonly IMemberRepository _memberRepository;
private readonly ILogger<BindAlipayCommandHandler> _logger;
public BindAlipayCommandHandler(
IMemberRepository memberRepository,
ILogger<BindAlipayCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<BindAlipayResponse> Handle(BindAlipayCommand request, CancellationToken cancellationToken) public async Task<BindAlipayResponse> Handle(BindAlipayCommand request, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Binding Alipay for member {MemberId}", request.MemberId); logger.LogInformation("Binding Alipay for member {MemberId}", request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken); var member = await memberRepository.GetAsync(request.MemberId, cancellationToken)
if (member == null) ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.BindOAuth(OAuthProvider.Alipay, request.AlipayOpenId, request.AlipayUserId); member.BindOAuth(OAuthProvider.Alipay, request.AlipayOpenId, request.AlipayUserId);
_logger.LogInformation("Alipay bound successfully for member {MemberId}", request.MemberId); logger.LogInformation("Alipay bound successfully for member {MemberId}", request.MemberId);
return new BindAlipayResponse return new BindAlipayResponse(member.Id, request.AlipayOpenId, request.AlipayUserId, DateTime.UtcNow);
{
MemberId = member.Id,
AlipayOpenId = request.AlipayOpenId,
AlipayUserId = request.AlipayUserId,
BoundAt = DateTime.UtcNow
};
} }
} }

View File

@ -5,70 +5,37 @@ using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member; namespace Fengling.Member.Application.Commands.Member;
public class BindOAuthCommand : IRequest<BindOAuthResponse> public record BindOAuthCommand(OAuthProvider Provider, MemberId MemberId, string OpenId, string? UnionId)
{ : IRequest<BindOAuthResponse>;
public OAuthProvider Provider { get; set; }
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindOAuthResponse public record BindOAuthResponse(MemberId MemberId, OAuthProvider Provider, string OpenId, string? UnionId, DateTime BoundAt);
{
public long MemberId { get; set; }
public OAuthProvider Provider { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}
public class BindOAuthCommandValidator : AbstractValidator<BindOAuthCommand> public class BindOAuthCommandValidator : AbstractValidator<BindOAuthCommand>
{ {
public BindOAuthCommandValidator() public BindOAuthCommandValidator()
{ {
RuleFor(x => x.Provider).IsInEnum(); RuleFor(x => x.Provider).IsInEnum();
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.OpenId).NotEmpty().MaximumLength(128); RuleFor(x => x.OpenId).NotEmpty().MaximumLength(128);
RuleFor(x => x.UnionId).MaximumLength(128); RuleFor(x => x.UnionId).MaximumLength(128);
} }
} }
public class BindOAuthCommandHandler : IRequestHandler<BindOAuthCommand, BindOAuthResponse> public class BindOAuthCommandHandler(IMemberRepository memberRepository, ILogger<BindOAuthCommandHandler> logger)
: IRequestHandler<BindOAuthCommand, BindOAuthResponse>
{ {
private readonly IMemberRepository _memberRepository;
private readonly ILogger<BindOAuthCommandHandler> _logger;
public BindOAuthCommandHandler(
IMemberRepository memberRepository,
ILogger<BindOAuthCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<BindOAuthResponse> Handle(BindOAuthCommand request, CancellationToken cancellationToken) public async Task<BindOAuthResponse> Handle(BindOAuthCommand request, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Binding {Provider} for member {MemberId}", logger.LogInformation("Binding {Provider} for member {MemberId}",
request.Provider.GetProviderName(), request.MemberId); request.Provider.GetProviderName(), request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken); var member = await memberRepository.GetAsync(request.MemberId, cancellationToken)
if (member == null) ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.BindOAuth(request.Provider, request.OpenId, request.UnionId); member.BindOAuth(request.Provider, request.OpenId, request.UnionId);
_logger.LogInformation("{Provider} bound successfully for member {MemberId}", logger.LogInformation("{Provider} bound successfully for member {MemberId}",
request.Provider.GetProviderName(), request.MemberId); request.Provider.GetProviderName(), request.MemberId);
return new BindOAuthResponse return new BindOAuthResponse(member.Id, request.Provider, request.OpenId, request.UnionId, DateTime.UtcNow);
{
MemberId = member.Id,
Provider = request.Provider,
OpenId = request.OpenId,
UnionId = request.UnionId,
BoundAt = DateTime.UtcNow
};
} }
} }

View File

@ -5,64 +5,34 @@ using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member; namespace Fengling.Member.Application.Commands.Member;
public class BindWechatCommand : IRequest<BindWechatResponse> public record BindWechatCommand(MemberId MemberId, string OpenId, string? UnionId)
{ : IRequest<BindWechatResponse>;
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindWechatResponse public record BindWechatResponse(MemberId MemberId, string OpenId, string? UnionId, DateTime BoundAt);
{
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}
public class BindWechatCommandValidator : AbstractValidator<BindWechatCommand> public class BindWechatCommandValidator : AbstractValidator<BindWechatCommand>
{ {
public BindWechatCommandValidator() public BindWechatCommandValidator()
{ {
RuleFor(x => x.MemberId).GreaterThan(0);
RuleFor(x => x.OpenId).NotEmpty().MaximumLength(64); RuleFor(x => x.OpenId).NotEmpty().MaximumLength(64);
RuleFor(x => x.UnionId).MaximumLength(64); RuleFor(x => x.UnionId).MaximumLength(64);
} }
} }
public class BindWechatCommandHandler : IRequestHandler<BindWechatCommand, BindWechatResponse> public class BindWechatCommandHandler(IMemberRepository memberRepository, ILogger<BindWechatCommandHandler> logger)
: IRequestHandler<BindWechatCommand, BindWechatResponse>
{ {
private readonly IMemberRepository _memberRepository;
private readonly ILogger<BindWechatCommandHandler> _logger;
public BindWechatCommandHandler(
IMemberRepository memberRepository,
ILogger<BindWechatCommandHandler> logger)
{
_memberRepository = memberRepository;
_logger = logger;
}
public async Task<BindWechatResponse> Handle(BindWechatCommand request, CancellationToken cancellationToken) public async Task<BindWechatResponse> Handle(BindWechatCommand request, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Binding wechat for member {MemberId}", request.MemberId); logger.LogInformation("Binding wechat for member {MemberId}", request.MemberId);
var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken); var member = await memberRepository.GetAsync(request.MemberId, cancellationToken)
if (member == null) ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
{
throw new KeyNotFoundException($"会员不存在: {request.MemberId}");
}
member.BindWechat(request.OpenId, request.UnionId); member.BindWechat(request.OpenId, request.UnionId);
_logger.LogInformation("Wechat bound successfully for member {MemberId}", request.MemberId); logger.LogInformation("Wechat bound successfully for member {MemberId}", request.MemberId);
return new BindWechatResponse return new BindWechatResponse(member.Id, request.OpenId, request.UnionId, DateTime.UtcNow);
{
MemberId = member.Id,
OpenId = request.OpenId,
UnionId = request.UnionId,
BoundAt = DateTime.UtcNow
};
} }
} }

View File

@ -5,48 +5,33 @@ using Fengling.Member.Infrastructure.Repositories;
namespace Fengling.Member.Application.Commands.Member; namespace Fengling.Member.Application.Commands.Member;
public class RegisterMemberCommand : IRequest<RegisterMemberResponse> public record RegisterMemberCommand(long TenantId, string? PhoneNumber, string? OpenId, string? UnionId, string? Source)
{ : IRequest<RegisterMemberResponse>;
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public string? UnionId { get; set; }
public string? Source { get; set; }
}
public class RegisterMemberResponse public record RegisterMemberResponse(MemberId MemberId, long TenantId, string? PhoneNumber, string? OpenId, MemberStatus Status, DateTime RegisteredAt);
{
public long MemberId { get; set; }
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public MemberStatus Status { get; set; } = MemberStatus.Active;
public DateTime RegisteredAt { get; set; }
}
public class RegisterMemberCommandHandler : IRequestHandler<RegisterMemberCommand, RegisterMemberResponse> public class RegisterMemberCommandValidator : AbstractValidator<RegisterMemberCommand>
{ {
private readonly IMemberRepository _memberRepository; public RegisterMemberCommandValidator()
private readonly ILogger<RegisterMemberCommandHandler> _logger;
public RegisterMemberCommandHandler(
IMemberRepository memberRepository,
ILogger<RegisterMemberCommandHandler> logger)
{ {
_memberRepository = memberRepository; RuleFor(x => x.TenantId).GreaterThan(0);
_logger = logger; RuleFor(x => x.PhoneNumber).Matches(@"^1[3-9]\d{9}$").When(x => x.PhoneNumber != null);
} }
}
public class RegisterMemberCommandHandler(IMemberRepository memberRepository, ILogger<RegisterMemberCommandHandler> logger)
: IRequestHandler<RegisterMemberCommand, RegisterMemberResponse>
{
public async Task<RegisterMemberResponse> Handle(RegisterMemberCommand request, CancellationToken cancellationToken) public async Task<RegisterMemberResponse> Handle(RegisterMemberCommand request, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Registering new member for tenant {TenantId}", request.TenantId); logger.LogInformation("Registering new member for tenant {TenantId}", request.TenantId);
if (!string.IsNullOrEmpty(request.PhoneNumber)) if (!string.IsNullOrEmpty(request.PhoneNumber))
{ {
var existingMember = await _memberRepository.GetByPhoneNumberAsync(request.TenantId, request.PhoneNumber, cancellationToken); var existingMember = await memberRepository.GetByPhoneNumberAsync(request.TenantId, request.PhoneNumber, cancellationToken);
if (existingMember != null) if (existingMember != null)
{ {
_logger.LogWarning("Member with phone {PhoneNumber} already exists in tenant {TenantId}", logger.LogWarning("Member with phone {PhoneNumber} already exists in tenant {TenantId}",
request.PhoneNumber, request.TenantId); request.PhoneNumber, request.TenantId);
if (!string.IsNullOrEmpty(request.OpenId) && string.IsNullOrEmpty(existingMember.OpenId)) if (!string.IsNullOrEmpty(request.OpenId) && string.IsNullOrEmpty(existingMember.OpenId))
@ -54,34 +39,18 @@ public class RegisterMemberCommandHandler : IRequestHandler<RegisterMemberComman
existingMember.BindWechat(request.OpenId, request.UnionId); existingMember.BindWechat(request.OpenId, request.UnionId);
} }
return new RegisterMemberResponse return new RegisterMemberResponse(existingMember.Id, existingMember.TenantId, existingMember.PhoneNumber, existingMember.OpenId, existingMember.Status, existingMember.CreatedAt);
{
MemberId = existingMember.Id,
TenantId = existingMember.TenantId,
PhoneNumber = existingMember.PhoneNumber,
OpenId = existingMember.OpenId,
Status = existingMember.Status,
RegisteredAt = existingMember.CreatedAt
};
} }
} }
if (!string.IsNullOrEmpty(request.OpenId)) if (!string.IsNullOrEmpty(request.OpenId))
{ {
var existingByOpenId = await _memberRepository.GetByOpenIdAsync(request.OpenId, cancellationToken); var existingByOpenId = await memberRepository.GetByOpenIdAsync(request.OpenId, cancellationToken);
if (existingByOpenId != null) if (existingByOpenId != null)
{ {
_logger.LogWarning("Member with OpenId {OpenId} already exists", request.OpenId); logger.LogWarning("Member with OpenId {OpenId} already exists", request.OpenId);
return new RegisterMemberResponse return new RegisterMemberResponse(existingByOpenId.Id, existingByOpenId.TenantId, existingByOpenId.PhoneNumber, existingByOpenId.OpenId, existingByOpenId.Status, existingByOpenId.CreatedAt);
{
MemberId = existingByOpenId.Id,
TenantId = existingByOpenId.TenantId,
PhoneNumber = existingByOpenId.PhoneNumber,
OpenId = existingByOpenId.OpenId,
Status = existingByOpenId.Status,
RegisteredAt = existingByOpenId.CreatedAt
};
} }
} }
@ -92,18 +61,10 @@ public class RegisterMemberCommandHandler : IRequestHandler<RegisterMemberComman
member.BindWechat(request.OpenId, request.UnionId); member.BindWechat(request.OpenId, request.UnionId);
} }
await _memberRepository.AddAsync(member, cancellationToken); await memberRepository.AddAsync(member, cancellationToken);
_logger.LogInformation("Member registered successfully with ID {MemberId}", member.Id); logger.LogInformation("Member registered successfully with ID {MemberId}", member.Id);
return new RegisterMemberResponse return new RegisterMemberResponse(member.Id, member.TenantId, member.PhoneNumber, member.OpenId, member.Status, member.CreatedAt);
{
MemberId = member.Id,
TenantId = member.TenantId,
PhoneNumber = member.PhoneNumber,
OpenId = member.OpenId,
Status = member.Status,
RegisteredAt = member.CreatedAt
};
} }
} }

View File

@ -3,7 +3,17 @@ using Fengling.Member.Domain.Events.Member;
namespace Fengling.Member.Domain.Aggregates.Users; namespace Fengling.Member.Domain.Aggregates.Users;
public class MemberEntity : Entity<long>, IAggregateRoot 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 long TenantId { get; private set; }
public string? PhoneNumber { get; private set; } public string? PhoneNumber { get; private set; }

View File

@ -2,7 +2,7 @@ namespace Fengling.Member.Domain.Aggregates.Users;
public class MemberTag : Entity<long> public class MemberTag : Entity<long>
{ {
public long MemberId { get; private set; } public MemberId MemberId { get; private set; } = default!;
public string TagId { get; private set; } = string.Empty; public string TagId { get; private set; } = string.Empty;
public string? TagName { get; private set; } public string? TagName { get; private set; }
public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
@ -11,7 +11,7 @@ public class MemberTag : Entity<long>
{ {
} }
public static MemberTag Create(long memberId, string tagId, string? tagName = null) public static MemberTag Create(MemberId memberId, string tagId, string? tagName = null)
{ {
return new MemberTag return new MemberTag
{ {

View File

@ -2,7 +2,7 @@ namespace Fengling.Member.Domain.Aggregates.Users;
public class OAuthAuthorization : Entity<long> public class OAuthAuthorization : Entity<long>
{ {
public long MemberId { get; private set; } public MemberId MemberId { get; private set; } = default!;
public OAuthProvider Provider { get; private set; } public OAuthProvider Provider { get; private set; }
public string OpenId { get; private set; } = string.Empty; public string OpenId { get; private set; } = string.Empty;
public string? UnionId { get; private set; } public string? UnionId { get; private set; }
@ -16,7 +16,7 @@ public class OAuthAuthorization : Entity<long>
{ {
} }
public static OAuthAuthorization Create(long memberId, OAuthProvider provider, string openId, string? unionId = null) public static OAuthAuthorization Create(MemberId memberId, OAuthProvider provider, string openId, string? unionId = null)
{ {
return new OAuthAuthorization return new OAuthAuthorization
{ {

View File

@ -1,7 +1,9 @@
using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Domain.Events.Member; namespace Fengling.Member.Domain.Events.Member;
public record MemberRegisteredEvent( public record MemberRegisteredEvent(
long MemberId, MemberId MemberId,
long TenantId, long TenantId,
string? PhoneNumber, string? PhoneNumber,
DateTime RegisteredAt DateTime RegisteredAt

View File

@ -19,7 +19,8 @@ public class MemberEntityTypeConfiguration : IEntityTypeConfiguration<MemberEnti
builder.Property(m => m.Id) builder.Property(m => m.Id)
.HasColumnName("id") .HasColumnName("id")
.UseIdentityColumn(); .UseGuidVersion7ValueGenerator()
.HasComment("会员标识");
builder.Property(m => m.TenantId) builder.Property(m => m.TenantId)
.HasColumnName("tenant_id") .HasColumnName("tenant_id")

View File

@ -0,0 +1,596 @@
// <auto-generated />
using System;
using Fengling.Member.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Member.Infrastructure.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260209163416_ChangeMemberIdToGuid")]
partial class ChangeMemberIdToGuid
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<int>("FrozenPoints")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("frozen_points");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("user_id");
b.Property<long>("TenantId")
.HasColumnType("bigint")
.HasColumnName("tenant_id");
b.Property<int>("TotalPoints")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("points");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1)
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("MemberId")
.IsUnique()
.HasDatabaseName("idx_points_account_memberid");
b.HasIndex("MemberId", "TenantId")
.HasDatabaseName("idx_points_account_member_tenant");
b.ToTable("mka_integraldetails", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExpireAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("MemberId")
.HasColumnType("bigint");
b.Property<int>("Points")
.HasColumnType("integer");
b.Property<long>("PointsAccountId")
.HasColumnType("bigint");
b.Property<string>("Remark")
.HasColumnType("text");
b.Property<string>("SourceId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TransactionType")
.IsRequired()
.HasColumnType("text");
b.Property<int>("TransactionTypeCategory")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("PointsAccountId");
b.ToTable("PointsTransactions");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRule", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasComment("规则标识");
b.Property<int>("BasePoints")
.HasColumnType("integer")
.HasComment("基础积分");
b.Property<int>("CalculationMode")
.HasColumnType("integer")
.HasComment("计算模式");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasComment("规则编码");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<DateTime>("EffectiveFrom")
.HasColumnType("timestamp with time zone")
.HasComment("生效开始时间");
b.Property<DateTime?>("EffectiveTo")
.HasColumnType("timestamp with time zone")
.HasComment("生效结束时间");
b.Property<bool>("IsActive")
.HasColumnType("boolean")
.HasComment("是否启用");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("规则名称");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasComment("优先级");
b.Property<int>("RuleType")
.HasColumnType("integer")
.HasComment("规则类型");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("更新时间");
b.Property<int>("ValidityDays")
.HasColumnType("integer")
.HasComment("有效期天数");
b.Property<decimal?>("WeightFactor")
.HasPrecision(18, 4)
.HasColumnType("numeric(18,4)")
.HasComment("权重因子");
b.HasKey("Id");
b.HasIndex("Code")
.IsUnique();
b.HasIndex("IsActive");
b.ToTable("PointsRules", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRuleCondition", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasComment("条件标识");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间");
b.Property<int>("DimensionType")
.HasColumnType("integer")
.HasComment("维度类型");
b.Property<string>("DimensionValue")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasComment("维度值");
b.Property<string>("Operator")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasComment("操作符");
b.Property<Guid>("RuleId")
.HasColumnType("uuid")
.HasComment("关联规则标识");
b.HasKey("Id");
b.HasIndex("RuleId", "DimensionType");
b.ToTable("PointsRuleConditions", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id")
.HasComment("会员标识");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<string>("OpenId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("open_id");
b.Property<string>("PhoneNumber")
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("phone_number");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("status");
b.Property<long>("TenantId")
.HasColumnType("bigint")
.HasColumnName("tenant_id");
b.Property<string>("UnionId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("union_id");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.Property<int>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1)
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("OpenId")
.HasDatabaseName("idx_member_openid");
b.HasIndex("TenantId")
.HasDatabaseName("idx_member_tenantid");
b.HasIndex("UnionId")
.HasDatabaseName("idx_member_unionid");
b.HasIndex("TenantId", "PhoneNumber")
.HasDatabaseName("idx_member_tenant_phone");
b.ToTable("fls_member", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<Guid>("MemberId")
.HasColumnType("uuid")
.HasColumnName("member_id");
b.Property<string>("TagId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("tag_id");
b.Property<string>("TagName")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("tag_name");
b.HasKey("Id");
b.HasIndex("TagId")
.HasDatabaseName("idx_membertag_tagid");
b.HasIndex("MemberId", "TagId")
.IsUnique()
.HasDatabaseName("idx_membertag_member_tag");
b.ToTable("fls_member_tag", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.OAuthAuthorization", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("AccessToken")
.HasColumnType("text");
b.Property<DateTime>("AuthorizedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("MemberId")
.HasColumnType("uuid");
b.Property<string>("OpenId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Provider")
.HasColumnType("integer");
b.Property<string>("RefreshToken")
.HasColumnType("text");
b.Property<DateTime?>("TokenExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UnionId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("MemberId");
b.ToTable("OAuthAuthorization");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("AuthorizedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("authorized_at");
b.Property<DateTime?>("LastLoginAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_login_at");
b.Property<long>("MemberId")
.HasColumnType("bigint")
.HasColumnName("member_id");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("open_id");
b.Property<string>("UnionId")
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("union_id");
b.HasKey("Id");
b.HasIndex("MemberId")
.HasDatabaseName("idx_wechat_auth_memberid");
b.HasIndex("OpenId")
.IsUnique()
.HasDatabaseName("idx_wechat_auth_openid");
b.HasIndex("UnionId")
.HasDatabaseName("idx_wechat_auth_unionid");
b.ToTable("fls_wechat_authorization", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.CapLock", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("Instance")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<DateTime?>("LastLockTime")
.HasColumnType("TIMESTAMP");
b.HasKey("Key");
b.ToTable("CAPLock", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("character varying(40)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName");
b.ToTable("CAPPublishedMessage", (string)null);
});
modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.ReceivedMessage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("Added")
.HasColumnType("TIMESTAMP");
b.Property<string>("Content")
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("TIMESTAMP");
b.Property<string>("Group")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(400)
.HasColumnType("character varying(400)");
b.Property<int?>("Retries")
.HasColumnType("integer");
b.Property<string>("StatusName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Version")
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.HasKey("Id");
b.HasIndex(new[] { "ExpiresAt", "StatusName" }, "IX_ExpiresAt_StatusName")
.HasDatabaseName("IX_ExpiresAt_StatusName1");
b.HasIndex(new[] { "Version", "ExpiresAt", "StatusName" }, "IX_Version_ExpiresAt_StatusName")
.HasDatabaseName("IX_Version_ExpiresAt_StatusName1");
b.ToTable("CAPReceivedMessage", (string)null);
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsTransaction", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", null)
.WithMany("Transactions")
.HasForeignKey("PointsAccountId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRuleCondition", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRule", null)
.WithMany("Conditions")
.HasForeignKey("RuleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberTag", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
.WithMany("Tags")
.HasForeignKey("MemberId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.OAuthAuthorization", b =>
{
b.HasOne("Fengling.Member.Domain.Aggregates.Users.MemberEntity", null)
.WithMany("OAuthAuthorizations")
.HasForeignKey("MemberId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsModel.PointsAccount", b =>
{
b.Navigation("Transactions");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRule", b =>
{
b.Navigation("Conditions");
});
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
{
b.Navigation("OAuthAuthorizations");
b.Navigation("Tags");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Fengling.Member.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class ChangeMemberIdToGuid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<Guid>(
name: "MemberId",
table: "OAuthAuthorization",
type: "uuid",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<Guid>(
name: "member_id",
table: "fls_member_tag",
type: "uuid",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<Guid>(
name: "id",
table: "fls_member",
type: "uuid",
nullable: false,
comment: "会员标识",
oldClrType: typeof(long),
oldType: "bigint")
.OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<long>(
name: "MemberId",
table: "OAuthAuthorization",
type: "bigint",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<long>(
name: "member_id",
table: "fls_member_tag",
type: "bigint",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<long>(
name: "id",
table: "fls_member",
type: "bigint",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "会员标识")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
}
}
}

View File

@ -233,12 +233,10 @@ namespace Fengling.Member.Infrastructure.Migrations
modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b => modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b =>
{ {
b.Property<long>("Id") b.Property<Guid>("Id")
.ValueGeneratedOnAdd() .HasColumnType("uuid")
.HasColumnType("bigint") .HasColumnName("id")
.HasColumnName("id"); .HasComment("会员标识");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@ -310,8 +308,8 @@ namespace Fengling.Member.Infrastructure.Migrations
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("created_at"); .HasColumnName("created_at");
b.Property<long>("MemberId") b.Property<Guid>("MemberId")
.HasColumnType("bigint") .HasColumnType("uuid")
.HasColumnName("member_id"); .HasColumnName("member_id");
b.Property<string>("TagId") b.Property<string>("TagId")
@ -354,8 +352,8 @@ namespace Fengling.Member.Infrastructure.Migrations
b.Property<DateTime?>("LastLoginAt") b.Property<DateTime?>("LastLoginAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<long>("MemberId") b.Property<Guid>("MemberId")
.HasColumnType("bigint"); .HasColumnType("uuid");
b.Property<string>("OpenId") b.Property<string>("OpenId")
.IsRequired() .IsRequired()

View File

@ -2,35 +2,51 @@ using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Infrastructure.Repositories; namespace Fengling.Member.Infrastructure.Repositories;
public interface IMemberRepository : IRepository<Fengling.Member.Domain.Aggregates.Users.MemberEntity, long> public interface IMemberRepository : IRepository<Fengling.Member.Domain.Aggregates.Users.MemberEntity, MemberId>
{ {
Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default); Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByPhoneNumberAsync(long tenantId, string phoneNumber,
Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByOpenIdAsync(string openId, CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByUnionIdAsync(string unionId, CancellationToken cancellationToken = default);
Task<IEnumerable<Fengling.Member.Domain.Aggregates.Users.MemberEntity>> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default); Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByOpenIdAsync(string openId,
Task<bool> ExistsByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByUnionIdAsync(string unionId,
CancellationToken cancellationToken = default);
Task<IEnumerable<Fengling.Member.Domain.Aggregates.Users.MemberEntity>> GetByTenantIdAsync(long tenantId,
int page = 1, int pageSize = 20, CancellationToken cancellationToken = default);
Task<bool> ExistsByPhoneNumberAsync(long tenantId, string phoneNumber,
CancellationToken cancellationToken = default);
Task<bool> ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default); Task<bool> ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default);
} }
public class MemberRepository(ApplicationDbContext context) : public class MemberRepository(ApplicationDbContext context)
RepositoryBase<Fengling.Member.Domain.Aggregates.Users.MemberEntity, long, ApplicationDbContext>(context), IMemberRepository : RepositoryBase<Fengling.Member.Domain.Aggregates.Users.MemberEntity, MemberId, ApplicationDbContext>(context),
IMemberRepository
{ {
public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default) public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByPhoneNumberAsync(long tenantId,
string phoneNumber, CancellationToken cancellationToken = default)
{ {
return await DbContext.Members.FirstOrDefaultAsync(m => m.TenantId == tenantId && m.PhoneNumber == phoneNumber, cancellationToken); return await DbContext.Members.FirstOrDefaultAsync(m => m.TenantId == tenantId && m.PhoneNumber == phoneNumber,
cancellationToken);
} }
public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByOpenIdAsync(string openId, CancellationToken cancellationToken = default) public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByOpenIdAsync(string openId,
CancellationToken cancellationToken = default)
{ {
return await DbContext.Members.FirstOrDefaultAsync(m => m.OpenId == openId, cancellationToken); return await DbContext.Members.FirstOrDefaultAsync(m => m.OpenId == openId, cancellationToken);
} }
public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByUnionIdAsync(string unionId, CancellationToken cancellationToken = default) public async Task<Fengling.Member.Domain.Aggregates.Users.MemberEntity?> GetByUnionIdAsync(string unionId,
CancellationToken cancellationToken = default)
{ {
return await DbContext.Members.FirstOrDefaultAsync(m => m.UnionId == unionId, cancellationToken); return await DbContext.Members.FirstOrDefaultAsync(m => m.UnionId == unionId, cancellationToken);
} }
public async Task<IEnumerable<Fengling.Member.Domain.Aggregates.Users.MemberEntity>> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default) public async Task<IEnumerable<Fengling.Member.Domain.Aggregates.Users.MemberEntity>> GetByTenantIdAsync(
long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default)
{ {
return await DbContext.Members return await DbContext.Members
.Where(m => m.TenantId == tenantId) .Where(m => m.TenantId == tenantId)
@ -40,13 +56,15 @@ public class MemberRepository(ApplicationDbContext context) :
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
public async Task<bool> ExistsByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default) public async Task<bool> ExistsByPhoneNumberAsync(long tenantId, string phoneNumber,
CancellationToken cancellationToken = default)
{ {
return await DbContext.Members.AnyAsync(m => m.TenantId == tenantId && m.PhoneNumber == phoneNumber, cancellationToken); return await DbContext.Members.AnyAsync(m => m.TenantId == tenantId && m.PhoneNumber == phoneNumber,
cancellationToken);
} }
public async Task<bool> ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default) public async Task<bool> ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default)
{ {
return await DbContext.Members.AnyAsync(m => m.OpenId == openId, cancellationToken); return await DbContext.Members.AnyAsync(m => m.OpenId == openId, cancellationToken);
} }
} }

View File

@ -2,18 +2,13 @@ using FastEndpoints;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Fengling.Member.Application.Commands.Member; using Fengling.Member.Application.Commands.Member;
using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Web.Endpoints.v1; namespace Fengling.Member.Web.Endpoints.v1;
public class BindAlipayEndpoint : Endpoint<BindAlipayRequest, BindAlipayResponse> public class BindAlipayEndpoint(IMediator mediator)
: Endpoint<BindAlipayRequest, BindAlipayResponse>
{ {
private readonly IMediator _mediator;
public BindAlipayEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure() public override void Configure()
{ {
Post("/api/v1/members/{MemberId}/alipay"); Post("/api/v1/members/{MemberId}/alipay");
@ -26,37 +21,13 @@ public class BindAlipayEndpoint : Endpoint<BindAlipayRequest, BindAlipayResponse
public override async Task HandleAsync(BindAlipayRequest req, CancellationToken ct) public override async Task HandleAsync(BindAlipayRequest req, CancellationToken ct)
{ {
var command = new BindAlipayCommand var command = new BindAlipayCommand(req.MemberId, req.AlipayOpenId, req.AlipayUserId);
{
MemberId = req.MemberId,
AlipayOpenId = req.AlipayOpenId,
AlipayUserId = req.AlipayUserId
};
var result = await _mediator.Send(command, ct); var result = await mediator.Send(command, ct);
Response = new BindAlipayResponse Response = new BindAlipayResponse(result.MemberId, result.AlipayOpenId, result.AlipayUserId, result.BoundAt);
{
MemberId = result.MemberId,
AlipayOpenId = result.AlipayOpenId,
AlipayUserId = result.AlipayUserId,
BoundAt = result.BoundAt
};
} }
} }
public class BindAlipayRequest public record BindAlipayRequest([FromRoute] MemberId MemberId, string AlipayOpenId, string? AlipayUserId);
{ public record BindAlipayResponse(MemberId MemberId, string AlipayOpenId, string? AlipayUserId, DateTime BoundAt);
[FromRoute]
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
}
public class BindAlipayResponse
{
public long MemberId { get; set; }
public string AlipayOpenId { get; set; } = string.Empty;
public string? AlipayUserId { get; set; }
public DateTime BoundAt { get; set; }
}

View File

@ -1,18 +1,13 @@
using FastEndpoints; using FastEndpoints;
using MediatR; using MediatR;
using Fengling.Member.Application.Commands.Member; using Fengling.Member.Application.Commands.Member;
using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Web.Endpoints.v1; namespace Fengling.Member.Web.Endpoints.v1;
public class RegisterMemberEndpoint : Endpoint<RegisterMemberRequest, RegisterMemberResponse> public class RegisterMemberEndpoint(IMediator mediator)
: Endpoint<RegisterMemberRequest, RegisterMemberResponse>
{ {
private readonly IMediator _mediator;
public RegisterMemberEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure() public override void Configure()
{ {
Post("/api/v1/members"); Post("/api/v1/members");
@ -26,44 +21,13 @@ public class RegisterMemberEndpoint : Endpoint<RegisterMemberRequest, RegisterMe
public override async Task HandleAsync(RegisterMemberRequest req, CancellationToken ct) public override async Task HandleAsync(RegisterMemberRequest req, CancellationToken ct)
{ {
var command = new RegisterMemberCommand var command = new RegisterMemberCommand(req.TenantId, req.PhoneNumber, req.OpenId, req.UnionId, req.Source);
{
TenantId = req.TenantId,
PhoneNumber = req.PhoneNumber,
OpenId = req.OpenId,
UnionId = req.UnionId,
Source = req.Source
};
var result = await _mediator.Send(command, ct); var result = await mediator.Send(command, ct);
Response = new RegisterMemberResponse Response = new RegisterMemberResponse(result.MemberId, result.TenantId, result.PhoneNumber, result.OpenId, result.Status.ToString(), result.RegisteredAt);
{
MemberId = result.MemberId,
TenantId = result.TenantId,
PhoneNumber = result.PhoneNumber,
OpenId = result.OpenId,
Status = result.Status.ToString(),
RegisteredAt = result.RegisteredAt
};
} }
} }
public class RegisterMemberRequest public record RegisterMemberRequest(long TenantId, string? PhoneNumber, string? OpenId, string? UnionId, string? Source);
{ public record RegisterMemberResponse(MemberId MemberId, long TenantId, string? PhoneNumber, string? OpenId, string Status, DateTime RegisteredAt);
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public string? UnionId { get; set; }
public string? Source { get; set; }
}
public class RegisterMemberResponse
{
public long MemberId { get; set; }
public long TenantId { get; set; }
public string? PhoneNumber { get; set; }
public string? OpenId { get; set; }
public string Status { get; set; } = string.Empty;
public DateTime RegisteredAt { get; set; }
}

View File

@ -2,18 +2,13 @@ using FastEndpoints;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Fengling.Member.Application.Commands.Member; using Fengling.Member.Application.Commands.Member;
using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Web.Endpoints.v1; namespace Fengling.Member.Web.Endpoints.v1;
public class BindOAuthEndpoint : Endpoint<BindOAuthRequest, BindOAuthResponse> public class BindOAuthEndpoint(IMediator mediator)
: Endpoint<BindOAuthRequest, BindOAuthResponse>
{ {
private readonly IMediator _mediator;
public BindOAuthEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure() public override void Configure()
{ {
Post("/api/v1/members/{MemberId}/oauth"); Post("/api/v1/members/{MemberId}/oauth");
@ -26,42 +21,13 @@ public class BindOAuthEndpoint : Endpoint<BindOAuthRequest, BindOAuthResponse>
public override async Task HandleAsync(BindOAuthRequest req, CancellationToken ct) public override async Task HandleAsync(BindOAuthRequest req, CancellationToken ct)
{ {
var command = new BindOAuthCommand var command = new BindOAuthCommand(req.Provider, req.MemberId, req.OpenId, req.UnionId);
{
MemberId = req.MemberId,
Provider = req.Provider,
OpenId = req.OpenId,
UnionId = req.UnionId
};
var result = await _mediator.Send(command, ct); var result = await mediator.Send(command, ct);
Response = new BindOAuthResponse Response = new BindOAuthResponse(result.MemberId, result.Provider, result.OpenId, result.UnionId, result.BoundAt);
{
MemberId = result.MemberId,
Provider = result.Provider,
OpenId = result.OpenId,
UnionId = result.UnionId,
BoundAt = result.BoundAt
};
} }
} }
public class BindOAuthRequest public record BindOAuthRequest([FromRoute] MemberId MemberId, [FromRoute] OAuthProvider Provider, string OpenId, string? UnionId);
{ public record BindOAuthResponse(MemberId MemberId, OAuthProvider Provider, string OpenId, string? UnionId, DateTime BoundAt);
[FromRoute]
public long MemberId { get; set; }
[FromRoute]
public Domain.Aggregates.Users.OAuthProvider Provider { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindOAuthResponse
{
public long MemberId { get; set; }
public Domain.Aggregates.Users.OAuthProvider Provider { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}

View File

@ -2,18 +2,13 @@ using FastEndpoints;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Fengling.Member.Application.Commands.Member; using Fengling.Member.Application.Commands.Member;
using Fengling.Member.Domain.Aggregates.Users;
namespace Fengling.Member.Web.Endpoints.v1; namespace Fengling.Member.Web.Endpoints.v1;
public class BindWechatEndpoint : Endpoint<BindWechatRequest, BindWechatResponse> public class BindWechatEndpoint(IMediator mediator)
: Endpoint<BindWechatRequest, BindWechatResponse>
{ {
private readonly IMediator _mediator;
public BindWechatEndpoint(IMediator mediator)
{
_mediator = mediator;
}
public override void Configure() public override void Configure()
{ {
Post("/api/v1/members/{MemberId}/wechat"); Post("/api/v1/members/{MemberId}/wechat");
@ -26,37 +21,13 @@ public class BindWechatEndpoint : Endpoint<BindWechatRequest, BindWechatResponse
public override async Task HandleAsync(BindWechatRequest req, CancellationToken ct) public override async Task HandleAsync(BindWechatRequest req, CancellationToken ct)
{ {
var command = new BindWechatCommand var command = new BindWechatCommand(req.MemberId, req.OpenId, req.UnionId);
{
MemberId = req.MemberId,
OpenId = req.OpenId,
UnionId = req.UnionId
};
var result = await _mediator.Send(command, ct); var result = await mediator.Send(command, ct);
Response = new BindWechatResponse Response = new BindWechatResponse(result.MemberId, result.OpenId, result.UnionId, result.BoundAt);
{
MemberId = result.MemberId,
OpenId = result.OpenId,
UnionId = result.UnionId,
BoundAt = result.BoundAt
};
} }
} }
public class BindWechatRequest public record BindWechatRequest([FromRoute] MemberId MemberId, string OpenId, string? UnionId);
{ public record BindWechatResponse(MemberId MemberId, string OpenId, string? UnionId, DateTime BoundAt);
[FromRoute]
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
}
public class BindWechatResponse
{
public long MemberId { get; set; }
public string OpenId { get; set; } = string.Empty;
public string? UnionId { get; set; }
public DateTime BoundAt { get; set; }
}