diff --git a/src/Fengling.Member.Web/Endpoints/v1/MemberQueryEndpoints.cs b/src/Fengling.Member.Web/Endpoints/v1/MemberQueryEndpoints.cs new file mode 100644 index 0000000..4b43447 --- /dev/null +++ b/src/Fengling.Member.Web/Endpoints/v1/MemberQueryEndpoints.cs @@ -0,0 +1,445 @@ +using FastEndpoints; +using Fengling.Member.Domain.Aggregates.Users; +using Microsoft.AspNetCore.Mvc; +using Fengling.Member.Infrastructure; +using Microsoft.EntityFrameworkCore; + +namespace Fengling.Member.Web.Endpoints.v1; + +public class QueryMembersEndpoint : Endpoint +{ + private readonly ApplicationDbContext _dbContext; + + public QueryMembersEndpoint(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + public override void Configure() + { + Get("/api/v1/members"); + Summary(s => + { + s.Summary = "查询会员列表"; + s.Description = "分页查询会员列表,支持按手机号、状态筛选"; + }); + } + + public override async Task HandleAsync(QueryMembersRequest req, CancellationToken ct) + { + var query = _dbContext.Members + .Where(m => m.TenantId == req.TenantId); + + if (!string.IsNullOrWhiteSpace(req.PhoneNumber)) + { + query = query.Where(m => m.PhoneNumber != null && m.PhoneNumber.Contains(req.PhoneNumber)); + } + + if (!string.IsNullOrWhiteSpace(req.Status)) + { + query = query.Where(m => m.Status.Value == req.Status); + } + + if (!string.IsNullOrWhiteSpace(req.OpenId)) + { + query = query.Where(m => m.OpenId != null && m.OpenId.Contains(req.OpenId)); + } + + var totalCount = await query.CountAsync(ct); + var members = await query + .Include(m => m.Tags) + .OrderByDescending(m => m.CreatedAt) + .Skip((req.Page - 1) * req.PageSize) + .Take(req.PageSize) + .Select(m => new MemberDto( + Id: m.Id.Value, + TenantId: m.TenantId, + PhoneNumber: m.PhoneNumber ?? string.Empty, + OpenId: m.OpenId ?? string.Empty, + UnionId: m.UnionId, + Status: m.Status.Value, + StatusDesc: m.Status.Description, + Tags: m.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(), + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt + )) + .ToListAsync(ct); + + Response = new QueryMembersResponse( + Items: members, + Total: totalCount, + Page: req.Page, + PageSize: req.PageSize + ); + } +} + +public class QueryMembersRequest +{ + [QueryParam] + public long TenantId { get; set; } = 1; + + [QueryParam] + public string? PhoneNumber { get; set; } + + [QueryParam] + public string? Status { get; set; } + + [QueryParam] + public string? OpenId { get; set; } + + [QueryParam] + public int Page { get; set; } = 1; + + [QueryParam] + public int PageSize { get; set; } = 20; +} + +public record QueryMembersResponse( + List Items, + int Total, + int Page, + int PageSize +); + +public record MemberDto( + Guid Id, + long TenantId, + string PhoneNumber, + string OpenId, + string? UnionId, + string Status, + string StatusDesc, + List Tags, + DateTime CreatedAt, + DateTime? UpdatedAt +); + +public record MemberTagDto( + string TagId, + string TagName +); + +public class GetMemberEndpoint : Endpoint +{ + private readonly ApplicationDbContext _dbContext; + + public GetMemberEndpoint(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + public override void Configure() + { + Get("/api/v1/members/{Id}"); + Summary(s => + { + s.Summary = "获取会员详情"; + s.Description = "根据ID获取会员详细信息"; + }); + } + + public override async Task HandleAsync(GetMemberRequest req, CancellationToken ct) + { + var member = await _dbContext.Members + .Include(m => m.Tags) + .FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct); + + if (member == null) + { + ThrowError("会员不存在"); + } + + Response = new MemberDto( + Id: member.Id.Value, + TenantId: member.TenantId, + PhoneNumber: member.PhoneNumber ?? string.Empty, + OpenId: member.OpenId ?? string.Empty, + UnionId: member.UnionId, + Status: member.Status.Value, + StatusDesc: member.Status.Description, + Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(), + CreatedAt: member.CreatedAt, + UpdatedAt: member.UpdatedAt + ); + } +} + +public record GetMemberRequest([FromRoute] Guid Id); + +public class UpdateMemberEndpoint : Endpoint +{ + private readonly ApplicationDbContext _dbContext; + + public UpdateMemberEndpoint(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + public override void Configure() + { + Put("/api/v1/members/{Id}"); + Summary(s => + { + s.Summary = "更新会员"; + s.Description = "更新会员基本信息"; + }); + } + + public override async Task HandleAsync(UpdateMemberRequest req, CancellationToken ct) + { + var member = await _dbContext.Members + .Include(m => m.Tags) + .FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct); + + if (member == null) + { + ThrowError("会员不存在"); + } + + if (!string.IsNullOrWhiteSpace(req.PhoneNumber)) + { + try + { + member.BindPhoneNumber(req.PhoneNumber); + } + catch (ArgumentException ex) + { + AddError(ex.Message); + return; + } + } + + await _dbContext.SaveChangesAsync(ct); + + Response = new MemberDto( + Id: member.Id.Value, + TenantId: member.TenantId, + PhoneNumber: member.PhoneNumber ?? string.Empty, + OpenId: member.OpenId ?? string.Empty, + UnionId: member.UnionId, + Status: member.Status.Value, + StatusDesc: member.Status.Description, + Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(), + CreatedAt: member.CreatedAt, + UpdatedAt: member.UpdatedAt + ); + } +} + +public record UpdateMemberRequest( + [FromRoute] Guid Id, + string? PhoneNumber +); + +public class UpdateMemberStatusEndpoint : Endpoint +{ + private readonly ApplicationDbContext _dbContext; + + public UpdateMemberStatusEndpoint(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + public override void Configure() + { + Put("/api/v1/members/{Id}/status"); + Summary(s => + { + s.Summary = "更新会员状态"; + s.Description = "冻结/解冻/停用会员"; + }); + } + + public override async Task HandleAsync(UpdateMemberStatusRequest req, CancellationToken ct) + { + var member = await _dbContext.Members + .Include(m => m.Tags) + .FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct); + + if (member == null) + { + ThrowError("会员不存在"); + } + + switch (req.Action.ToLower()) + { + case "freeze": + member.Freeze(); + break; + case "unfreeze": + member.Unfreeze(); + break; + case "deactivate": + member.Deactivate(); + break; + default: + AddError("无效的操作类型"); + return; + } + + await _dbContext.SaveChangesAsync(ct); + + Response = new MemberDto( + Id: member.Id.Value, + TenantId: member.TenantId, + PhoneNumber: member.PhoneNumber ?? string.Empty, + OpenId: member.OpenId ?? string.Empty, + UnionId: member.UnionId, + Status: member.Status.Value, + StatusDesc: member.Status.Description, + Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(), + CreatedAt: member.CreatedAt, + UpdatedAt: member.UpdatedAt + ); + } +} + +public record UpdateMemberStatusRequest +{ + [FromRoute] + public Guid Id { get; set; } + public string Action { get; set; } = string.Empty; +} + +public class DeleteMemberEndpoint : Endpoint +{ + private readonly ApplicationDbContext _dbContext; + + public DeleteMemberEndpoint(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + public override void Configure() + { + Delete("/api/v1/members/{Id}"); + Summary(s => + { + s.Summary = "删除会员"; + s.Description = "软删除会员"; + }); + } + + public override async Task HandleAsync(DeleteMemberRequest req, CancellationToken ct) + { + var member = await _dbContext.Members + .FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct); + + if (member == null) + { + ThrowError("会员不存在"); + } + + member.Deactivate(); + await _dbContext.SaveChangesAsync(ct); + } +} + +public record DeleteMemberRequest([FromRoute] Guid Id); + +public class AddMemberTagEndpoint : Endpoint +{ + private readonly ApplicationDbContext _dbContext; + + public AddMemberTagEndpoint(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + public override void Configure() + { + Post("/api/v1/members/{Id}/tags"); + Summary(s => + { + s.Summary = "添加会员标签"; + s.Description = "为会员添加标签"; + }); + } + + public override async Task HandleAsync(AddMemberTagRequest req, CancellationToken ct) + { + var member = await _dbContext.Members + .Include(m => m.Tags) + .FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct); + + if (member == null) + { + ThrowError("会员不存在"); + } + + member.AddTag(req.TagId, req.TagName); + await _dbContext.SaveChangesAsync(ct); + + Response = new MemberDto( + Id: member.Id.Value, + TenantId: member.TenantId, + PhoneNumber: member.PhoneNumber ?? string.Empty, + OpenId: member.OpenId ?? string.Empty, + UnionId: member.UnionId, + Status: member.Status.Value, + StatusDesc: member.Status.Description, + Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(), + CreatedAt: member.CreatedAt, + UpdatedAt: member.UpdatedAt + ); + } +} + +public record AddMemberTagRequest +{ + [FromRoute] + public Guid Id { get; set; } + public string TagId { get; set; } = string.Empty; + public string? TagName { get; set; } +} + +public class RemoveMemberTagEndpoint : Endpoint +{ + private readonly ApplicationDbContext _dbContext; + + public RemoveMemberTagEndpoint(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } + + public override void Configure() + { + Delete("/api/v1/members/{Id}/tags/{TagId}"); + Summary(s => + { + s.Summary = "移除会员标签"; + s.Description = "移除会员的指定标签"; + }); + } + + public override async Task HandleAsync(RemoveMemberTagRequest req, CancellationToken ct) + { + var member = await _dbContext.Members + .Include(m => m.Tags) + .FirstOrDefaultAsync(m => m.Id.Value == req.Id, ct); + + if (member == null) + { + ThrowError("会员不存在"); + } + + member.RemoveTag(req.TagId); + await _dbContext.SaveChangesAsync(ct); + + Response = new MemberDto( + Id: member.Id.Value, + TenantId: member.TenantId, + PhoneNumber: member.PhoneNumber ?? string.Empty, + OpenId: member.OpenId ?? string.Empty, + UnionId: member.UnionId, + Status: member.Status.Value, + StatusDesc: member.Status.Description, + Tags: member.Tags.Select(t => new MemberTagDto(t.TagId, t.TagName ?? string.Empty)).ToList(), + CreatedAt: member.CreatedAt, + UpdatedAt: member.UpdatedAt + ); + } +} + +public record RemoveMemberTagRequest([FromRoute] Guid Id, [FromRoute] string TagId);