From d96ca4971a9804ceb372975dfe57f422b869b4ff Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Tue, 3 Mar 2026 20:38:31 +0800
Subject: [PATCH] feat(member): implement member center management module
---
TakeoutSaaS.Docs | 2 +-
.../Contracts/Member/MemberListContracts.cs | 346 +++++
.../Contracts/Member/MemberTierContracts.cs | 427 +++++++
.../Controllers/MemberController.cs | 251 ++++
.../Controllers/MemberTierController.cs | 330 +++++
.../Commands/DeleteMemberTierCommand.cs | 14 +
.../Commands/SaveMemberDaySettingCommand.cs | 25 +
.../Members/Commands/SaveMemberTagsCommand.cs | 19 +
.../Members/Commands/SaveMemberTierCommand.cs | 50 +
.../App/Members/Dto/MemberDtos.cs | 567 +++++++++
.../DeleteMemberTierCommandHandler.cs | 60 +
.../Handlers/ExportMemberCsvQueryHandler.cs | 87 ++
.../GetMemberDaySettingQueryHandler.cs | 42 +
.../Handlers/GetMemberDetailQueryHandler.cs | 84 ++
.../GetMemberListStatsQueryHandler.cs | 52 +
.../GetMemberTierDetailQueryHandler.cs | 70 ++
.../Handlers/GetMemberTierListQueryHandler.cs | 71 ++
.../Members/Handlers/MemberCenterSupport.cs | 1108 +++++++++++++++++
.../SaveMemberDaySettingCommandHandler.cs | 67 +
.../Handlers/SaveMemberTagsCommandHandler.cs | 47 +
.../Handlers/SaveMemberTierCommandHandler.cs | 202 +++
.../SearchMemberCouponPickerQueryHandler.cs | 55 +
.../Handlers/SearchMemberListQueryHandler.cs | 73 ++
.../Members/Queries/ExportMemberCsvQuery.cs | 25 +
.../Queries/GetMemberDaySettingQuery.cs | 11 +
.../Members/Queries/GetMemberDetailQuery.cs | 20 +
.../Queries/GetMemberListStatsQuery.cs | 25 +
.../Queries/GetMemberTierDetailQuery.cs | 15 +
.../Members/Queries/GetMemberTierListQuery.cs | 11 +
.../Queries/SearchMemberCouponPickerQuery.cs | 20 +
.../Members/Queries/SearchMemberListQuery.cs | 36 +
.../Membership/Entities/MemberDaySetting.cs | 24 +
.../Membership/Entities/MemberProfile.cs | 15 +
.../Membership/Entities/MemberProfileTag.cs | 19 +
.../Membership/Entities/MemberTier.cs | 35 +
.../Repositories/IMemberRepository.cs | 99 ++
.../AppServiceCollectionExtensions.cs | 2 +
.../App/Persistence/TakeoutAppDbContext.cs | 39 +
.../App/Repositories/EfMemberRepository.cs | 177 +++
.../20260303103000_AddMemberCenterModule.cs | 225 ++++
40 files changed, 4846 insertions(+), 1 deletion(-)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberListContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberTierContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/MemberController.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/MemberTierController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Commands/DeleteMemberTierCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Commands/SaveMemberDaySettingCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Commands/SaveMemberTagsCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Commands/SaveMemberTierCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Dto/MemberDtos.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/DeleteMemberTierCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/ExportMemberCsvQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/GetMemberDaySettingQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/GetMemberDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/GetMemberListStatsQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/GetMemberTierDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/GetMemberTierListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/MemberCenterSupport.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/SaveMemberDaySettingCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/SaveMemberTagsCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/SaveMemberTierCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/SearchMemberCouponPickerQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Handlers/SearchMemberListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/ExportMemberCsvQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/GetMemberDaySettingQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/GetMemberDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/GetMemberListStatsQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/GetMemberTierDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/GetMemberTierListQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/SearchMemberCouponPickerQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/Queries/SearchMemberListQuery.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberDaySetting.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberProfileTag.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Repositories/IMemberRepository.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfMemberRepository.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260303103000_AddMemberCenterModule.cs
diff --git a/TakeoutSaaS.Docs b/TakeoutSaaS.Docs
index d7d55e9..9006c8a 160000
--- a/TakeoutSaaS.Docs
+++ b/TakeoutSaaS.Docs
@@ -1 +1 @@
-Subproject commit d7d55e990836f82ef7de91af02ae2f3a3a02b71b
+Subproject commit 9006c8a58995d4adddab28599193c3631935ee43
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberListContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberListContracts.cs
new file mode 100644
index 0000000..f21cc4e
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberListContracts.cs
@@ -0,0 +1,346 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Member;
+
+///
+/// 会员列表筛选请求。
+///
+public class MemberListFilterRequest
+{
+ ///
+ /// 门店 ID(可选,未传表示当前商户全部可见门店)。
+ ///
+ public string? StoreId { get; set; }
+
+ ///
+ /// 关键词(姓名/手机号)。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 等级标识。
+ ///
+ public string? TierId { get; set; }
+}
+
+///
+/// 会员列表分页请求。
+///
+public sealed class MemberListRequest : MemberListFilterRequest
+{
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; } = 1;
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; } = 10;
+}
+
+///
+/// 会员详情请求。
+///
+public sealed class MemberDetailRequest
+{
+ ///
+ /// 门店 ID(可选,未传表示当前商户全部可见门店)。
+ ///
+ public string? StoreId { get; set; }
+
+ ///
+ /// 会员标识。
+ ///
+ public string MemberId { get; set; } = string.Empty;
+}
+
+///
+/// 保存会员标签请求。
+///
+public sealed class SaveMemberTagsRequest
+{
+ ///
+ /// 门店 ID(可选,未传表示当前商户全部可见门店)。
+ ///
+ public string? StoreId { get; set; }
+
+ ///
+ /// 会员标识。
+ ///
+ public string MemberId { get; set; } = string.Empty;
+
+ ///
+ /// 标签集合。
+ ///
+ public List Tags { get; set; } = [];
+}
+
+///
+/// 会员列表行响应。
+///
+public sealed class MemberListItemResponse
+{
+ ///
+ /// 会员标识。
+ ///
+ public string MemberId { get; set; } = string.Empty;
+
+ ///
+ /// 会员名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 头像文案。
+ ///
+ public string AvatarText { get; set; } = string.Empty;
+
+ ///
+ /// 头像颜色。
+ ///
+ public string AvatarColor { get; set; } = string.Empty;
+
+ ///
+ /// 手机号(脱敏)。
+ ///
+ public string MobileMasked { get; set; } = string.Empty;
+
+ ///
+ /// 会员等级标识。
+ ///
+ public string? TierId { get; set; }
+
+ ///
+ /// 会员等级名称。
+ ///
+ public string TierName { get; set; } = string.Empty;
+
+ ///
+ /// 等级主题色。
+ ///
+ public string TierColorHex { get; set; } = string.Empty;
+
+ ///
+ /// 累计消费。
+ ///
+ public decimal TotalAmount { get; set; }
+
+ ///
+ /// 消费次数。
+ ///
+ public int OrderCount { get; set; }
+
+ ///
+ /// 最近消费时间(yyyy-MM-dd)。
+ ///
+ public string LastOrderAt { get; set; } = string.Empty;
+
+ ///
+ /// 储值余额。
+ ///
+ public decimal StoredBalance { get; set; }
+
+ ///
+ /// 积分余额。
+ ///
+ public int PointsBalance { get; set; }
+
+ ///
+ /// 是否沉睡会员。
+ ///
+ public bool IsDormant { get; set; }
+}
+
+///
+/// 会员列表响应。
+///
+public sealed class MemberListResultResponse
+{
+ ///
+ /// 列表项。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 总数。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+}
+
+///
+/// 会员列表统计响应。
+///
+public sealed class MemberListStatsResponse
+{
+ ///
+ /// 会员总数。
+ ///
+ public int TotalMembers { get; set; }
+
+ ///
+ /// 本月新增会员数。
+ ///
+ public int MonthlyNewMembers { get; set; }
+
+ ///
+ /// 活跃会员数。
+ ///
+ public int ActiveMembers { get; set; }
+
+ ///
+ /// 沉睡会员数。
+ ///
+ public int DormantMembers { get; set; }
+}
+
+///
+/// 会员最近订单响应。
+///
+public sealed class MemberRecentOrderResponse
+{
+ ///
+ /// 下单日期(yyyy-MM-dd)。
+ ///
+ public string OrderedAt { get; set; } = string.Empty;
+
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; set; } = string.Empty;
+
+ ///
+ /// 订单金额。
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// 订单状态文案。
+ ///
+ public string StatusText { get; set; } = string.Empty;
+}
+
+///
+/// 会员详情响应。
+///
+public sealed class MemberDetailResponse
+{
+ ///
+ /// 会员标识。
+ ///
+ public string MemberId { get; set; } = string.Empty;
+
+ ///
+ /// 会员名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 头像文案。
+ ///
+ public string AvatarText { get; set; } = string.Empty;
+
+ ///
+ /// 头像颜色。
+ ///
+ public string AvatarColor { get; set; } = string.Empty;
+
+ ///
+ /// 手机号(脱敏)。
+ ///
+ public string MobileMasked { get; set; } = string.Empty;
+
+ ///
+ /// 注册时间(yyyy-MM-dd)。
+ ///
+ public string JoinedAt { get; set; } = string.Empty;
+
+ ///
+ /// 会员等级标识。
+ ///
+ public string? TierId { get; set; }
+
+ ///
+ /// 会员等级名称。
+ ///
+ public string TierName { get; set; } = string.Empty;
+
+ ///
+ /// 等级主题色。
+ ///
+ public string TierColorHex { get; set; } = string.Empty;
+
+ ///
+ /// 累计消费。
+ ///
+ public decimal TotalAmount { get; set; }
+
+ ///
+ /// 消费次数。
+ ///
+ public int OrderCount { get; set; }
+
+ ///
+ /// 平均客单价。
+ ///
+ public decimal AverageAmount { get; set; }
+
+ ///
+ /// 储值余额。
+ ///
+ public decimal StoredBalance { get; set; }
+
+ ///
+ /// 储值实充余额。
+ ///
+ public decimal StoredRechargeBalance { get; set; }
+
+ ///
+ /// 储值赠金余额。
+ ///
+ public decimal StoredGiftBalance { get; set; }
+
+ ///
+ /// 积分余额。
+ ///
+ public int PointsBalance { get; set; }
+
+ ///
+ /// 会员标签。
+ ///
+ public List Tags { get; set; } = [];
+
+ ///
+ /// 最近订单。
+ ///
+ public List RecentOrders { get; set; } = [];
+}
+
+///
+/// 会员导出响应。
+///
+public sealed class MemberExportResponse
+{
+ ///
+ /// 文件名。
+ ///
+ public string FileName { get; set; } = string.Empty;
+
+ ///
+ /// 文件内容 Base64。
+ ///
+ public string FileContentBase64 { get; set; } = string.Empty;
+
+ ///
+ /// 导出总数。
+ ///
+ public int TotalCount { get; set; }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberTierContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberTierContracts.cs
new file mode 100644
index 0000000..e8698ef
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberTierContracts.cs
@@ -0,0 +1,427 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Member;
+
+///
+/// 会员等级列表项响应。
+///
+public sealed class MemberTierListItemResponse
+{
+ ///
+ /// 等级标识。
+ ///
+ public string TierId { get; set; } = string.Empty;
+
+ ///
+ /// 排序序号。
+ ///
+ public int SortOrder { get; set; }
+
+ ///
+ /// 等级名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 图标键。
+ ///
+ public string IconKey { get; set; } = string.Empty;
+
+ ///
+ /// 主题色。
+ ///
+ public string ColorHex { get; set; } = string.Empty;
+
+ ///
+ /// 升级条件文案。
+ ///
+ public string ConditionText { get; set; } = string.Empty;
+
+ ///
+ /// 权益摘要。
+ ///
+ public List Perks { get; set; } = [];
+
+ ///
+ /// 等级会员数。
+ ///
+ public int MemberCount { get; set; }
+
+ ///
+ /// 是否默认等级。
+ ///
+ public bool IsDefault { get; set; }
+
+ ///
+ /// 是否可删除。
+ ///
+ public bool CanDelete { get; set; }
+}
+
+///
+/// 等级详情查询请求。
+///
+public sealed class MemberTierDetailRequest
+{
+ ///
+ /// 等级标识。
+ ///
+ public string? TierId { get; set; }
+}
+
+///
+/// 等级规则响应。
+///
+public sealed class MemberTierRuleResponse
+{
+ ///
+ /// 升级规则类型。
+ ///
+ public string UpgradeRuleType { get; set; } = "none";
+
+ ///
+ /// 升级累计消费门槛。
+ ///
+ public decimal? UpgradeAmountThreshold { get; set; }
+
+ ///
+ /// 升级消费次数门槛。
+ ///
+ public int? UpgradeOrderCountThreshold { get; set; }
+
+ ///
+ /// 降级观察窗口天数。
+ ///
+ public int DowngradeWindowDays { get; set; }
+}
+
+///
+/// 折扣权益响应。
+///
+public sealed class MemberTierDiscountBenefitResponse
+{
+ ///
+ /// 是否启用。
+ ///
+ public bool Enabled { get; set; }
+
+ ///
+ /// 折扣值。
+ ///
+ public decimal? DiscountRate { get; set; }
+}
+
+///
+/// 积分倍率权益响应。
+///
+public sealed class MemberTierPointMultiplierBenefitResponse
+{
+ ///
+ /// 是否启用。
+ ///
+ public bool Enabled { get; set; }
+
+ ///
+ /// 倍率。
+ ///
+ public decimal? Multiplier { get; set; }
+}
+
+///
+/// 生日特权响应。
+///
+public sealed class MemberTierBirthdayBenefitResponse
+{
+ ///
+ /// 是否启用。
+ ///
+ public bool Enabled { get; set; }
+
+ ///
+ /// 是否双倍积分。
+ ///
+ public bool DoublePointsEnabled { get; set; }
+
+ ///
+ /// 券模板 ID。
+ ///
+ public List CouponTemplateIds { get; set; } = [];
+}
+
+///
+/// 每月赠券响应。
+///
+public sealed class MemberTierMonthlyCouponBenefitResponse
+{
+ ///
+ /// 是否启用。
+ ///
+ public bool Enabled { get; set; }
+
+ ///
+ /// 每月发放日。
+ ///
+ public int GrantDay { get; set; }
+
+ ///
+ /// 券模板 ID。
+ ///
+ public List CouponTemplateIds { get; set; } = [];
+}
+
+///
+/// 免配送费权益响应。
+///
+public sealed class MemberTierFreeDeliveryBenefitResponse
+{
+ ///
+ /// 是否启用。
+ ///
+ public bool Enabled { get; set; }
+
+ ///
+ /// 每月免配送费次数。
+ ///
+ public int MonthlyFreeTimes { get; set; }
+}
+
+///
+/// 等级权益响应。
+///
+public sealed class MemberTierBenefitsResponse
+{
+ ///
+ /// 折扣权益。
+ ///
+ public MemberTierDiscountBenefitResponse Discount { get; set; } = new();
+
+ ///
+ /// 积分倍率权益。
+ ///
+ public MemberTierPointMultiplierBenefitResponse PointMultiplier { get; set; } = new();
+
+ ///
+ /// 生日特权。
+ ///
+ public MemberTierBirthdayBenefitResponse Birthday { get; set; } = new();
+
+ ///
+ /// 每月赠券。
+ ///
+ public MemberTierMonthlyCouponBenefitResponse MonthlyCoupon { get; set; } = new();
+
+ ///
+ /// 免配送费权益。
+ ///
+ public MemberTierFreeDeliveryBenefitResponse FreeDelivery { get; set; } = new();
+
+ ///
+ /// 优先配送。
+ ///
+ public bool PriorityDeliveryEnabled { get; set; }
+
+ ///
+ /// 专属客服。
+ ///
+ public bool ExclusiveServiceEnabled { get; set; }
+}
+
+///
+/// 等级详情响应。
+///
+public sealed class MemberTierDetailResponse
+{
+ ///
+ /// 等级标识。
+ ///
+ public string? TierId { get; set; }
+
+ ///
+ /// 排序序号。
+ ///
+ public int SortOrder { get; set; }
+
+ ///
+ /// 等级名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 图标键。
+ ///
+ public string IconKey { get; set; } = string.Empty;
+
+ ///
+ /// 主题色。
+ ///
+ public string ColorHex { get; set; } = string.Empty;
+
+ ///
+ /// 是否默认等级。
+ ///
+ public bool IsDefault { get; set; }
+
+ ///
+ /// 升降级规则。
+ ///
+ public MemberTierRuleResponse Rule { get; set; } = new();
+
+ ///
+ /// 等级权益。
+ ///
+ public MemberTierBenefitsResponse Benefits { get; set; } = new();
+
+ ///
+ /// 是否可删除。
+ ///
+ public bool CanDelete { get; set; }
+}
+
+///
+/// 保存等级请求。
+///
+public sealed class SaveMemberTierRequest
+{
+ ///
+ /// 等级标识(为空时新增)。
+ ///
+ public string? TierId { get; set; }
+
+ ///
+ /// 排序序号。
+ ///
+ public int SortOrder { get; set; }
+
+ ///
+ /// 等级名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 图标键。
+ ///
+ public string IconKey { get; set; } = "user";
+
+ ///
+ /// 主题色。
+ ///
+ public string ColorHex { get; set; } = "#999999";
+
+ ///
+ /// 是否默认等级。
+ ///
+ public bool IsDefault { get; set; }
+
+ ///
+ /// 升降级规则。
+ ///
+ public MemberTierRuleResponse Rule { get; set; } = new();
+
+ ///
+ /// 等级权益。
+ ///
+ public MemberTierBenefitsResponse Benefits { get; set; } = new();
+}
+
+///
+/// 删除等级请求。
+///
+public sealed class DeleteMemberTierRequest
+{
+ ///
+ /// 等级标识。
+ ///
+ public string TierId { get; set; } = string.Empty;
+}
+
+///
+/// 会员日配置响应。
+///
+public sealed class MemberDaySettingResponse
+{
+ ///
+ /// 是否启用会员日。
+ ///
+ public bool IsEnabled { get; set; }
+
+ ///
+ /// 周几(1-7,对应周一到周日)。
+ ///
+ public int Weekday { get; set; }
+
+ ///
+ /// 会员日额外折扣。
+ ///
+ public decimal ExtraDiscountRate { get; set; }
+}
+
+///
+/// 保存会员日配置请求。
+///
+public sealed class SaveMemberDaySettingRequest
+{
+ ///
+ /// 是否启用会员日。
+ ///
+ public bool IsEnabled { get; set; }
+
+ ///
+ /// 周几(1-7,对应周一到周日)。
+ ///
+ public int Weekday { get; set; }
+
+ ///
+ /// 会员日额外折扣。
+ ///
+ public decimal ExtraDiscountRate { get; set; }
+}
+
+///
+/// 优惠券选择器请求。
+///
+public sealed class MemberCouponPickerRequest
+{
+ ///
+ /// 门店 ID(可选)。
+ ///
+ public string? StoreId { get; set; }
+
+ ///
+ /// 关键词。
+ ///
+ public string? Keyword { get; set; }
+}
+
+///
+/// 优惠券选择器项响应。
+///
+public sealed class MemberCouponPickerItemResponse
+{
+ ///
+ /// 券模板标识。
+ ///
+ public string CouponTemplateId { get; set; } = string.Empty;
+
+ ///
+ /// 券名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 券类型。
+ ///
+ public string CouponType { get; set; } = string.Empty;
+
+ ///
+ /// 面值或折扣值。
+ ///
+ public decimal Value { get; set; }
+
+ ///
+ /// 最低消费门槛。
+ ///
+ public decimal? MinimumSpend { get; set; }
+
+ ///
+ /// 展示文案。
+ ///
+ public string DisplayText { get; set; } = string.Empty;
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/MemberController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MemberController.cs
new file mode 100644
index 0000000..23184ed
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MemberController.cs
@@ -0,0 +1,251 @@
+using System.Globalization;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using TakeoutSaaS.Application.App.Members.Commands;
+using TakeoutSaaS.Application.App.Members.Dto;
+using TakeoutSaaS.Application.App.Members.Queries;
+using TakeoutSaaS.Application.App.Stores.Services;
+using TakeoutSaaS.Infrastructure.App.Persistence;
+using TakeoutSaaS.Module.Authorization.Attributes;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Web.Api;
+using TakeoutSaaS.TenantApi.Contracts.Member;
+
+namespace TakeoutSaaS.TenantApi.Controllers;
+
+///
+/// 会员管理列表与详情。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/tenant/v{version:apiVersion}/member/list")]
+public sealed class MemberController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService)
+ : BaseApiController
+{
+ private const string ViewPermission = "tenant:member:view";
+ private const string ManagePermission = "tenant:member:manage";
+
+ ///
+ /// 获取会员列表。
+ ///
+ [HttpGet("list")]
+ [PermissionAuthorize(ViewPermission, ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> List(
+ [FromQuery] MemberListRequest request,
+ CancellationToken cancellationToken)
+ {
+ var visibleStoreIds = await ResolveVisibleStoreIdsAsync(request.StoreId, cancellationToken);
+
+ var result = await mediator.Send(new SearchMemberListQuery
+ {
+ VisibleStoreIds = visibleStoreIds,
+ Keyword = request.Keyword,
+ TierId = StoreApiHelpers.ParseSnowflakeOrNull(request.TierId),
+ Page = Math.Max(1, request.Page),
+ PageSize = Math.Clamp(request.PageSize, 1, 200)
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new MemberListResultResponse
+ {
+ Items = result.Items.Select(MapListItem).ToList(),
+ Total = result.TotalCount,
+ Page = result.Page,
+ PageSize = result.PageSize
+ });
+ }
+
+ ///
+ /// 获取会员列表统计。
+ ///
+ [HttpGet("stats")]
+ [PermissionAuthorize(ViewPermission, ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Stats(
+ [FromQuery] MemberListFilterRequest request,
+ CancellationToken cancellationToken)
+ {
+ var visibleStoreIds = await ResolveVisibleStoreIdsAsync(request.StoreId, cancellationToken);
+
+ var result = await mediator.Send(new GetMemberListStatsQuery
+ {
+ VisibleStoreIds = visibleStoreIds,
+ Keyword = request.Keyword,
+ TierId = StoreApiHelpers.ParseSnowflakeOrNull(request.TierId)
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new MemberListStatsResponse
+ {
+ TotalMembers = result.TotalMembers,
+ MonthlyNewMembers = result.MonthlyNewMembers,
+ ActiveMembers = result.ActiveMembers,
+ DormantMembers = result.DormantMembers
+ });
+ }
+
+ ///
+ /// 获取会员详情。
+ ///
+ [HttpGet("detail")]
+ [PermissionAuthorize(ViewPermission, ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Detail(
+ [FromQuery] MemberDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ var visibleStoreIds = await ResolveVisibleStoreIdsAsync(request.StoreId, cancellationToken);
+
+ var result = await mediator.Send(new GetMemberDetailQuery
+ {
+ VisibleStoreIds = visibleStoreIds,
+ MemberId = StoreApiHelpers.ParseRequiredSnowflake(request.MemberId, nameof(request.MemberId))
+ }, cancellationToken);
+
+ if (result is null)
+ {
+ return ApiResponse.Error(ErrorCodes.NotFound, "会员不存在");
+ }
+
+ return ApiResponse.Ok(MapDetail(result));
+ }
+
+ ///
+ /// 保存会员标签。
+ ///
+ [HttpPost("tags")]
+ [PermissionAuthorize(ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse