feat(member): add member management module with frontend and backend

- Add MemberQueryEndpoints with CRUD operations and status management
- Add frontend member management pages (list, detail, points, tags)
- Add frontend routes and API client for member module
- Move completed docs to docs/completed/
This commit is contained in:
movingsam 2026-02-17 16:29:34 +08:00
parent 73f84a1237
commit daf8bc9e24

View File

@ -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<QueryMembersRequest, QueryMembersResponse>
{
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<MemberDto> 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<MemberTagDto> Tags,
DateTime CreatedAt,
DateTime? UpdatedAt
);
public record MemberTagDto(
string TagId,
string TagName
);
public class GetMemberEndpoint : Endpoint<GetMemberRequest, MemberDto>
{
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<UpdateMemberRequest, MemberDto>
{
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<UpdateMemberStatusRequest, MemberDto>
{
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<DeleteMemberRequest>
{
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<AddMemberTagRequest, MemberDto>
{
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<RemoveMemberTagRequest, MemberDto>
{
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);