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:
parent
73f84a1237
commit
daf8bc9e24
445
src/Fengling.Member.Web/Endpoints/v1/MemberQueryEndpoints.cs
Normal file
445
src/Fengling.Member.Web/Endpoints/v1/MemberQueryEndpoints.cs
Normal 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);
|
||||
Loading…
Reference in New Issue
Block a user