feat(member): implement points mall backend module
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 兑换方式。
|
||||
/// </summary>
|
||||
public enum MemberPointMallExchangeType
|
||||
{
|
||||
/// <summary>
|
||||
/// 纯积分。
|
||||
/// </summary>
|
||||
PointsOnly = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 积分 + 现金。
|
||||
/// </summary>
|
||||
PointsAndCash = 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 积分有效期模式。
|
||||
/// </summary>
|
||||
public enum MemberPointMallExpiryMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 永久有效。
|
||||
/// </summary>
|
||||
Permanent = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 按年清零(每年 12 月 31 日)。
|
||||
/// </summary>
|
||||
YearlyClear = 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 到账通知渠道。
|
||||
/// </summary>
|
||||
public enum MemberPointMallNotifyChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// 站内消息。
|
||||
/// </summary>
|
||||
InApp = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 短信通知。
|
||||
/// </summary>
|
||||
Sms = 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 实物领取方式。
|
||||
/// </summary>
|
||||
public enum MemberPointMallPickupMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// 到店自提。
|
||||
/// </summary>
|
||||
StorePickup = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 快递配送。
|
||||
/// </summary>
|
||||
Delivery = 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城商品状态。
|
||||
/// </summary>
|
||||
public enum MemberPointMallProductStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 下架。
|
||||
/// </summary>
|
||||
Disabled = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 上架。
|
||||
/// </summary>
|
||||
Enabled = 1
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 核销方式。
|
||||
/// </summary>
|
||||
public enum MemberPointMallVerifyMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫码核销。
|
||||
/// </summary>
|
||||
Scan = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 手动核销。
|
||||
/// </summary>
|
||||
Manual = 1
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user