diff --git a/src/Fengling.Member.Application/Commands/Member/AddMemberTagCommand.cs b/src/Fengling.Member.Application/Commands/Member/AddMemberTagCommand.cs index c6ab71d..3cc9c3d 100644 --- a/src/Fengling.Member.Application/Commands/Member/AddMemberTagCommand.cs +++ b/src/Fengling.Member.Application/Commands/Member/AddMemberTagCommand.cs @@ -5,64 +5,34 @@ using Fengling.Member.Infrastructure.Repositories; namespace Fengling.Member.Application.Commands.Member; -public class AddMemberTagCommand : IRequest -{ - public long MemberId { get; set; } - public string TagId { get; set; } = string.Empty; - public string? TagName { get; set; } -} +public record AddMemberTagCommand(MemberId MemberId, string TagId, string? TagName) + : IRequest; -public class AddMemberTagResponse -{ - public long MemberId { get; set; } - public string TagId { get; set; } = string.Empty; - public string? TagName { get; set; } - public DateTime AddedAt { get; set; } -} +public record AddMemberTagResponse(MemberId MemberId, string TagId, string? TagName, DateTime AddedAt); public class AddMemberTagCommandValidator : AbstractValidator { public AddMemberTagCommandValidator() { - RuleFor(x => x.MemberId).GreaterThan(0); RuleFor(x => x.TagId).NotEmpty().MaximumLength(50); RuleFor(x => x.TagName).MaximumLength(100); } } -public class AddMemberTagCommandHandler : IRequestHandler +public class AddMemberTagCommandHandler(IMemberRepository memberRepository, ILogger logger) + : IRequestHandler { - private readonly IMemberRepository _memberRepository; - private readonly ILogger _logger; - - public AddMemberTagCommandHandler( - IMemberRepository memberRepository, - ILogger logger) - { - _memberRepository = memberRepository; - _logger = logger; - } - public async Task 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); - if (member == null) - { - throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); - } + var member = await memberRepository.GetAsync(request.MemberId, cancellationToken) + ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); 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 - { - MemberId = member.Id, - TagId = request.TagId, - TagName = request.TagName, - AddedAt = DateTime.UtcNow - }; + return new AddMemberTagResponse(member.Id, request.TagId, request.TagName, DateTime.UtcNow); } } diff --git a/src/Fengling.Member.Application/Commands/Member/BindAlipayCommand.cs b/src/Fengling.Member.Application/Commands/Member/BindAlipayCommand.cs index 024ccbd..968313d 100644 --- a/src/Fengling.Member.Application/Commands/Member/BindAlipayCommand.cs +++ b/src/Fengling.Member.Application/Commands/Member/BindAlipayCommand.cs @@ -5,64 +5,34 @@ using Fengling.Member.Infrastructure.Repositories; namespace Fengling.Member.Application.Commands.Member; -public class BindAlipayCommand : IRequest -{ - public long MemberId { get; set; } - public string AlipayOpenId { get; set; } = string.Empty; - public string? AlipayUserId { get; set; } -} +public record BindAlipayCommand(MemberId MemberId, string AlipayOpenId, string? AlipayUserId) + : IRequest; -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; } -} +public record BindAlipayResponse(MemberId MemberId, string AlipayOpenId, string? AlipayUserId, DateTime BoundAt); public class BindAlipayCommandValidator : AbstractValidator { public BindAlipayCommandValidator() { - RuleFor(x => x.MemberId).GreaterThan(0); RuleFor(x => x.AlipayOpenId).NotEmpty().MaximumLength(128); RuleFor(x => x.AlipayUserId).MaximumLength(128); } } -public class BindAlipayCommandHandler : IRequestHandler +public class BindAlipayCommandHandler(IMemberRepository memberRepository, ILogger logger) + : IRequestHandler { - private readonly IMemberRepository _memberRepository; - private readonly ILogger _logger; - - public BindAlipayCommandHandler( - IMemberRepository memberRepository, - ILogger logger) - { - _memberRepository = memberRepository; - _logger = logger; - } - public async Task 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); - if (member == null) - { - throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); - } + var member = await memberRepository.GetAsync(request.MemberId, cancellationToken) + ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); 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 - { - MemberId = member.Id, - AlipayOpenId = request.AlipayOpenId, - AlipayUserId = request.AlipayUserId, - BoundAt = DateTime.UtcNow - }; + return new BindAlipayResponse(member.Id, request.AlipayOpenId, request.AlipayUserId, DateTime.UtcNow); } } diff --git a/src/Fengling.Member.Application/Commands/Member/BindOAuthCommand.cs b/src/Fengling.Member.Application/Commands/Member/BindOAuthCommand.cs index d1020f1..157a1d9 100644 --- a/src/Fengling.Member.Application/Commands/Member/BindOAuthCommand.cs +++ b/src/Fengling.Member.Application/Commands/Member/BindOAuthCommand.cs @@ -5,70 +5,37 @@ using Fengling.Member.Infrastructure.Repositories; namespace Fengling.Member.Application.Commands.Member; -public class BindOAuthCommand : IRequest -{ - public OAuthProvider Provider { get; set; } - public long MemberId { get; set; } - public string OpenId { get; set; } = string.Empty; - public string? UnionId { get; set; } -} +public record BindOAuthCommand(OAuthProvider Provider, MemberId MemberId, string OpenId, string? UnionId) + : IRequest; -public class BindOAuthResponse -{ - 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 record BindOAuthResponse(MemberId MemberId, OAuthProvider Provider, string OpenId, string? UnionId, DateTime BoundAt); public class BindOAuthCommandValidator : AbstractValidator { public BindOAuthCommandValidator() { RuleFor(x => x.Provider).IsInEnum(); - RuleFor(x => x.MemberId).GreaterThan(0); RuleFor(x => x.OpenId).NotEmpty().MaximumLength(128); RuleFor(x => x.UnionId).MaximumLength(128); } } -public class BindOAuthCommandHandler : IRequestHandler +public class BindOAuthCommandHandler(IMemberRepository memberRepository, ILogger logger) + : IRequestHandler { - private readonly IMemberRepository _memberRepository; - private readonly ILogger _logger; - - public BindOAuthCommandHandler( - IMemberRepository memberRepository, - ILogger logger) - { - _memberRepository = memberRepository; - _logger = logger; - } - public async Task Handle(BindOAuthCommand request, CancellationToken cancellationToken) { - _logger.LogInformation("Binding {Provider} for member {MemberId}", + logger.LogInformation("Binding {Provider} for member {MemberId}", request.Provider.GetProviderName(), request.MemberId); - var member = await _memberRepository.GetAsync(request.MemberId, cancellationToken); - if (member == null) - { - throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); - } + var member = await memberRepository.GetAsync(request.MemberId, cancellationToken) + ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); 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); - return new BindOAuthResponse - { - MemberId = member.Id, - Provider = request.Provider, - OpenId = request.OpenId, - UnionId = request.UnionId, - BoundAt = DateTime.UtcNow - }; + return new BindOAuthResponse(member.Id, request.Provider, request.OpenId, request.UnionId, DateTime.UtcNow); } } diff --git a/src/Fengling.Member.Application/Commands/Member/BindWechatCommand.cs b/src/Fengling.Member.Application/Commands/Member/BindWechatCommand.cs index 705f410..83b1c01 100644 --- a/src/Fengling.Member.Application/Commands/Member/BindWechatCommand.cs +++ b/src/Fengling.Member.Application/Commands/Member/BindWechatCommand.cs @@ -5,64 +5,34 @@ using Fengling.Member.Infrastructure.Repositories; namespace Fengling.Member.Application.Commands.Member; -public class BindWechatCommand : IRequest -{ - public long MemberId { get; set; } - public string OpenId { get; set; } = string.Empty; - public string? UnionId { get; set; } -} +public record BindWechatCommand(MemberId MemberId, string OpenId, string? UnionId) + : IRequest; -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; } -} +public record BindWechatResponse(MemberId MemberId, string OpenId, string? UnionId, DateTime BoundAt); public class BindWechatCommandValidator : AbstractValidator { public BindWechatCommandValidator() { - RuleFor(x => x.MemberId).GreaterThan(0); RuleFor(x => x.OpenId).NotEmpty().MaximumLength(64); RuleFor(x => x.UnionId).MaximumLength(64); } } -public class BindWechatCommandHandler : IRequestHandler +public class BindWechatCommandHandler(IMemberRepository memberRepository, ILogger logger) + : IRequestHandler { - private readonly IMemberRepository _memberRepository; - private readonly ILogger _logger; - - public BindWechatCommandHandler( - IMemberRepository memberRepository, - ILogger logger) - { - _memberRepository = memberRepository; - _logger = logger; - } - public async Task 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); - if (member == null) - { - throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); - } + var member = await memberRepository.GetAsync(request.MemberId, cancellationToken) + ?? throw new KeyNotFoundException($"会员不存在: {request.MemberId}"); 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 - { - MemberId = member.Id, - OpenId = request.OpenId, - UnionId = request.UnionId, - BoundAt = DateTime.UtcNow - }; + return new BindWechatResponse(member.Id, request.OpenId, request.UnionId, DateTime.UtcNow); } } diff --git a/src/Fengling.Member.Application/Commands/Member/RegisterMemberCommand.cs b/src/Fengling.Member.Application/Commands/Member/RegisterMemberCommand.cs index 16551f5..9195ac7 100644 --- a/src/Fengling.Member.Application/Commands/Member/RegisterMemberCommand.cs +++ b/src/Fengling.Member.Application/Commands/Member/RegisterMemberCommand.cs @@ -5,48 +5,33 @@ using Fengling.Member.Infrastructure.Repositories; namespace Fengling.Member.Application.Commands.Member; -public class RegisterMemberCommand : IRequest -{ - 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 record RegisterMemberCommand(long TenantId, string? PhoneNumber, string? OpenId, string? UnionId, string? Source) + : IRequest; -public class RegisterMemberResponse -{ - 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 record RegisterMemberResponse(MemberId MemberId, long TenantId, string? PhoneNumber, string? OpenId, MemberStatus Status, DateTime RegisteredAt); -public class RegisterMemberCommandHandler : IRequestHandler +public class RegisterMemberCommandValidator : AbstractValidator { - private readonly IMemberRepository _memberRepository; - private readonly ILogger _logger; - - public RegisterMemberCommandHandler( - IMemberRepository memberRepository, - ILogger logger) + public RegisterMemberCommandValidator() { - _memberRepository = memberRepository; - _logger = logger; + RuleFor(x => x.TenantId).GreaterThan(0); + RuleFor(x => x.PhoneNumber).Matches(@"^1[3-9]\d{9}$").When(x => x.PhoneNumber != null); } +} +public class RegisterMemberCommandHandler(IMemberRepository memberRepository, ILogger logger) + : IRequestHandler +{ public async Task 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)) { - var existingMember = await _memberRepository.GetByPhoneNumberAsync(request.TenantId, request.PhoneNumber, cancellationToken); + var existingMember = await memberRepository.GetByPhoneNumberAsync(request.TenantId, request.PhoneNumber, cancellationToken); 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); if (!string.IsNullOrEmpty(request.OpenId) && string.IsNullOrEmpty(existingMember.OpenId)) @@ -54,34 +39,18 @@ public class RegisterMemberCommandHandler : IRequestHandler, 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, IAggregateRoot { public long TenantId { get; private set; } public string? PhoneNumber { get; private set; } diff --git a/src/Fengling.Member.Domain/Aggregates/Users/MemberTag.cs b/src/Fengling.Member.Domain/Aggregates/Users/MemberTag.cs index 0b61979..8c848b7 100644 --- a/src/Fengling.Member.Domain/Aggregates/Users/MemberTag.cs +++ b/src/Fengling.Member.Domain/Aggregates/Users/MemberTag.cs @@ -2,7 +2,7 @@ namespace Fengling.Member.Domain.Aggregates.Users; public class MemberTag : Entity { - public long MemberId { get; private set; } + public MemberId MemberId { get; private set; } = default!; public string TagId { get; private set; } = string.Empty; public string? TagName { get; private set; } public DateTime CreatedAt { get; private set; } = DateTime.UtcNow; @@ -11,7 +11,7 @@ public class MemberTag : Entity { } - 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 { diff --git a/src/Fengling.Member.Domain/Aggregates/Users/OAuthAuthorization.cs b/src/Fengling.Member.Domain/Aggregates/Users/OAuthAuthorization.cs index 100c8d6..746ddc8 100644 --- a/src/Fengling.Member.Domain/Aggregates/Users/OAuthAuthorization.cs +++ b/src/Fengling.Member.Domain/Aggregates/Users/OAuthAuthorization.cs @@ -2,7 +2,7 @@ namespace Fengling.Member.Domain.Aggregates.Users; public class OAuthAuthorization : Entity { - public long MemberId { get; private set; } + public MemberId MemberId { get; private set; } = default!; public OAuthProvider Provider { get; private set; } public string OpenId { get; private set; } = string.Empty; public string? UnionId { get; private set; } @@ -16,7 +16,7 @@ public class OAuthAuthorization : Entity { } - 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 { diff --git a/src/Fengling.Member.Domain/Events/Member/MemberRegisteredEvent.cs b/src/Fengling.Member.Domain/Events/Member/MemberRegisteredEvent.cs index 5d1c56f..235ee5a 100644 --- a/src/Fengling.Member.Domain/Events/Member/MemberRegisteredEvent.cs +++ b/src/Fengling.Member.Domain/Events/Member/MemberRegisteredEvent.cs @@ -1,7 +1,9 @@ +using Fengling.Member.Domain.Aggregates.Users; + namespace Fengling.Member.Domain.Events.Member; public record MemberRegisteredEvent( - long MemberId, + MemberId MemberId, long TenantId, string? PhoneNumber, DateTime RegisteredAt diff --git a/src/Fengling.Member.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs b/src/Fengling.Member.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs index 27800a8..54baf48 100644 --- a/src/Fengling.Member.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs +++ b/src/Fengling.Member.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs @@ -19,7 +19,8 @@ public class MemberEntityTypeConfiguration : IEntityTypeConfiguration m.Id) .HasColumnName("id") - .UseIdentityColumn(); + .UseGuidVersion7ValueGenerator() + .HasComment("会员标识"); builder.Property(m => m.TenantId) .HasColumnName("tenant_id") diff --git a/src/Fengling.Member.Infrastructure/Migrations/20260209163416_ChangeMemberIdToGuid.Designer.cs b/src/Fengling.Member.Infrastructure/Migrations/20260209163416_ChangeMemberIdToGuid.Designer.cs new file mode 100644 index 0000000..34468d2 --- /dev/null +++ b/src/Fengling.Member.Infrastructure/Migrations/20260209163416_ChangeMemberIdToGuid.Designer.cs @@ -0,0 +1,596 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("FrozenPoints") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("frozen_points"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasColumnName("tenant_id"); + + b.Property("TotalPoints") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("points"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasColumnType("bigint"); + + b.Property("Points") + .HasColumnType("integer"); + + b.Property("PointsAccountId") + .HasColumnType("bigint"); + + b.Property("Remark") + .HasColumnType("text"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionTypeCategory") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("PointsAccountId"); + + b.ToTable("PointsTransactions"); + }); + + modelBuilder.Entity("Fengling.Member.Domain.Aggregates.PointsRuleModel.PointsRule", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasComment("规则标识"); + + b.Property("BasePoints") + .HasColumnType("integer") + .HasComment("基础积分"); + + b.Property("CalculationMode") + .HasColumnType("integer") + .HasComment("计算模式"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("规则编码"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("生效开始时间"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("生效结束时间"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否启用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasComment("规则名称"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("优先级"); + + b.Property("RuleType") + .HasColumnType("integer") + .HasComment("规则类型"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("ValidityDays") + .HasColumnType("integer") + .HasComment("有效期天数"); + + b.Property("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("Id") + .HasColumnType("uuid") + .HasComment("条件标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("DimensionType") + .HasColumnType("integer") + .HasComment("维度类型"); + + b.Property("DimensionValue") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasComment("维度值"); + + b.Property("Operator") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasComment("操作符"); + + b.Property("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("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("会员标识"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("OpenId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("open_id"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("phone_number"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasColumnName("tenant_id"); + + b.Property("UnionId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("union_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("MemberId") + .HasColumnType("uuid") + .HasColumnName("member_id"); + + b.Property("TagId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("tag_id"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasColumnType("text"); + + b.Property("AuthorizedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MemberId") + .HasColumnType("uuid"); + + b.Property("OpenId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("RefreshToken") + .HasColumnType("text"); + + b.Property("TokenExpiredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UnionId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MemberId"); + + b.ToTable("OAuthAuthorization"); + }); + + modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.WechatAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorizedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("authorized_at"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_login_at"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasColumnName("member_id"); + + b.Property("OpenId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("open_id"); + + b.Property("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("Key") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Instance") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastLockTime") + .HasColumnType("TIMESTAMP"); + + b.HasKey("Key"); + + b.ToTable("CAPLock", (string)null); + }); + + modelBuilder.Entity("NetCorePal.Extensions.DistributedTransactions.CAP.Persistence.PublishedMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Added") + .HasColumnType("TIMESTAMP"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Retries") + .HasColumnType("integer"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Added") + .HasColumnType("TIMESTAMP"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TIMESTAMP"); + + b.Property("Group") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Retries") + .HasColumnType("integer"); + + b.Property("StatusName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("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 + } + } +} diff --git a/src/Fengling.Member.Infrastructure/Migrations/20260209163416_ChangeMemberIdToGuid.cs b/src/Fengling.Member.Infrastructure/Migrations/20260209163416_ChangeMemberIdToGuid.cs new file mode 100644 index 0000000..45466ae --- /dev/null +++ b/src/Fengling.Member.Infrastructure/Migrations/20260209163416_ChangeMemberIdToGuid.cs @@ -0,0 +1,72 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Fengling.Member.Infrastructure.Migrations +{ + /// + public partial class ChangeMemberIdToGuid : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MemberId", + table: "OAuthAuthorization", + type: "uuid", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "member_id", + table: "fls_member_tag", + type: "uuid", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "id", + table: "fls_member", + type: "uuid", + nullable: false, + comment: "会员标识", + oldClrType: typeof(long), + oldType: "bigint") + .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "MemberId", + table: "OAuthAuthorization", + type: "bigint", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "member_id", + table: "fls_member_tag", + type: "bigint", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "id", + table: "fls_member", + type: "bigint", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldComment: "会员标识") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + } + } +} diff --git a/src/Fengling.Member.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Fengling.Member.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index c6ffc10..87d5861 100644 --- a/src/Fengling.Member.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Fengling.Member.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -233,12 +233,10 @@ namespace Fengling.Member.Infrastructure.Migrations modelBuilder.Entity("Fengling.Member.Domain.Aggregates.Users.MemberEntity", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id") + .HasComment("会员标识"); b.Property("CreatedAt") .HasColumnType("timestamp with time zone") @@ -310,8 +308,8 @@ namespace Fengling.Member.Infrastructure.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("created_at"); - b.Property("MemberId") - .HasColumnType("bigint") + b.Property("MemberId") + .HasColumnType("uuid") .HasColumnName("member_id"); b.Property("TagId") @@ -354,8 +352,8 @@ namespace Fengling.Member.Infrastructure.Migrations b.Property("LastLoginAt") .HasColumnType("timestamp with time zone"); - b.Property("MemberId") - .HasColumnType("bigint"); + b.Property("MemberId") + .HasColumnType("uuid"); b.Property("OpenId") .IsRequired() diff --git a/src/Fengling.Member.Infrastructure/Repositories/MemberRepository.cs b/src/Fengling.Member.Infrastructure/Repositories/MemberRepository.cs index 0c4d027..4c8501e 100644 --- a/src/Fengling.Member.Infrastructure/Repositories/MemberRepository.cs +++ b/src/Fengling.Member.Infrastructure/Repositories/MemberRepository.cs @@ -2,35 +2,51 @@ using Fengling.Member.Domain.Aggregates.Users; namespace Fengling.Member.Infrastructure.Repositories; -public interface IMemberRepository : IRepository +public interface IMemberRepository : IRepository { - Task GetByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default); - Task GetByOpenIdAsync(string openId, CancellationToken cancellationToken = default); - Task GetByUnionIdAsync(string unionId, CancellationToken cancellationToken = default); - Task> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default); - Task ExistsByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default); + Task GetByPhoneNumberAsync(long tenantId, string phoneNumber, + CancellationToken cancellationToken = default); + + Task GetByOpenIdAsync(string openId, + CancellationToken cancellationToken = default); + + Task GetByUnionIdAsync(string unionId, + CancellationToken cancellationToken = default); + + Task> GetByTenantIdAsync(long tenantId, + int page = 1, int pageSize = 20, CancellationToken cancellationToken = default); + + Task ExistsByPhoneNumberAsync(long tenantId, string phoneNumber, + CancellationToken cancellationToken = default); + Task ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default); } -public class MemberRepository(ApplicationDbContext context) : - RepositoryBase(context), IMemberRepository +public class MemberRepository(ApplicationDbContext context) + : RepositoryBase(context), + IMemberRepository { - public async Task GetByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default) + public async Task 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 GetByOpenIdAsync(string openId, CancellationToken cancellationToken = default) + public async Task GetByOpenIdAsync(string openId, + CancellationToken cancellationToken = default) { return await DbContext.Members.FirstOrDefaultAsync(m => m.OpenId == openId, cancellationToken); } - public async Task GetByUnionIdAsync(string unionId, CancellationToken cancellationToken = default) + public async Task GetByUnionIdAsync(string unionId, + CancellationToken cancellationToken = default) { return await DbContext.Members.FirstOrDefaultAsync(m => m.UnionId == unionId, cancellationToken); } - public async Task> GetByTenantIdAsync(long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default) + public async Task> GetByTenantIdAsync( + long tenantId, int page = 1, int pageSize = 20, CancellationToken cancellationToken = default) { return await DbContext.Members .Where(m => m.TenantId == tenantId) @@ -40,13 +56,15 @@ public class MemberRepository(ApplicationDbContext context) : .ToListAsync(cancellationToken); } - public async Task ExistsByPhoneNumberAsync(long tenantId, string phoneNumber, CancellationToken cancellationToken = default) + public async Task 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 ExistsByOpenIdAsync(string openId, CancellationToken cancellationToken = default) { return await DbContext.Members.AnyAsync(m => m.OpenId == openId, cancellationToken); } -} +} \ No newline at end of file diff --git a/src/Fengling.Member.Web/Endpoints/v1/AlipayBindingEndpoints.cs b/src/Fengling.Member.Web/Endpoints/v1/AlipayBindingEndpoints.cs index d019ccd..4985aa2 100644 --- a/src/Fengling.Member.Web/Endpoints/v1/AlipayBindingEndpoints.cs +++ b/src/Fengling.Member.Web/Endpoints/v1/AlipayBindingEndpoints.cs @@ -2,18 +2,13 @@ using FastEndpoints; using MediatR; using Microsoft.AspNetCore.Mvc; using Fengling.Member.Application.Commands.Member; +using Fengling.Member.Domain.Aggregates.Users; namespace Fengling.Member.Web.Endpoints.v1; -public class BindAlipayEndpoint : Endpoint +public class BindAlipayEndpoint(IMediator mediator) + : Endpoint { - private readonly IMediator _mediator; - - public BindAlipayEndpoint(IMediator mediator) - { - _mediator = mediator; - } - public override void Configure() { Post("/api/v1/members/{MemberId}/alipay"); @@ -26,37 +21,13 @@ public class BindAlipayEndpoint : Endpoint +public class RegisterMemberEndpoint(IMediator mediator) + : Endpoint { - private readonly IMediator _mediator; - - public RegisterMemberEndpoint(IMediator mediator) - { - _mediator = mediator; - } - public override void Configure() { Post("/api/v1/members"); @@ -26,44 +21,13 @@ public class RegisterMemberEndpoint : Endpoint +public class BindOAuthEndpoint(IMediator mediator) + : Endpoint { - private readonly IMediator _mediator; - - public BindOAuthEndpoint(IMediator mediator) - { - _mediator = mediator; - } - public override void Configure() { Post("/api/v1/members/{MemberId}/oauth"); @@ -26,42 +21,13 @@ public class BindOAuthEndpoint : Endpoint public override async Task HandleAsync(BindOAuthRequest req, CancellationToken ct) { - var command = new BindOAuthCommand - { - MemberId = req.MemberId, - Provider = req.Provider, - OpenId = req.OpenId, - UnionId = req.UnionId - }; + var command = new BindOAuthCommand(req.Provider, req.MemberId, req.OpenId, req.UnionId); - var result = await _mediator.Send(command, ct); + var result = await mediator.Send(command, ct); - Response = new BindOAuthResponse - { - MemberId = result.MemberId, - Provider = result.Provider, - OpenId = result.OpenId, - UnionId = result.UnionId, - BoundAt = result.BoundAt - }; + Response = new BindOAuthResponse(result.MemberId, result.Provider, result.OpenId, result.UnionId, result.BoundAt); } } -public class BindOAuthRequest -{ - [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; } -} +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); diff --git a/src/Fengling.Member.Web/Endpoints/v1/WechatBindingEndpoints.cs b/src/Fengling.Member.Web/Endpoints/v1/WechatBindingEndpoints.cs index d8c054c..feb2c8a 100644 --- a/src/Fengling.Member.Web/Endpoints/v1/WechatBindingEndpoints.cs +++ b/src/Fengling.Member.Web/Endpoints/v1/WechatBindingEndpoints.cs @@ -2,18 +2,13 @@ using FastEndpoints; using MediatR; using Microsoft.AspNetCore.Mvc; using Fengling.Member.Application.Commands.Member; +using Fengling.Member.Domain.Aggregates.Users; namespace Fengling.Member.Web.Endpoints.v1; -public class BindWechatEndpoint : Endpoint +public class BindWechatEndpoint(IMediator mediator) + : Endpoint { - private readonly IMediator _mediator; - - public BindWechatEndpoint(IMediator mediator) - { - _mediator = mediator; - } - public override void Configure() { Post("/api/v1/members/{MemberId}/wechat"); @@ -26,37 +21,13 @@ public class BindWechatEndpoint : Endpoint