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