Merge pull request #3 from msumshk/feature/member-points-mall-1to1

feat(member): implement points mall backend module
This commit is contained in:
2026-03-04 12:32:58 +08:00
committed by GitHub
53 changed files with 5193 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Membership.Entities;
/// <summary>
/// 会员积分商城兑换商品。
/// </summary>
public sealed class MemberPointMallProduct : MultiTenantEntityBase
{
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; set; }
/// <summary>
/// 展示名称。
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 展示图片地址。
/// </summary>
public string? ImageUrl { get; set; }
/// <summary>
/// 兑换类型。
/// </summary>
public MemberPointMallRedeemType RedeemType { get; set; } = MemberPointMallRedeemType.Product;
/// <summary>
/// 关联商品 ID兑换商品时必填
/// </summary>
public long? ProductId { get; set; }
/// <summary>
/// 关联优惠券模板 ID兑换优惠券时必填
/// </summary>
public long? CouponTemplateId { get; set; }
/// <summary>
/// 实物名称(兑换实物时必填)。
/// </summary>
public string? PhysicalName { get; set; }
/// <summary>
/// 实物领取方式。
/// </summary>
public MemberPointMallPickupMethod? PickupMethod { get; set; }
/// <summary>
/// 商品描述。
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 兑换方式(纯积分/积分+现金)。
/// </summary>
public MemberPointMallExchangeType ExchangeType { get; set; } = MemberPointMallExchangeType.PointsOnly;
/// <summary>
/// 所需积分。
/// </summary>
public int RequiredPoints { get; set; }
/// <summary>
/// 现金部分(积分+现金时使用)。
/// </summary>
public decimal CashAmount { get; set; }
/// <summary>
/// 初始库存数量。
/// </summary>
public int StockTotal { get; set; }
/// <summary>
/// 剩余库存数量。
/// </summary>
public int StockAvailable { get; set; }
/// <summary>
/// 每人限兑次数null 表示不限)。
/// </summary>
public int? PerMemberLimit { get; set; }
/// <summary>
/// 到账通知渠道JSON 数组)。
/// </summary>
public string NotifyChannelsJson { get; set; } = "[]";
/// <summary>
/// 上下架状态。
/// </summary>
public MemberPointMallProductStatus Status { get; set; } = MemberPointMallProductStatus.Enabled;
}

View File

@@ -0,0 +1,100 @@
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Membership.Entities;
/// <summary>
/// 会员积分商城兑换记录。
/// </summary>
public sealed class MemberPointMallRecord : MultiTenantEntityBase
{
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; set; }
/// <summary>
/// 兑换记录单号。
/// </summary>
public string RecordNo { get; set; } = string.Empty;
/// <summary>
/// 关联积分商品 ID。
/// </summary>
public long PointMallProductId { get; set; }
/// <summary>
/// 会员标识。
/// </summary>
public long MemberId { get; set; }
/// <summary>
/// 会员名称快照。
/// </summary>
public string MemberName { get; set; } = string.Empty;
/// <summary>
/// 会员手机号快照(脱敏)。
/// </summary>
public string MemberMobileMasked { get; set; } = string.Empty;
/// <summary>
/// 商品名称快照。
/// </summary>
public string ProductName { get; set; } = string.Empty;
/// <summary>
/// 兑换类型快照。
/// </summary>
public MemberPointMallRedeemType RedeemType { get; set; } = MemberPointMallRedeemType.Product;
/// <summary>
/// 兑换方式快照。
/// </summary>
public MemberPointMallExchangeType ExchangeType { get; set; } = MemberPointMallExchangeType.PointsOnly;
/// <summary>
/// 消耗积分。
/// </summary>
public int UsedPoints { get; set; }
/// <summary>
/// 现金部分。
/// </summary>
public decimal CashAmount { get; set; }
/// <summary>
/// 记录状态。
/// </summary>
public MemberPointMallRecordStatus Status { get; set; } = MemberPointMallRecordStatus.Issued;
/// <summary>
/// 兑换时间UTC
/// </summary>
public DateTime RedeemedAt { get; set; }
/// <summary>
/// 发放时间UTC
/// </summary>
public DateTime? IssuedAt { get; set; }
/// <summary>
/// 核销时间UTC
/// </summary>
public DateTime? VerifiedAt { get; set; }
/// <summary>
/// 核销方式。
/// </summary>
public MemberPointMallVerifyMethod? VerifyMethod { get; set; }
/// <summary>
/// 核销备注。
/// </summary>
public string? VerifyRemark { get; set; }
/// <summary>
/// 核销人用户标识。
/// </summary>
public long? VerifiedBy { get; set; }
}

View File

@@ -0,0 +1,65 @@
using TakeoutSaaS.Domain.Membership.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
namespace TakeoutSaaS.Domain.Membership.Entities;
/// <summary>
/// 会员积分商城规则配置。
/// </summary>
public sealed class MemberPointMallRule : MultiTenantEntityBase
{
/// <summary>
/// 门店标识。
/// </summary>
public long StoreId { get; set; }
/// <summary>
/// 是否启用消费获取。
/// </summary>
public bool IsConsumeRewardEnabled { get; set; } = true;
/// <summary>
/// 每消费多少元触发一次积分计算。
/// </summary>
public int ConsumeAmountPerStep { get; set; } = 1;
/// <summary>
/// 每步获得积分。
/// </summary>
public int ConsumeRewardPointsPerStep { get; set; } = 1;
/// <summary>
/// 是否启用评价奖励。
/// </summary>
public bool IsReviewRewardEnabled { get; set; } = true;
/// <summary>
/// 评价奖励积分。
/// </summary>
public int ReviewRewardPoints { get; set; } = 10;
/// <summary>
/// 是否启用注册奖励。
/// </summary>
public bool IsRegisterRewardEnabled { get; set; } = true;
/// <summary>
/// 注册奖励积分。
/// </summary>
public int RegisterRewardPoints { get; set; } = 100;
/// <summary>
/// 是否启用签到奖励。
/// </summary>
public bool IsSigninRewardEnabled { get; set; }
/// <summary>
/// 签到奖励积分。
/// </summary>
public int SigninRewardPoints { get; set; } = 5;
/// <summary>
/// 积分有效期模式。
/// </summary>
public MemberPointMallExpiryMode ExpiryMode { get; set; } = MemberPointMallExpiryMode.YearlyClear;
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 兑换方式。
/// </summary>
public enum MemberPointMallExchangeType
{
/// <summary>
/// 纯积分。
/// </summary>
PointsOnly = 0,
/// <summary>
/// 积分 + 现金。
/// </summary>
PointsAndCash = 1
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 积分有效期模式。
/// </summary>
public enum MemberPointMallExpiryMode
{
/// <summary>
/// 永久有效。
/// </summary>
Permanent = 0,
/// <summary>
/// 按年清零(每年 12 月 31 日)。
/// </summary>
YearlyClear = 1
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 到账通知渠道。
/// </summary>
public enum MemberPointMallNotifyChannel
{
/// <summary>
/// 站内消息。
/// </summary>
InApp = 0,
/// <summary>
/// 短信通知。
/// </summary>
Sms = 1
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 实物领取方式。
/// </summary>
public enum MemberPointMallPickupMethod
{
/// <summary>
/// 到店自提。
/// </summary>
StorePickup = 0,
/// <summary>
/// 快递配送。
/// </summary>
Delivery = 1
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 积分商城商品状态。
/// </summary>
public enum MemberPointMallProductStatus
{
/// <summary>
/// 下架。
/// </summary>
Disabled = 0,
/// <summary>
/// 上架。
/// </summary>
Enabled = 1
}

View File

@@ -0,0 +1,27 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 兑换记录状态。
/// </summary>
public enum MemberPointMallRecordStatus
{
/// <summary>
/// 待领取。
/// </summary>
PendingPickup = 0,
/// <summary>
/// 已发放。
/// </summary>
Issued = 1,
/// <summary>
/// 已完成。
/// </summary>
Completed = 2,
/// <summary>
/// 已取消。
/// </summary>
Canceled = 3
}

View File

@@ -0,0 +1,22 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 积分兑换类型。
/// </summary>
public enum MemberPointMallRedeemType
{
/// <summary>
/// 兑换商品。
/// </summary>
Product = 0,
/// <summary>
/// 兑换优惠券。
/// </summary>
Coupon = 1,
/// <summary>
/// 兑换实物。
/// </summary>
Physical = 2
}

View File

@@ -0,0 +1,17 @@
namespace TakeoutSaaS.Domain.Membership.Enums;
/// <summary>
/// 核销方式。
/// </summary>
public enum MemberPointMallVerifyMethod
{
/// <summary>
/// 扫码核销。
/// </summary>
Scan = 0,
/// <summary>
/// 手动核销。
/// </summary>
Manual = 1
}

View File

@@ -0,0 +1,245 @@
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Membership.Enums;
namespace TakeoutSaaS.Domain.Membership.Repositories;
/// <summary>
/// 会员积分商城仓储契约。
/// </summary>
public interface IPointMallRepository
{
/// <summary>
/// 查询门店积分规则。
/// </summary>
Task<MemberPointMallRule?> GetRuleByStoreAsync(
long tenantId,
long storeId,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增积分规则。
/// </summary>
Task AddRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default);
/// <summary>
/// 更新积分规则。
/// </summary>
Task UpdateRuleAsync(MemberPointMallRule entity, CancellationToken cancellationToken = default);
/// <summary>
/// 查询兑换商品列表。
/// </summary>
Task<IReadOnlyList<MemberPointMallProduct>> SearchProductsAsync(
long tenantId,
long storeId,
MemberPointMallProductStatus? status,
string? keyword,
CancellationToken cancellationToken = default);
/// <summary>
/// 按标识查询兑换商品(追踪)。
/// </summary>
Task<MemberPointMallProduct?> FindProductByIdAsync(
long tenantId,
long storeId,
long productId,
CancellationToken cancellationToken = default);
/// <summary>
/// 按标识查询兑换商品(只读)。
/// </summary>
Task<MemberPointMallProduct?> GetProductByIdAsync(
long tenantId,
long storeId,
long productId,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增兑换商品。
/// </summary>
Task AddProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default);
/// <summary>
/// 更新兑换商品。
/// </summary>
Task UpdateProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default);
/// <summary>
/// 删除兑换商品。
/// </summary>
Task DeleteProductAsync(MemberPointMallProduct entity, CancellationToken cancellationToken = default);
/// <summary>
/// 查询商品是否已有兑换记录。
/// </summary>
Task<bool> HasRecordsByProductIdAsync(
long tenantId,
long storeId,
long pointMallProductId,
CancellationToken cancellationToken = default);
/// <summary>
/// 统计会员在某商品上的有效兑换次数(排除已取消)。
/// </summary>
Task<int> CountMemberRedeemsByProductAsync(
long tenantId,
long storeId,
long pointMallProductId,
long memberId,
CancellationToken cancellationToken = default);
/// <summary>
/// 查询兑换记录分页。
/// </summary>
Task<(IReadOnlyList<MemberPointMallRecord> Items, int TotalCount)> SearchRecordsAsync(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword,
int page,
int pageSize,
CancellationToken cancellationToken = default);
/// <summary>
/// 查询兑换记录详情。
/// </summary>
Task<MemberPointMallRecord?> GetRecordByIdAsync(
long tenantId,
long storeId,
long recordId,
CancellationToken cancellationToken = default);
/// <summary>
/// 查询兑换记录(追踪)。
/// </summary>
Task<MemberPointMallRecord?> FindRecordByIdAsync(
long tenantId,
long storeId,
long recordId,
CancellationToken cancellationToken = default);
/// <summary>
/// 查询兑换记录导出数据。
/// </summary>
Task<IReadOnlyList<MemberPointMallRecord>> ListRecordsForExportAsync(
long tenantId,
long storeId,
MemberPointMallRedeemType? redeemType,
MemberPointMallRecordStatus? status,
DateTime? startUtc,
DateTime? endUtc,
string? keyword,
CancellationToken cancellationToken = default);
/// <summary>
/// 新增兑换记录。
/// </summary>
Task AddRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default);
/// <summary>
/// 更新兑换记录。
/// </summary>
Task UpdateRecordAsync(MemberPointMallRecord entity, CancellationToken cancellationToken = default);
/// <summary>
/// 新增积分流水。
/// </summary>
Task AddPointLedgerAsync(MemberPointLedger entity, CancellationToken cancellationToken = default);
/// <summary>
/// 查询规则页统计。
/// </summary>
Task<MemberPointMallRuleStatsSnapshot> GetRuleStatsAsync(
long tenantId,
long storeId,
CancellationToken cancellationToken = default);
/// <summary>
/// 查询记录页统计。
/// </summary>
Task<MemberPointMallRecordStatsSnapshot> GetRecordStatsAsync(
long tenantId,
long storeId,
DateTime nowUtc,
CancellationToken cancellationToken = default);
/// <summary>
/// 查询商品聚合统计快照。
/// </summary>
Task<Dictionary<long, MemberPointMallProductAggregateSnapshot>> GetProductAggregatesAsync(
long tenantId,
long storeId,
IReadOnlyCollection<long> pointMallProductIds,
CancellationToken cancellationToken = default);
/// <summary>
/// 持久化变更。
/// </summary>
Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// 积分商城规则页统计快照。
/// </summary>
public sealed record MemberPointMallRuleStatsSnapshot
{
/// <summary>
/// 累计发放积分。
/// </summary>
public int TotalIssuedPoints { get; init; }
/// <summary>
/// 已兑换积分。
/// </summary>
public int RedeemedPoints { get; init; }
/// <summary>
/// 积分用户数。
/// </summary>
public int PointMembers { get; init; }
/// <summary>
/// 兑换率0-100
/// </summary>
public decimal RedeemRate { get; init; }
}
/// <summary>
/// 积分商城记录页统计快照。
/// </summary>
public sealed record MemberPointMallRecordStatsSnapshot
{
/// <summary>
/// 今日兑换数量。
/// </summary>
public int TodayRedeemCount { get; init; }
/// <summary>
/// 待领取实物数量。
/// </summary>
public int PendingPhysicalCount { get; init; }
/// <summary>
/// 本月消耗积分。
/// </summary>
public int CurrentMonthUsedPoints { get; init; }
}
/// <summary>
/// 积分商城商品聚合快照。
/// </summary>
public sealed record MemberPointMallProductAggregateSnapshot
{
/// <summary>
/// 商品标识。
/// </summary>
public required long PointMallProductId { get; init; }
/// <summary>
/// 已兑换数量。
/// </summary>
public int RedeemedCount { get; init; }
}