feat: implement marketing punch card backend module
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Coupons.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡实例(顾客购买后生成)。
|
||||
/// </summary>
|
||||
public sealed class PunchCardInstance : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 次卡模板 ID。
|
||||
/// </summary>
|
||||
public long PunchCardTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实例编号(业务唯一)。
|
||||
/// </summary>
|
||||
public string InstanceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 会员名称。
|
||||
/// </summary>
|
||||
public string MemberName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 会员手机号(脱敏)。
|
||||
/// </summary>
|
||||
public string MemberPhoneMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 购买时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime PurchasedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期时间(UTC,可空)。
|
||||
/// </summary>
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总次数。
|
||||
/// </summary>
|
||||
public int TotalTimes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余次数。
|
||||
/// </summary>
|
||||
public int RemainingTimes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实付金额。
|
||||
/// </summary>
|
||||
public decimal PaidAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实例状态。
|
||||
/// </summary>
|
||||
public PunchCardInstanceStatus Status { get; set; } = PunchCardInstanceStatus.Active;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Coupons.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡模板配置。
|
||||
/// </summary>
|
||||
public sealed class PunchCardTemplate : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 次卡名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 封面图片地址。
|
||||
/// </summary>
|
||||
public string? CoverImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 售价。
|
||||
/// </summary>
|
||||
public decimal SalePrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 原价。
|
||||
/// </summary>
|
||||
public decimal? OriginalPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总次数。
|
||||
/// </summary>
|
||||
public int TotalTimes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 有效期类型。
|
||||
/// </summary>
|
||||
public PunchCardValidityType ValidityType { get; set; } = PunchCardValidityType.Days;
|
||||
|
||||
/// <summary>
|
||||
/// 固定天数(ValidityType=Days 时有效)。
|
||||
/// </summary>
|
||||
public int? ValidityDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 固定开始日期(UTC,ValidityType=DateRange 时有效)。
|
||||
/// </summary>
|
||||
public DateTime? ValidFrom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 固定结束日期(UTC,ValidityType=DateRange 时有效)。
|
||||
/// </summary>
|
||||
public DateTime? ValidTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 适用范围类型。
|
||||
/// </summary>
|
||||
public PunchCardScopeType ScopeType { get; set; } = PunchCardScopeType.All;
|
||||
|
||||
/// <summary>
|
||||
/// 指定分类 ID JSON。
|
||||
/// </summary>
|
||||
public string ScopeCategoryIdsJson { get; set; } = "[]";
|
||||
|
||||
/// <summary>
|
||||
/// 指定标签 ID JSON。
|
||||
/// </summary>
|
||||
public string ScopeTagIdsJson { get; set; } = "[]";
|
||||
|
||||
/// <summary>
|
||||
/// 指定商品 ID JSON。
|
||||
/// </summary>
|
||||
public string ScopeProductIdsJson { get; set; } = "[]";
|
||||
|
||||
/// <summary>
|
||||
/// 使用模式。
|
||||
/// </summary>
|
||||
public PunchCardUsageMode UsageMode { get; set; } = PunchCardUsageMode.Free;
|
||||
|
||||
/// <summary>
|
||||
/// 金额上限(UsageMode=Cap 时有效)。
|
||||
/// </summary>
|
||||
public decimal? UsageCapAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每日限用次数。
|
||||
/// </summary>
|
||||
public int? DailyLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每单限用次数。
|
||||
/// </summary>
|
||||
public int? PerOrderLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每人限购张数。
|
||||
/// </summary>
|
||||
public int? PerUserPurchaseLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许转赠。
|
||||
/// </summary>
|
||||
public bool AllowTransfer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期策略。
|
||||
/// </summary>
|
||||
public PunchCardExpireStrategy ExpireStrategy { get; set; } = PunchCardExpireStrategy.Invalidate;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡描述。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 购买通知渠道 JSON。
|
||||
/// </summary>
|
||||
public string NotifyChannelsJson { get; set; } = "[]";
|
||||
|
||||
/// <summary>
|
||||
/// 次卡状态。
|
||||
/// </summary>
|
||||
public PunchCardStatus Status { get; set; } = PunchCardStatus.Enabled;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Coupons.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡使用记录。
|
||||
/// </summary>
|
||||
public sealed class PunchCardUsageRecord : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 次卡模板 ID。
|
||||
/// </summary>
|
||||
public long PunchCardTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 次卡实例 ID。
|
||||
/// </summary>
|
||||
public long PunchCardInstanceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用单号。
|
||||
/// </summary>
|
||||
public string RecordNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 兑换商品名称。
|
||||
/// </summary>
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 使用时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime UsedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次使用次数。
|
||||
/// </summary>
|
||||
public int UsedTimes { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 使用后剩余次数。
|
||||
/// </summary>
|
||||
public int RemainingTimesAfterUse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次记录状态。
|
||||
/// </summary>
|
||||
public PunchCardUsageRecordStatus StatusAfterUse { get; set; } = PunchCardUsageRecordStatus.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 超额补差金额。
|
||||
/// </summary>
|
||||
public decimal? ExtraPayAmount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡过期策略。
|
||||
/// </summary>
|
||||
public enum PunchCardExpireStrategy
|
||||
{
|
||||
/// <summary>
|
||||
/// 剩余次数作废。
|
||||
/// </summary>
|
||||
Invalidate = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 可申请退款。
|
||||
/// </summary>
|
||||
Refund = 1
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡实例状态。
|
||||
/// </summary>
|
||||
public enum PunchCardInstanceStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用中。
|
||||
/// </summary>
|
||||
Active = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已用完。
|
||||
/// </summary>
|
||||
UsedUp = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 已过期。
|
||||
/// </summary>
|
||||
Expired = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 已退款。
|
||||
/// </summary>
|
||||
Refunded = 3
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡适用范围类型。
|
||||
/// </summary>
|
||||
public enum PunchCardScopeType
|
||||
{
|
||||
/// <summary>
|
||||
/// 全部商品。
|
||||
/// </summary>
|
||||
All = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 指定分类。
|
||||
/// </summary>
|
||||
Category = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 指定标签。
|
||||
/// </summary>
|
||||
Tag = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 指定商品。
|
||||
/// </summary>
|
||||
Product = 3
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡状态。
|
||||
/// </summary>
|
||||
public enum PunchCardStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 已下架。
|
||||
/// </summary>
|
||||
Disabled = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已上架。
|
||||
/// </summary>
|
||||
Enabled = 1
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡使用模式。
|
||||
/// </summary>
|
||||
public enum PunchCardUsageMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 完全免费。
|
||||
/// </summary>
|
||||
Free = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 金额上限。
|
||||
/// </summary>
|
||||
Cap = 1
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡使用记录状态。
|
||||
/// </summary>
|
||||
public enum PunchCardUsageRecordStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 正常使用。
|
||||
/// </summary>
|
||||
Normal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 即将用完。
|
||||
/// </summary>
|
||||
AlmostUsedUp = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 已用完。
|
||||
/// </summary>
|
||||
UsedUp = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 已过期。
|
||||
/// </summary>
|
||||
Expired = 3
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡有效期类型。
|
||||
/// </summary>
|
||||
public enum PunchCardValidityType
|
||||
{
|
||||
/// <summary>
|
||||
/// 购买后固定天数。
|
||||
/// </summary>
|
||||
Days = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 固定日期区间。
|
||||
/// </summary>
|
||||
DateRange = 1
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Coupons.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 次卡管理仓储契约。
|
||||
/// </summary>
|
||||
public interface IPunchCardRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询次卡模板分页。
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<PunchCardTemplate> Items, int TotalCount)> SearchTemplatesAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
string? keyword,
|
||||
PunchCardStatus? status,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定次卡模板。
|
||||
/// </summary>
|
||||
Task<PunchCardTemplate?> FindTemplateByIdAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
long templateId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按标识批量读取次卡模板。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<PunchCardTemplate>> GetTemplatesByIdsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
IReadOnlyCollection<long> templateIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按模板批量统计售卖与在用信息。
|
||||
/// </summary>
|
||||
Task<Dictionary<long, PunchCardTemplateAggregateSnapshot>> GetTemplateAggregateByTemplateIdsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
IReadOnlyCollection<long> templateIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询页面统计。
|
||||
/// </summary>
|
||||
Task<PunchCardTemplateStatsSnapshot> GetTemplateStatsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增次卡模板。
|
||||
/// </summary>
|
||||
Task AddTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新次卡模板。
|
||||
/// </summary>
|
||||
Task UpdateTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除次卡模板。
|
||||
/// </summary>
|
||||
Task DeleteTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询次卡实例。
|
||||
/// </summary>
|
||||
Task<PunchCardInstance?> FindInstanceByNoAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
string instanceNo,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询次卡实例。
|
||||
/// </summary>
|
||||
Task<PunchCardInstance?> FindInstanceByIdAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
long instanceId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按标识批量读取次卡实例。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<PunchCardInstance>> GetInstancesByIdsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
IReadOnlyCollection<long> instanceIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增次卡实例。
|
||||
/// </summary>
|
||||
Task AddInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新次卡实例。
|
||||
/// </summary>
|
||||
Task UpdateInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询使用记录分页。
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<PunchCardUsageRecord> Items, int TotalCount)> SearchUsageRecordsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
long? templateId,
|
||||
string? keyword,
|
||||
PunchCardUsageRecordFilterStatus? status,
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询导出使用记录(不分页)。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<PunchCardUsageRecord>> ListUsageRecordsForExportAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
long? templateId,
|
||||
string? keyword,
|
||||
PunchCardUsageRecordFilterStatus? status,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询使用记录统计。
|
||||
/// </summary>
|
||||
Task<PunchCardUsageStatsSnapshot> GetUsageStatsAsync(
|
||||
long tenantId,
|
||||
long storeId,
|
||||
long? templateId,
|
||||
DateTime nowUtc,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增使用记录。
|
||||
/// </summary>
|
||||
Task AddUsageRecordAsync(PunchCardUsageRecord entity, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化变更。
|
||||
/// </summary>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 次卡模板聚合快照。
|
||||
/// </summary>
|
||||
public sealed record PunchCardTemplateAggregateSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 次卡模板标识。
|
||||
/// </summary>
|
||||
public required long TemplateId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 已售数量。
|
||||
/// </summary>
|
||||
public int SoldCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 在用数量。
|
||||
/// </summary>
|
||||
public int ActiveCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 累计收入。
|
||||
/// </summary>
|
||||
public decimal RevenueAmount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 次卡模板统计快照。
|
||||
/// </summary>
|
||||
public sealed record PunchCardTemplateStatsSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 在售数量。
|
||||
/// </summary>
|
||||
public int OnSaleCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 累计售出数量。
|
||||
/// </summary>
|
||||
public int TotalSoldCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 累计收入。
|
||||
/// </summary>
|
||||
public decimal TotalRevenueAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用中数量。
|
||||
/// </summary>
|
||||
public int ActiveInUseCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用记录筛选状态。
|
||||
/// </summary>
|
||||
public enum PunchCardUsageRecordFilterStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 正常(包含即将用完)。
|
||||
/// </summary>
|
||||
Normal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已用完。
|
||||
/// </summary>
|
||||
UsedUp = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 已过期。
|
||||
/// </summary>
|
||||
Expired = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用记录统计快照。
|
||||
/// </summary>
|
||||
public sealed record PunchCardUsageStatsSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// 今日使用次数。
|
||||
/// </summary>
|
||||
public int TodayUsedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月使用次数。
|
||||
/// </summary>
|
||||
public int MonthUsedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 7 天内即将过期数量。
|
||||
/// </summary>
|
||||
public int ExpiringSoonCount { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user