From bd418c5927cdf12c44472d38c040d6cd9dc53b67 Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Wed, 4 Mar 2026 12:15:18 +0800
Subject: [PATCH] feat(member): implement points mall backend module
---
.../Member/MemberPointsMallContracts.cs | 808 ++++++++++++++++++
.../Controllers/MemberPointsMallController.cs | 526 ++++++++++++
.../ChangePointMallProductStatusCommand.cs | 25 +
.../Commands/DeletePointMallProductCommand.cs | 19 +
.../Commands/SavePointMallProductCommand.cs | 95 ++
.../Commands/SavePointMallRuleCommand.cs | 65 ++
.../Commands/VerifyPointMallRecordCommand.cs | 30 +
.../Commands/WritePointMallRecordCommand.cs | 30 +
.../Dto/MemberPointMallProductDto.cs | 107 +++
.../MemberPointMallProductListResultDto.cs | 12 +
.../Dto/MemberPointMallRecordDetailDto.cs | 22 +
.../Dto/MemberPointMallRecordDto.cs | 82 ++
.../Dto/MemberPointMallRecordExportDto.cs | 22 +
.../Dto/MemberPointMallRecordListResultDto.cs | 32 +
.../Dto/MemberPointMallRecordStatsDto.cs | 22 +
.../Dto/MemberPointMallRuleDetailResultDto.cs | 17 +
.../PointsMall/Dto/MemberPointMallRuleDto.cs | 62 ++
.../Dto/MemberPointMallRuleStatsDto.cs | 27 +
...ngePointMallProductStatusCommandHandler.cs | 50 ++
.../DeletePointMallProductCommandHandler.cs | 44 +
.../ExportPointMallRecordCsvQueryHandler.cs | 83 ++
.../GetPointMallProductDetailQueryHandler.cs | 45 +
.../GetPointMallProductListQueryHandler.cs | 55 ++
.../GetPointMallRecordDetailQueryHandler.cs | 35 +
.../GetPointMallRecordListQueryHandler.cs | 59 ++
.../GetPointMallRuleDetailQueryHandler.cs | 53 ++
.../SavePointMallProductCommandHandler.cs | 147 ++++
.../SavePointMallRuleCommandHandler.cs | 75 ++
.../VerifyPointMallRecordCommandHandler.cs | 58 ++
.../WritePointMallRecordCommandHandler.cs | 118 +++
.../PointsMall/MemberPointMallDtoFactory.cs | 203 +++++
.../PointsMall/MemberPointMallMapping.cs | 583 +++++++++++++
.../Queries/ExportPointMallRecordCsvQuery.cs | 40 +
.../Queries/GetPointMallProductDetailQuery.cs | 20 +
.../Queries/GetPointMallProductListQuery.cs | 25 +
.../Queries/GetPointMallRecordDetailQuery.cs | 20 +
.../Queries/GetPointMallRecordListQuery.cs | 50 ++
.../Queries/GetPointMallRuleDetailQuery.cs | 15 +
.../Entities/MemberPointMallProduct.cs | 95 ++
.../Entities/MemberPointMallRecord.cs | 100 +++
.../Entities/MemberPointMallRule.cs | 65 ++
.../Enums/MemberPointMallExchangeType.cs | 17 +
.../Enums/MemberPointMallExpiryMode.cs | 17 +
.../Enums/MemberPointMallNotifyChannel.cs | 17 +
.../Enums/MemberPointMallPickupMethod.cs | 17 +
.../Enums/MemberPointMallProductStatus.cs | 17 +
.../Enums/MemberPointMallRecordStatus.cs | 27 +
.../Enums/MemberPointMallRedeemType.cs | 22 +
.../Enums/MemberPointMallVerifyMethod.cs | 17 +
.../Repositories/IPointMallRepository.cs | 245 ++++++
.../App/Persistence/TakeoutAppDbContext.cs | 90 ++
.../App/Repositories/EfPointMallRepository.cs | 479 +++++++++++
...0260304153000_AddMemberPointsMallModule.cs | 187 ++++
53 files changed, 5193 insertions(+)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberPointsMallContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/MemberPointsMallController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Commands/ChangePointMallProductStatusCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Commands/DeletePointMallProductCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Commands/SavePointMallProductCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Commands/SavePointMallRuleCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Commands/VerifyPointMallRecordCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Commands/WritePointMallRecordCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallProductDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallProductListResultDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRecordDetailDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRecordDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRecordExportDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRecordListResultDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRecordStatsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRuleDetailResultDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRuleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Dto/MemberPointMallRuleStatsDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/ChangePointMallProductStatusCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/DeletePointMallProductCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/ExportPointMallRecordCsvQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/GetPointMallProductDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/GetPointMallProductListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/GetPointMallRecordDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/GetPointMallRecordListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/GetPointMallRuleDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/SavePointMallProductCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/SavePointMallRuleCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/VerifyPointMallRecordCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Handlers/WritePointMallRecordCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/MemberPointMallDtoFactory.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/MemberPointMallMapping.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Queries/ExportPointMallRecordCsvQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Queries/GetPointMallProductDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Queries/GetPointMallProductListQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Queries/GetPointMallRecordDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Queries/GetPointMallRecordListQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Members/PointsMall/Queries/GetPointMallRuleDetailQuery.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointMallProduct.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointMallRecord.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Entities/MemberPointMallRule.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallExchangeType.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallExpiryMode.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallNotifyChannel.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallPickupMethod.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallProductStatus.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallRecordStatus.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallRedeemType.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Enums/MemberPointMallVerifyMethod.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Membership/Repositories/IPointMallRepository.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfPointMallRepository.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304153000_AddMemberPointsMallModule.cs
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberPointsMallContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberPointsMallContracts.cs
new file mode 100644
index 0000000..8d8f0f3
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Member/MemberPointsMallContracts.cs
@@ -0,0 +1,808 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Member;
+
+///
+/// 积分商城规则详情查询请求。
+///
+public sealed class PointMallRuleDetailRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+}
+
+///
+/// 保存积分商城规则请求。
+///
+public sealed class SavePointMallRuleRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 是否启用消费获取。
+ ///
+ public bool IsConsumeRewardEnabled { get; set; }
+
+ ///
+ /// 每消费多少元触发一次积分计算。
+ ///
+ public int ConsumeAmountPerStep { get; set; }
+
+ ///
+ /// 每步获得积分。
+ ///
+ public int ConsumeRewardPointsPerStep { get; set; }
+
+ ///
+ /// 是否启用评价奖励。
+ ///
+ public bool IsReviewRewardEnabled { get; set; }
+
+ ///
+ /// 评价奖励积分。
+ ///
+ public int ReviewRewardPoints { get; set; }
+
+ ///
+ /// 是否启用注册奖励。
+ ///
+ public bool IsRegisterRewardEnabled { get; set; }
+
+ ///
+ /// 注册奖励积分。
+ ///
+ public int RegisterRewardPoints { get; set; }
+
+ ///
+ /// 是否启用签到奖励。
+ ///
+ public bool IsSigninRewardEnabled { get; set; }
+
+ ///
+ /// 签到奖励积分。
+ ///
+ public int SigninRewardPoints { get; set; }
+
+ ///
+ /// 有效期模式(permanent/yearly_clear)。
+ ///
+ public string ExpiryMode { get; set; } = "yearly_clear";
+}
+
+///
+/// 积分商城商品列表查询请求。
+///
+public sealed class PointMallProductListRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 状态(enabled/disabled,可空)。
+ ///
+ public string? Status { get; set; }
+
+ ///
+ /// 关键字。
+ ///
+ public string? Keyword { get; set; }
+}
+
+///
+/// 积分商城商品详情查询请求。
+///
+public sealed class PointMallProductDetailRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 积分商城商品 ID。
+ ///
+ public string PointMallProductId { get; set; } = string.Empty;
+}
+
+///
+/// 保存积分商城商品请求。
+///
+public sealed class SavePointMallProductRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 积分商城商品 ID(编辑时传)。
+ ///
+ public string? PointMallProductId { get; set; }
+
+ ///
+ /// 展示名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 展示图片。
+ ///
+ public string? ImageUrl { get; set; }
+
+ ///
+ /// 兑换类型(product/coupon/physical)。
+ ///
+ public string RedeemType { get; set; } = "product";
+
+ ///
+ /// 关联商品 ID。
+ ///
+ public string? ProductId { get; set; }
+
+ ///
+ /// 关联优惠券模板 ID。
+ ///
+ public string? CouponTemplateId { get; set; }
+
+ ///
+ /// 实物名称。
+ ///
+ public string? PhysicalName { get; set; }
+
+ ///
+ /// 领取方式(store_pickup/delivery)。
+ ///
+ public string? PickupMethod { get; set; }
+
+ ///
+ /// 商品描述。
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// 兑换方式(points/mixed)。
+ ///
+ public string ExchangeType { get; set; } = "points";
+
+ ///
+ /// 所需积分。
+ ///
+ public int RequiredPoints { get; set; }
+
+ ///
+ /// 现金部分。
+ ///
+ public decimal CashAmount { get; set; }
+
+ ///
+ /// 库存总量。
+ ///
+ public int StockTotal { get; set; }
+
+ ///
+ /// 每人限兑次数。
+ ///
+ public int? PerMemberLimit { get; set; }
+
+ ///
+ /// 通知渠道(in_app/sms)。
+ ///
+ public List NotifyChannels { get; set; } = [];
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+}
+
+///
+/// 修改积分商城商品状态请求。
+///
+public sealed class ChangePointMallProductStatusRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 积分商城商品 ID。
+ ///
+ public string PointMallProductId { get; set; } = string.Empty;
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "disabled";
+}
+
+///
+/// 删除积分商城商品请求。
+///
+public sealed class DeletePointMallProductRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 积分商城商品 ID。
+ ///
+ public string PointMallProductId { get; set; } = string.Empty;
+}
+
+///
+/// 积分商城兑换记录分页查询请求。
+///
+public sealed class PointMallRecordListRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 兑换类型(product/coupon/physical)。
+ ///
+ public string? RedeemType { get; set; }
+
+ ///
+ /// 状态(pending_pickup/issued/completed/canceled)。
+ ///
+ public string? Status { get; set; }
+
+ ///
+ /// 开始日期(yyyy-MM-dd)。
+ ///
+ public string? StartDate { get; set; }
+
+ ///
+ /// 结束日期(yyyy-MM-dd)。
+ ///
+ public string? EndDate { get; set; }
+
+ ///
+ /// 关键字。
+ ///
+ public string? Keyword { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; } = 1;
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; } = 10;
+}
+
+///
+/// 积分商城兑换记录详情请求。
+///
+public sealed class PointMallRecordDetailRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 兑换记录 ID。
+ ///
+ public string RecordId { get; set; } = string.Empty;
+}
+
+///
+/// 导出积分商城兑换记录请求。
+///
+public sealed class ExportPointMallRecordRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 兑换类型(product/coupon/physical)。
+ ///
+ public string? RedeemType { get; set; }
+
+ ///
+ /// 状态(pending_pickup/issued/completed/canceled)。
+ ///
+ public string? Status { get; set; }
+
+ ///
+ /// 开始日期(yyyy-MM-dd)。
+ ///
+ public string? StartDate { get; set; }
+
+ ///
+ /// 结束日期(yyyy-MM-dd)。
+ ///
+ public string? EndDate { get; set; }
+
+ ///
+ /// 关键字。
+ ///
+ public string? Keyword { get; set; }
+}
+
+///
+/// 写入积分商城兑换记录请求。
+///
+public sealed class WritePointMallRecordRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 积分商城商品 ID。
+ ///
+ public string PointMallProductId { get; set; } = string.Empty;
+
+ ///
+ /// 会员 ID。
+ ///
+ public string MemberId { get; set; } = string.Empty;
+
+ ///
+ /// 兑换时间(可空,默认当前时间)。
+ ///
+ public DateTime? RedeemedAt { get; set; }
+}
+
+///
+/// 核销积分商城兑换记录请求。
+///
+public sealed class VerifyPointMallRecordRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 兑换记录 ID。
+ ///
+ public string RecordId { get; set; } = string.Empty;
+
+ ///
+ /// 核销方式(scan/manual)。
+ ///
+ public string VerifyMethod { get; set; } = "manual";
+
+ ///
+ /// 核销备注。
+ ///
+ public string? VerifyRemark { get; set; }
+}
+
+///
+/// 积分商城规则响应。
+///
+public sealed class PointMallRuleResponse
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 是否启用消费获取。
+ ///
+ public bool IsConsumeRewardEnabled { get; set; }
+
+ ///
+ /// 每消费多少元触发一次积分计算。
+ ///
+ public int ConsumeAmountPerStep { get; set; }
+
+ ///
+ /// 每步获得积分。
+ ///
+ public int ConsumeRewardPointsPerStep { get; set; }
+
+ ///
+ /// 是否启用评价奖励。
+ ///
+ public bool IsReviewRewardEnabled { get; set; }
+
+ ///
+ /// 评价奖励积分。
+ ///
+ public int ReviewRewardPoints { get; set; }
+
+ ///
+ /// 是否启用注册奖励。
+ ///
+ public bool IsRegisterRewardEnabled { get; set; }
+
+ ///
+ /// 注册奖励积分。
+ ///
+ public int RegisterRewardPoints { get; set; }
+
+ ///
+ /// 是否启用签到奖励。
+ ///
+ public bool IsSigninRewardEnabled { get; set; }
+
+ ///
+ /// 签到奖励积分。
+ ///
+ public int SigninRewardPoints { get; set; }
+
+ ///
+ /// 有效期模式(permanent/yearly_clear)。
+ ///
+ public string ExpiryMode { get; set; } = "yearly_clear";
+}
+
+///
+/// 积分商城规则统计响应。
+///
+public sealed class PointMallRuleStatsResponse
+{
+ ///
+ /// 累计发放积分。
+ ///
+ public int TotalIssuedPoints { get; set; }
+
+ ///
+ /// 已兑换积分。
+ ///
+ public int RedeemedPoints { get; set; }
+
+ ///
+ /// 积分用户。
+ ///
+ public int PointMembers { get; set; }
+
+ ///
+ /// 兑换率(0-100)。
+ ///
+ public decimal RedeemRate { get; set; }
+}
+
+///
+/// 积分商城规则详情响应。
+///
+public sealed class PointMallRuleDetailResultResponse
+{
+ ///
+ /// 规则。
+ ///
+ public PointMallRuleResponse Rule { get; set; } = new();
+
+ ///
+ /// 统计。
+ ///
+ public PointMallRuleStatsResponse Stats { get; set; } = new();
+}
+
+///
+/// 积分商城商品响应。
+///
+public sealed class PointMallProductResponse
+{
+ ///
+ /// 积分商城商品 ID。
+ ///
+ public string PointMallProductId { get; set; } = string.Empty;
+
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 展示名称。
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 展示图片。
+ ///
+ public string? ImageUrl { get; set; }
+
+ ///
+ /// 兑换类型(product/coupon/physical)。
+ ///
+ public string RedeemType { get; set; } = "product";
+
+ ///
+ /// 兑换类型文案。
+ ///
+ public string RedeemTypeText { get; set; } = "商品";
+
+ ///
+ /// 关联商品 ID。
+ ///
+ public string? ProductId { get; set; }
+
+ ///
+ /// 关联优惠券模板 ID。
+ ///
+ public string? CouponTemplateId { get; set; }
+
+ ///
+ /// 实物名称。
+ ///
+ public string? PhysicalName { get; set; }
+
+ ///
+ /// 领取方式(store_pickup/delivery)。
+ ///
+ public string? PickupMethod { get; set; }
+
+ ///
+ /// 商品描述。
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// 兑换方式(points/mixed)。
+ ///
+ public string ExchangeType { get; set; } = "points";
+
+ ///
+ /// 所需积分。
+ ///
+ public int RequiredPoints { get; set; }
+
+ ///
+ /// 现金部分。
+ ///
+ public decimal CashAmount { get; set; }
+
+ ///
+ /// 初始库存。
+ ///
+ public int StockTotal { get; set; }
+
+ ///
+ /// 剩余库存。
+ ///
+ public int StockAvailable { get; set; }
+
+ ///
+ /// 已兑换数量。
+ ///
+ public int RedeemedCount { get; set; }
+
+ ///
+ /// 每人限兑次数。
+ ///
+ public int? PerMemberLimit { get; set; }
+
+ ///
+ /// 通知渠道。
+ ///
+ public List NotifyChannels { get; set; } = [];
+
+ ///
+ /// 状态(enabled/disabled)。
+ ///
+ public string Status { get; set; } = "enabled";
+
+ ///
+ /// 状态文案。
+ ///
+ public string StatusText { get; set; } = "上架";
+
+ ///
+ /// 更新时间。
+ ///
+ public string UpdatedAt { get; set; } = string.Empty;
+}
+
+///
+/// 积分商城商品列表响应。
+///
+public sealed class PointMallProductListResultResponse
+{
+ ///
+ /// 列表。
+ ///
+ public List Items { get; set; } = [];
+}
+
+///
+/// 积分商城兑换记录响应。
+///
+public class PointMallRecordResponse
+{
+ ///
+ /// 兑换记录 ID。
+ ///
+ public string RecordId { get; set; } = string.Empty;
+
+ ///
+ /// 兑换单号。
+ ///
+ public string RecordNo { get; set; } = string.Empty;
+
+ ///
+ /// 积分商城商品 ID。
+ ///
+ public string PointMallProductId { get; set; } = string.Empty;
+
+ ///
+ /// 商品名称。
+ ///
+ public string ProductName { get; set; } = string.Empty;
+
+ ///
+ /// 兑换类型(product/coupon/physical)。
+ ///
+ public string RedeemType { get; set; } = "product";
+
+ ///
+ /// 兑换类型文案。
+ ///
+ public string RedeemTypeText { get; set; } = "商品";
+
+ ///
+ /// 兑换方式(points/mixed)。
+ ///
+ public string ExchangeType { get; set; } = "points";
+
+ ///
+ /// 会员 ID。
+ ///
+ public string MemberId { get; set; } = string.Empty;
+
+ ///
+ /// 会员名称。
+ ///
+ public string MemberName { get; set; } = string.Empty;
+
+ ///
+ /// 会员手机号(脱敏)。
+ ///
+ public string MemberMobileMasked { get; set; } = string.Empty;
+
+ ///
+ /// 消耗积分。
+ ///
+ public int UsedPoints { get; set; }
+
+ ///
+ /// 现金部分。
+ ///
+ public decimal CashAmount { get; set; }
+
+ ///
+ /// 状态(pending_pickup/issued/completed/canceled)。
+ ///
+ public string Status { get; set; } = "issued";
+
+ ///
+ /// 状态文案。
+ ///
+ public string StatusText { get; set; } = "已发放";
+
+ ///
+ /// 兑换时间。
+ ///
+ public string RedeemedAt { get; set; } = string.Empty;
+
+ ///
+ /// 发放时间。
+ ///
+ public string? IssuedAt { get; set; }
+
+ ///
+ /// 核销时间。
+ ///
+ public string? VerifiedAt { get; set; }
+}
+
+///
+/// 积分商城兑换记录详情响应。
+///
+public sealed class PointMallRecordDetailResponse : PointMallRecordResponse
+{
+ ///
+ /// 核销方式(scan/manual)。
+ ///
+ public string? VerifyMethod { get; set; }
+
+ ///
+ /// 核销方式文案。
+ ///
+ public string? VerifyMethodText { get; set; }
+
+ ///
+ /// 核销备注。
+ ///
+ public string? VerifyRemark { get; set; }
+
+ ///
+ /// 核销人 ID。
+ ///
+ public string? VerifiedBy { get; set; }
+}
+
+///
+/// 积分商城兑换记录统计响应。
+///
+public sealed class PointMallRecordStatsResponse
+{
+ ///
+ /// 今日兑换。
+ ///
+ public int TodayRedeemCount { get; set; }
+
+ ///
+ /// 待领取实物。
+ ///
+ public int PendingPhysicalCount { get; set; }
+
+ ///
+ /// 本月消耗积分。
+ ///
+ public int CurrentMonthUsedPoints { get; set; }
+}
+
+///
+/// 积分商城兑换记录分页响应。
+///
+public sealed class PointMallRecordListResultResponse
+{
+ ///
+ /// 列表。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+
+ ///
+ /// 总条数。
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// 统计。
+ ///
+ public PointMallRecordStatsResponse Stats { get; set; } = new();
+}
+
+///
+/// 积分商城兑换记录导出响应。
+///
+public sealed class PointMallRecordExportResponse
+{
+ ///
+ /// 文件名。
+ ///
+ 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/Controllers/MemberPointsMallController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/MemberPointsMallController.cs
new file mode 100644
index 0000000..1647f89
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/MemberPointsMallController.cs
@@ -0,0 +1,526 @@
+using System.Globalization;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
+using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
+using TakeoutSaaS.Application.App.Members.PointsMall.Queries;
+using TakeoutSaaS.Application.App.Stores.Services;
+using TakeoutSaaS.Infrastructure.App.Persistence;
+using TakeoutSaaS.Module.Authorization.Attributes;
+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/points-mall")]
+public sealed class MemberPointsMallController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService)
+ : BaseApiController
+{
+ private const string ViewPermission = "tenant:member:points-mall:view";
+ private const string ManagePermission = "tenant:member:points-mall:manage";
+
+ ///
+ /// 获取积分规则详情。
+ ///
+ [HttpGet("rule/detail")]
+ [PermissionAuthorize(ViewPermission, ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> RuleDetail(
+ [FromQuery] PointMallRuleDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new GetPointMallRuleDetailQuery
+ {
+ StoreId = storeId
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new PointMallRuleDetailResultResponse
+ {
+ Rule = MapRule(result.Rule),
+ Stats = new PointMallRuleStatsResponse
+ {
+ TotalIssuedPoints = result.Stats.TotalIssuedPoints,
+ RedeemedPoints = result.Stats.RedeemedPoints,
+ PointMembers = result.Stats.PointMembers,
+ RedeemRate = result.Stats.RedeemRate
+ }
+ });
+ }
+
+ ///
+ /// 保存积分规则。
+ ///
+ [HttpPost("rule/save")]
+ [PermissionAuthorize(ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> SaveRule(
+ [FromBody] SavePointMallRuleRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new SavePointMallRuleCommand
+ {
+ StoreId = storeId,
+ IsConsumeRewardEnabled = request.IsConsumeRewardEnabled,
+ ConsumeAmountPerStep = request.ConsumeAmountPerStep,
+ ConsumeRewardPointsPerStep = request.ConsumeRewardPointsPerStep,
+ IsReviewRewardEnabled = request.IsReviewRewardEnabled,
+ ReviewRewardPoints = request.ReviewRewardPoints,
+ IsRegisterRewardEnabled = request.IsRegisterRewardEnabled,
+ RegisterRewardPoints = request.RegisterRewardPoints,
+ IsSigninRewardEnabled = request.IsSigninRewardEnabled,
+ SigninRewardPoints = request.SigninRewardPoints,
+ ExpiryMode = request.ExpiryMode
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapRule(result));
+ }
+
+ ///
+ /// 查询兑换商品列表。
+ ///
+ [HttpGet("product/list")]
+ [PermissionAuthorize(ViewPermission, ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> ProductList(
+ [FromQuery] PointMallProductListRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new GetPointMallProductListQuery
+ {
+ StoreId = storeId,
+ Status = request.Status,
+ Keyword = request.Keyword
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new PointMallProductListResultResponse
+ {
+ Items = result.Items.Select(MapProduct).ToList()
+ });
+ }
+
+ ///
+ /// 查询兑换商品详情。
+ ///
+ [HttpGet("product/detail")]
+ [PermissionAuthorize(ViewPermission, ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> ProductDetail(
+ [FromQuery] PointMallProductDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new GetPointMallProductDetailQuery
+ {
+ StoreId = storeId,
+ PointMallProductId = StoreApiHelpers.ParseRequiredSnowflake(request.PointMallProductId, nameof(request.PointMallProductId))
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapProduct(result));
+ }
+
+ ///
+ /// 保存兑换商品。
+ ///
+ [HttpPost("product/save")]
+ [PermissionAuthorize(ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> SaveProduct(
+ [FromBody] SavePointMallProductRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new SavePointMallProductCommand
+ {
+ StoreId = storeId,
+ PointMallProductId = StoreApiHelpers.ParseSnowflakeOrNull(request.PointMallProductId),
+ Name = request.Name,
+ ImageUrl = request.ImageUrl,
+ RedeemType = request.RedeemType,
+ ProductId = StoreApiHelpers.ParseSnowflakeOrNull(request.ProductId),
+ CouponTemplateId = StoreApiHelpers.ParseSnowflakeOrNull(request.CouponTemplateId),
+ PhysicalName = request.PhysicalName,
+ PickupMethod = request.PickupMethod,
+ Description = request.Description,
+ ExchangeType = request.ExchangeType,
+ RequiredPoints = request.RequiredPoints,
+ CashAmount = request.CashAmount,
+ StockTotal = request.StockTotal,
+ PerMemberLimit = request.PerMemberLimit,
+ NotifyChannels = request.NotifyChannels,
+ Status = request.Status
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapProduct(result));
+ }
+
+ ///
+ /// 修改兑换商品状态。
+ ///
+ [HttpPost("product/status")]
+ [PermissionAuthorize(ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> ChangeProductStatus(
+ [FromBody] ChangePointMallProductStatusRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var result = await mediator.Send(new ChangePointMallProductStatusCommand
+ {
+ StoreId = storeId,
+ PointMallProductId = StoreApiHelpers.ParseRequiredSnowflake(request.PointMallProductId, nameof(request.PointMallProductId)),
+ Status = request.Status
+ }, cancellationToken);
+
+ return ApiResponse.Ok(MapProduct(result));
+ }
+
+ ///
+ /// 删除兑换商品。
+ ///
+ [HttpPost("product/delete")]
+ [PermissionAuthorize(ManagePermission)]
+ [ProducesResponseType(typeof(ApiResponse