Compare commits
3 Commits
c9e2226b48
...
decfa4fa12
| Author | SHA1 | Date | |
|---|---|---|---|
| decfa4fa12 | |||
| 3b3bdcee71 | |||
| 6588c85f27 |
Submodule TakeoutSaaS.Docs updated: 5da102c97c...315fec77b6
@@ -0,0 +1,489 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Marketing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼详情请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerDetailRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录页码。
|
||||||
|
/// </summary>
|
||||||
|
public int RecordPage { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int RecordPageSize { get; set; } = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼配置保存请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveNewCustomerSettingsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启新客礼包。
|
||||||
|
/// </summary>
|
||||||
|
public bool GiftEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包类型(coupon/direct)。
|
||||||
|
/// </summary>
|
||||||
|
public string GiftType { get; set; } = "coupon";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectReduceAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectMinimumSpend { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启老带新分享。
|
||||||
|
/// </summary>
|
||||||
|
public bool InviteEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分享渠道(wechat_friend/moments/sms)。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ShareChannels { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客礼包券列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<NewCustomerSaveCouponRuleRequest> WelcomeCoupons { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人奖励券列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<NewCustomerSaveCouponRuleRequest> InviterCoupons { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人奖励券列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<NewCustomerSaveCouponRuleRequest> InviteeCoupons { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客邀请记录分页请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerInviteRecordListRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客邀请记录请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteNewCustomerInviteRecordRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviteeName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime InviteTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态(pending_order/ordered)。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderStatus { get; set; } = "pending_order";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励状态(pending/issued)。
|
||||||
|
/// </summary>
|
||||||
|
public string RewardStatus { get; set; } = "pending";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励发放时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RewardIssuedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客成长记录请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteNewCustomerGrowthRecordRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客业务唯一键。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RegisteredAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? GiftClaimedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FirstOrderAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存优惠券规则请求项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerSaveCouponRuleRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 券类型(amount_off/discount/free_shipping)。
|
||||||
|
/// </summary>
|
||||||
|
public string CouponType { get; set; } = "amount_off";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 面值或折扣值。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MinimumSpend { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期天数。
|
||||||
|
/// </summary>
|
||||||
|
public int ValidDays { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼详情响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerDetailResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置详情。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerSettingsResponse Settings { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计数据。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerStatsResponse Stats { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录分页。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerInviteRecordListResultResponse InviteRecords { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼配置响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerSettingsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启新客礼包。
|
||||||
|
/// </summary>
|
||||||
|
public bool GiftEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包类型(coupon/direct)。
|
||||||
|
/// </summary>
|
||||||
|
public string GiftType { get; set; } = "coupon";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectReduceAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectMinimumSpend { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启老带新分享。
|
||||||
|
/// </summary>
|
||||||
|
public bool InviteEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分享渠道(wechat_friend/moments/sms)。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ShareChannels { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客礼包券列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<NewCustomerCouponRuleResponse> WelcomeCoupons { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人奖励券列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<NewCustomerCouponRuleResponse> InviterCoupons { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人奖励券列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<NewCustomerCouponRuleResponse> InviteeCoupons { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string UpdatedAt { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼统计响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerStatsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本月新客数。
|
||||||
|
/// </summary>
|
||||||
|
public int MonthlyNewCustomers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 较上月增长人数。
|
||||||
|
/// </summary>
|
||||||
|
public int MonthlyGrowthCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 较上月增长百分比。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MonthlyGrowthRatePercent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月礼包领取率(百分比)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftClaimRate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月礼包已领取人数。
|
||||||
|
/// </summary>
|
||||||
|
public int GiftClaimedCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月首单转化率(百分比)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal FirstOrderConversionRate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月首单完成人数。
|
||||||
|
/// </summary>
|
||||||
|
public int FirstOrderedCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录分页结果响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerInviteRecordListResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public List<NewCustomerInviteRecordResponse> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客邀请记录响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerInviteRecordResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviteeName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string InviteTime { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态(pending_order/ordered)。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderStatus { get; set; } = "pending_order";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励状态(pending/issued)。
|
||||||
|
/// </summary>
|
||||||
|
public string RewardStatus { get; set; } = "pending";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励发放时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string? RewardIssuedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客成长记录响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerGrowthRecordResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客业务唯一键。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string RegisteredAt { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包领取时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string? GiftClaimedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string? FirstOrderAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客券规则响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerCouponRuleResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 规则 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景(welcome/inviter/invitee)。
|
||||||
|
/// </summary>
|
||||||
|
public string Scene { get; set; } = "welcome";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 券类型(amount_off/discount/free_shipping)。
|
||||||
|
/// </summary>
|
||||||
|
public string CouponType { get; set; } = "amount_off";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 面值或折扣值。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MinimumSpend { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期天数。
|
||||||
|
/// </summary>
|
||||||
|
public int ValidDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序值。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,809 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Marketing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡列表查询请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardListRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 名称关键字。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态筛选(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡详情请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardDetailRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存次卡请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SavePunchCardRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID(编辑时传)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Id { 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>
|
||||||
|
/// 有效期类型(days/range)。
|
||||||
|
/// </summary>
|
||||||
|
public string ValidityType { get; set; } = "days";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定天数。
|
||||||
|
/// </summary>
|
||||||
|
public int? ValidityDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定开始日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? ValidFrom { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定结束日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? ValidTo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 范围类型(all/category/tag/product)。
|
||||||
|
/// </summary>
|
||||||
|
public string ScopeType { get; set; } = "all";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ScopeCategoryIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定标签 ID。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ScopeTagIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ScopeProductIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用模式(free/cap)。
|
||||||
|
/// </summary>
|
||||||
|
public string UsageMode { get; set; } = "free";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次上限金额。
|
||||||
|
/// </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>
|
||||||
|
/// 过期策略(invalidate/refund)。
|
||||||
|
/// </summary>
|
||||||
|
public string ExpireStrategy { get; set; } = "invalidate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通知渠道(in_app/sms)。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> NotifyChannels { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡状态修改请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChangePunchCardStatusRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡删除请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeletePunchCardRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录查询请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageRecordListRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string? PunchCardId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态筛选(normal/used_up/expired)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(会员/商品)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录导出请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportPunchCardUsageRecordRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string? PunchCardId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态筛选(normal/used_up/expired)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(会员/商品)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入次卡使用记录请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WritePunchCardUsageRecordRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡实例 ID(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public string? PunchCardInstanceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡实例编号(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public string? PunchCardInstanceNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? MemberName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员手机号(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public string? MemberPhoneMasked { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兑换商品。
|
||||||
|
/// </summary>
|
||||||
|
public string ProductName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UsedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int UsedTimes { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超额补差金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? ExtraPayAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板统计。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardStatsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 在售次卡数量。
|
||||||
|
/// </summary>
|
||||||
|
public int OnSaleCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计售出数量。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalSoldCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计收入。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalRevenueAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用中数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ActiveInUseCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡列表项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardListItemResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <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 string ValiditySummary { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用范围类型。
|
||||||
|
/// </summary>
|
||||||
|
public string ScopeType { get; set; } = "all";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用模式。
|
||||||
|
/// </summary>
|
||||||
|
public string UsageMode { get; set; } = "free";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次上限金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? UsageCapAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每日限用。
|
||||||
|
/// </summary>
|
||||||
|
public int? DailyLimit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "enabled";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否弱化展示。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDimmed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已售数量。
|
||||||
|
/// </summary>
|
||||||
|
public int SoldCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用中数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ActiveCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计收入。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RevenueAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string UpdatedAt { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡列表结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardListResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<PunchCardListItemResponse> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前页。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计。
|
||||||
|
/// </summary>
|
||||||
|
public PunchCardStatsResponse Stats { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡范围。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardScopeResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 范围类型(all/category/tag/product)。
|
||||||
|
/// </summary>
|
||||||
|
public string ScopeType { get; set; } = "all";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> CategoryIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标签 ID。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TagIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> ProductIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardDetailResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <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>
|
||||||
|
/// 有效期类型(days/range)。
|
||||||
|
/// </summary>
|
||||||
|
public string ValidityType { get; set; } = "days";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定天数。
|
||||||
|
/// </summary>
|
||||||
|
public int? ValidityDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定开始日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? ValidFrom { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定结束日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? ValidTo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用范围。
|
||||||
|
/// </summary>
|
||||||
|
public PunchCardScopeResponse Scope { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用模式(free/cap)。
|
||||||
|
/// </summary>
|
||||||
|
public string UsageMode { get; set; } = "free";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次上限金额。
|
||||||
|
/// </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>
|
||||||
|
/// 过期策略(invalidate/refund)。
|
||||||
|
/// </summary>
|
||||||
|
public string ExpireStrategy { get; set; } = "invalidate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通知渠道。
|
||||||
|
/// </summary>
|
||||||
|
public List<string> NotifyChannels { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "enabled";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已售数量。
|
||||||
|
/// </summary>
|
||||||
|
public int SoldCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用中数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ActiveCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计收入。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RevenueAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string UpdatedAt { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡下拉选项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardTemplateOptionResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用记录统计。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageStatsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 今日使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TodayUsedCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int MonthUsedCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 7 天内即将过期数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ExpiringSoonCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageRecordResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 使用记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用单号。
|
||||||
|
/// </summary>
|
||||||
|
public string RecordNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡名称。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡实例 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardInstanceId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员名称。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员手机号(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberPhoneMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兑换商品。
|
||||||
|
/// </summary>
|
||||||
|
public string ProductName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用时间(yyyy-MM-dd HH:mm:ss)。
|
||||||
|
/// </summary>
|
||||||
|
public string UsedAt { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int UsedTimes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 剩余次数。
|
||||||
|
/// </summary>
|
||||||
|
public int RemainingTimesAfterUse { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTimes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(normal/almost_used_up/used_up/expired)。
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayStatus { get; set; } = "normal";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超额补差金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? ExtraPayAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用记录分页结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageRecordListResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<PunchCardUsageRecordResponse> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计。
|
||||||
|
/// </summary>
|
||||||
|
public PunchCardUsageStatsResponse Stats { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡筛选项。
|
||||||
|
/// </summary>
|
||||||
|
public List<PunchCardTemplateOptionResponse> TemplateOptions { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用记录导出回执。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageRecordExportResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64 文件内容。
|
||||||
|
/// </summary>
|
||||||
|
public string FileContentBase64 { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.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.Marketing;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销中心新客有礼管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/marketing/new-customer")]
|
||||||
|
public sealed class MarketingNewCustomerController(
|
||||||
|
IMediator mediator,
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService)
|
||||||
|
: BaseApiController
|
||||||
|
{
|
||||||
|
private const string ViewPermission = "tenant:marketing:new-customer:view";
|
||||||
|
private const string ManagePermission = "tenant:marketing:new-customer:manage";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取新客有礼详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("detail")]
|
||||||
|
[PermissionAuthorize(ViewPermission, ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<NewCustomerDetailResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<NewCustomerDetailResponse>> Detail(
|
||||||
|
[FromQuery] NewCustomerDetailRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 解析并校验门店权限
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
// 2. 查询应用层详情
|
||||||
|
var result = await mediator.Send(new GetNewCustomerDetailQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
RecordPage = request.RecordPage,
|
||||||
|
RecordPageSize = request.RecordPageSize
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回响应
|
||||||
|
return ApiResponse<NewCustomerDetailResponse>.Ok(MapDetail(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存新客有礼配置。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("save")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<NewCustomerSettingsResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<NewCustomerSettingsResponse>> Save(
|
||||||
|
[FromBody] SaveNewCustomerSettingsRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 解析并校验门店权限
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
// 2. 调用应用层保存
|
||||||
|
var result = await mediator.Send(new SaveNewCustomerSettingsCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
GiftEnabled = request.GiftEnabled,
|
||||||
|
GiftType = request.GiftType,
|
||||||
|
DirectReduceAmount = request.DirectReduceAmount,
|
||||||
|
DirectMinimumSpend = request.DirectMinimumSpend,
|
||||||
|
InviteEnabled = request.InviteEnabled,
|
||||||
|
ShareChannels = request.ShareChannels,
|
||||||
|
WelcomeCoupons = request.WelcomeCoupons.Select(MapSaveCouponRule).ToList(),
|
||||||
|
InviterCoupons = request.InviterCoupons.Select(MapSaveCouponRule).ToList(),
|
||||||
|
InviteeCoupons = request.InviteeCoupons.Select(MapSaveCouponRule).ToList()
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回响应
|
||||||
|
return ApiResponse<NewCustomerSettingsResponse>.Ok(MapSettings(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取新客邀请记录分页。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("invite-record/list")]
|
||||||
|
[PermissionAuthorize(ViewPermission, ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<NewCustomerInviteRecordListResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<NewCustomerInviteRecordListResultResponse>> InviteRecordList(
|
||||||
|
[FromQuery] NewCustomerInviteRecordListRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 解析并校验门店权限
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
// 2. 查询应用层分页
|
||||||
|
var result = await mediator.Send(new GetNewCustomerInviteRecordListQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
Page = request.Page,
|
||||||
|
PageSize = request.PageSize
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回响应
|
||||||
|
return ApiResponse<NewCustomerInviteRecordListResultResponse>.Ok(MapInviteRecordList(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客邀请记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("invite-record/write")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<NewCustomerInviteRecordResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<NewCustomerInviteRecordResponse>> WriteInviteRecord(
|
||||||
|
[FromBody] WriteNewCustomerInviteRecordRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 解析并校验门店权限
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
// 2. 调用应用层写入
|
||||||
|
var result = await mediator.Send(new WriteNewCustomerInviteRecordCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
InviterName = request.InviterName,
|
||||||
|
InviteeName = request.InviteeName,
|
||||||
|
InviteTime = request.InviteTime,
|
||||||
|
OrderStatus = request.OrderStatus,
|
||||||
|
RewardStatus = request.RewardStatus,
|
||||||
|
RewardIssuedAt = request.RewardIssuedAt,
|
||||||
|
SourceChannel = request.SourceChannel
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回响应
|
||||||
|
return ApiResponse<NewCustomerInviteRecordResponse>.Ok(MapInviteRecord(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客成长记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("growth-record/write")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<NewCustomerGrowthRecordResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<NewCustomerGrowthRecordResponse>> WriteGrowthRecord(
|
||||||
|
[FromBody] WriteNewCustomerGrowthRecordRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 1. 解析并校验门店权限
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
// 2. 调用应用层写入
|
||||||
|
var result = await mediator.Send(new WriteNewCustomerGrowthRecordCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
CustomerKey = request.CustomerKey,
|
||||||
|
CustomerName = request.CustomerName,
|
||||||
|
RegisteredAt = request.RegisteredAt,
|
||||||
|
GiftClaimedAt = request.GiftClaimedAt,
|
||||||
|
FirstOrderAt = request.FirstOrderAt,
|
||||||
|
SourceChannel = request.SourceChannel
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
// 3. 返回响应
|
||||||
|
return ApiResponse<NewCustomerGrowthRecordResponse>.Ok(MapGrowthRecord(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerSaveCouponRuleInputDto MapSaveCouponRule(NewCustomerSaveCouponRuleRequest source)
|
||||||
|
{
|
||||||
|
return new NewCustomerSaveCouponRuleInputDto
|
||||||
|
{
|
||||||
|
CouponType = source.CouponType,
|
||||||
|
Value = source.Value,
|
||||||
|
MinimumSpend = source.MinimumSpend,
|
||||||
|
ValidDays = source.ValidDays
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerDetailResponse MapDetail(NewCustomerDetailDto source)
|
||||||
|
{
|
||||||
|
return new NewCustomerDetailResponse
|
||||||
|
{
|
||||||
|
Settings = MapSettings(source.Settings),
|
||||||
|
Stats = new NewCustomerStatsResponse
|
||||||
|
{
|
||||||
|
MonthlyNewCustomers = source.Stats.MonthlyNewCustomers,
|
||||||
|
MonthlyGrowthCount = source.Stats.MonthlyGrowthCount,
|
||||||
|
MonthlyGrowthRatePercent = source.Stats.MonthlyGrowthRatePercent,
|
||||||
|
GiftClaimRate = source.Stats.GiftClaimRate,
|
||||||
|
GiftClaimedCount = source.Stats.GiftClaimedCount,
|
||||||
|
FirstOrderConversionRate = source.Stats.FirstOrderConversionRate,
|
||||||
|
FirstOrderedCount = source.Stats.FirstOrderedCount
|
||||||
|
},
|
||||||
|
InviteRecords = MapInviteRecordList(source.InviteRecords)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerSettingsResponse MapSettings(NewCustomerSettingsDto source)
|
||||||
|
{
|
||||||
|
return new NewCustomerSettingsResponse
|
||||||
|
{
|
||||||
|
StoreId = source.StoreId.ToString(),
|
||||||
|
GiftEnabled = source.GiftEnabled,
|
||||||
|
GiftType = source.GiftType,
|
||||||
|
DirectReduceAmount = source.DirectReduceAmount,
|
||||||
|
DirectMinimumSpend = source.DirectMinimumSpend,
|
||||||
|
InviteEnabled = source.InviteEnabled,
|
||||||
|
ShareChannels = source.ShareChannels.ToList(),
|
||||||
|
WelcomeCoupons = source.WelcomeCoupons.Select(MapCouponRule).ToList(),
|
||||||
|
InviterCoupons = source.InviterCoupons.Select(MapCouponRule).ToList(),
|
||||||
|
InviteeCoupons = source.InviteeCoupons.Select(MapCouponRule).ToList(),
|
||||||
|
UpdatedAt = ToDateTime(source.UpdatedAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerCouponRuleResponse MapCouponRule(NewCustomerCouponRuleDto source)
|
||||||
|
{
|
||||||
|
return new NewCustomerCouponRuleResponse
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
Scene = source.Scene,
|
||||||
|
CouponType = source.CouponType,
|
||||||
|
Value = source.Value,
|
||||||
|
MinimumSpend = source.MinimumSpend,
|
||||||
|
ValidDays = source.ValidDays,
|
||||||
|
SortOrder = source.SortOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerInviteRecordListResultResponse MapInviteRecordList(
|
||||||
|
NewCustomerInviteRecordListResultDto source)
|
||||||
|
{
|
||||||
|
return new NewCustomerInviteRecordListResultResponse
|
||||||
|
{
|
||||||
|
Items = source.Items.Select(MapInviteRecord).ToList(),
|
||||||
|
Page = source.Page,
|
||||||
|
PageSize = source.PageSize,
|
||||||
|
TotalCount = source.TotalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerInviteRecordResponse MapInviteRecord(NewCustomerInviteRecordDto source)
|
||||||
|
{
|
||||||
|
return new NewCustomerInviteRecordResponse
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
InviterName = source.InviterName,
|
||||||
|
InviteeName = source.InviteeName,
|
||||||
|
InviteTime = ToDateTime(source.InviteTime),
|
||||||
|
OrderStatus = source.OrderStatus,
|
||||||
|
RewardStatus = source.RewardStatus,
|
||||||
|
RewardIssuedAt = source.RewardIssuedAt.HasValue
|
||||||
|
? ToDateTime(source.RewardIssuedAt.Value)
|
||||||
|
: null,
|
||||||
|
SourceChannel = source.SourceChannel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerGrowthRecordResponse MapGrowthRecord(NewCustomerGrowthRecordDto source)
|
||||||
|
{
|
||||||
|
return new NewCustomerGrowthRecordResponse
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
CustomerKey = source.CustomerKey,
|
||||||
|
CustomerName = source.CustomerName,
|
||||||
|
RegisteredAt = ToDateTime(source.RegisteredAt),
|
||||||
|
GiftClaimedAt = source.GiftClaimedAt.HasValue
|
||||||
|
? ToDateTime(source.GiftClaimedAt.Value)
|
||||||
|
: null,
|
||||||
|
FirstOrderAt = source.FirstOrderAt.HasValue
|
||||||
|
? ToDateTime(source.FirstOrderAt.Value)
|
||||||
|
: null,
|
||||||
|
SourceChannel = source.SourceChannel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToDateTime(DateTime value)
|
||||||
|
{
|
||||||
|
return value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.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.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
using TakeoutSaaS.TenantApi.Contracts.Marketing;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 营销中心次卡管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/marketing/punch-card")]
|
||||||
|
public sealed class MarketingPunchCardController(
|
||||||
|
IMediator mediator,
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService)
|
||||||
|
: BaseApiController
|
||||||
|
{
|
||||||
|
private const string ViewPermission = "tenant:marketing:punch-card:view";
|
||||||
|
private const string ManagePermission = "tenant:marketing:punch-card:manage";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取次卡列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
[PermissionAuthorize(ViewPermission, ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PunchCardListResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PunchCardListResultResponse>> List(
|
||||||
|
[FromQuery] PunchCardListRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new GetPunchCardTemplateListQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
Keyword = request.Keyword,
|
||||||
|
Status = request.Status,
|
||||||
|
Page = request.Page,
|
||||||
|
PageSize = request.PageSize
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<PunchCardListResultResponse>.Ok(new PunchCardListResultResponse
|
||||||
|
{
|
||||||
|
Items = result.Items.Select(MapListItem).ToList(),
|
||||||
|
Page = result.Page,
|
||||||
|
PageSize = result.PageSize,
|
||||||
|
TotalCount = result.TotalCount,
|
||||||
|
Stats = MapTemplateStats(result.Stats)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取次卡详情。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("detail")]
|
||||||
|
[PermissionAuthorize(ViewPermission, ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PunchCardDetailResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PunchCardDetailResponse>> Detail(
|
||||||
|
[FromQuery] PunchCardDetailRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new GetPunchCardTemplateDetailQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId))
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
if (result is null)
|
||||||
|
{
|
||||||
|
return ApiResponse<PunchCardDetailResponse>.Error(ErrorCodes.NotFound, "次卡不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<PunchCardDetailResponse>.Ok(MapDetail(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存次卡。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("save")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PunchCardDetailResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PunchCardDetailResponse>> Save(
|
||||||
|
[FromBody] SavePunchCardRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new SavePunchCardTemplateCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
TemplateId = StoreApiHelpers.ParseSnowflakeOrNull(request.Id),
|
||||||
|
Name = request.Name,
|
||||||
|
CoverImageUrl = request.CoverImageUrl,
|
||||||
|
SalePrice = request.SalePrice,
|
||||||
|
OriginalPrice = request.OriginalPrice,
|
||||||
|
TotalTimes = request.TotalTimes,
|
||||||
|
ValidityType = request.ValidityType,
|
||||||
|
ValidityDays = request.ValidityDays,
|
||||||
|
ValidFrom = ParseDateOrNull(request.ValidFrom, nameof(request.ValidFrom)),
|
||||||
|
ValidTo = ParseDateOrNull(request.ValidTo, nameof(request.ValidTo)),
|
||||||
|
ScopeType = request.ScopeType,
|
||||||
|
ScopeCategoryIds = StoreApiHelpers.ParseSnowflakeList(request.ScopeCategoryIds),
|
||||||
|
ScopeTagIds = StoreApiHelpers.ParseSnowflakeList(request.ScopeTagIds),
|
||||||
|
ScopeProductIds = StoreApiHelpers.ParseSnowflakeList(request.ScopeProductIds),
|
||||||
|
UsageMode = request.UsageMode,
|
||||||
|
UsageCapAmount = request.UsageCapAmount,
|
||||||
|
DailyLimit = request.DailyLimit,
|
||||||
|
PerOrderLimit = request.PerOrderLimit,
|
||||||
|
PerUserPurchaseLimit = request.PerUserPurchaseLimit,
|
||||||
|
AllowTransfer = request.AllowTransfer,
|
||||||
|
ExpireStrategy = request.ExpireStrategy,
|
||||||
|
Description = request.Description,
|
||||||
|
NotifyChannels = request.NotifyChannels
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<PunchCardDetailResponse>.Ok(MapDetail(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改次卡状态。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("status")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PunchCardDetailResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PunchCardDetailResponse>> ChangeStatus(
|
||||||
|
[FromBody] ChangePunchCardStatusRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new ChangePunchCardTemplateStatusCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId)),
|
||||||
|
Status = request.Status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<PunchCardDetailResponse>.Ok(MapDetail(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除次卡。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("delete")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> Delete(
|
||||||
|
[FromBody] DeletePunchCardRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
await mediator.Send(new DeletePunchCardTemplateCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId))
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取次卡使用记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("usage-record/list")]
|
||||||
|
[PermissionAuthorize(ViewPermission, ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PunchCardUsageRecordListResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PunchCardUsageRecordListResultResponse>> UsageRecordList(
|
||||||
|
[FromQuery] PunchCardUsageRecordListRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new GetPunchCardUsageRecordListQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
TemplateId = StoreApiHelpers.ParseSnowflakeOrNull(request.PunchCardId),
|
||||||
|
Status = request.Status,
|
||||||
|
Keyword = request.Keyword,
|
||||||
|
Page = request.Page,
|
||||||
|
PageSize = request.PageSize
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<PunchCardUsageRecordListResultResponse>.Ok(new PunchCardUsageRecordListResultResponse
|
||||||
|
{
|
||||||
|
Items = result.Items.Select(MapUsageRecord).ToList(),
|
||||||
|
Page = result.Page,
|
||||||
|
PageSize = result.PageSize,
|
||||||
|
TotalCount = result.TotalCount,
|
||||||
|
Stats = new PunchCardUsageStatsResponse
|
||||||
|
{
|
||||||
|
TodayUsedCount = result.Stats.TodayUsedCount,
|
||||||
|
MonthUsedCount = result.Stats.MonthUsedCount,
|
||||||
|
ExpiringSoonCount = result.Stats.ExpiringSoonCount
|
||||||
|
},
|
||||||
|
TemplateOptions = result.TemplateOptions.Select(item => new PunchCardTemplateOptionResponse
|
||||||
|
{
|
||||||
|
TemplateId = item.TemplateId.ToString(),
|
||||||
|
Name = item.Name
|
||||||
|
}).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出次卡使用记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("usage-record/export")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PunchCardUsageRecordExportResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PunchCardUsageRecordExportResponse>> ExportUsageRecord(
|
||||||
|
[FromQuery] ExportPunchCardUsageRecordRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new ExportPunchCardUsageRecordCsvQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
TemplateId = StoreApiHelpers.ParseSnowflakeOrNull(request.PunchCardId),
|
||||||
|
Status = request.Status,
|
||||||
|
Keyword = request.Keyword
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<PunchCardUsageRecordExportResponse>.Ok(new PunchCardUsageRecordExportResponse
|
||||||
|
{
|
||||||
|
FileName = result.FileName,
|
||||||
|
FileContentBase64 = result.FileContentBase64,
|
||||||
|
TotalCount = result.TotalCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入次卡使用记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("usage-record/write")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<PunchCardUsageRecordResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<PunchCardUsageRecordResponse>> WriteUsageRecord(
|
||||||
|
[FromBody] WritePunchCardUsageRecordRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new WritePunchCardUsageRecordCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
TemplateId = StoreApiHelpers.ParseRequiredSnowflake(request.PunchCardId, nameof(request.PunchCardId)),
|
||||||
|
InstanceId = StoreApiHelpers.ParseSnowflakeOrNull(request.PunchCardInstanceId),
|
||||||
|
InstanceNo = request.PunchCardInstanceNo,
|
||||||
|
MemberName = request.MemberName,
|
||||||
|
MemberPhoneMasked = request.MemberPhoneMasked,
|
||||||
|
ProductName = request.ProductName,
|
||||||
|
UsedAt = request.UsedAt,
|
||||||
|
UsedTimes = request.UsedTimes,
|
||||||
|
ExtraPayAmount = request.ExtraPayAmount
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<PunchCardUsageRecordResponse>.Ok(MapUsageRecord(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ParseDateOrNull(string? value, string fieldName)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value)
|
||||||
|
? null
|
||||||
|
: StoreApiHelpers.ParseDateOnly(value, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PunchCardListItemResponse MapListItem(PunchCardListItemDto source)
|
||||||
|
{
|
||||||
|
return new PunchCardListItemResponse
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
Name = source.Name,
|
||||||
|
CoverImageUrl = source.CoverImageUrl,
|
||||||
|
SalePrice = source.SalePrice,
|
||||||
|
OriginalPrice = source.OriginalPrice,
|
||||||
|
TotalTimes = source.TotalTimes,
|
||||||
|
ValiditySummary = source.ValiditySummary,
|
||||||
|
ScopeType = source.ScopeType,
|
||||||
|
UsageMode = source.UsageMode,
|
||||||
|
UsageCapAmount = source.UsageCapAmount,
|
||||||
|
DailyLimit = source.DailyLimit,
|
||||||
|
Status = source.Status,
|
||||||
|
IsDimmed = source.IsDimmed,
|
||||||
|
SoldCount = source.SoldCount,
|
||||||
|
ActiveCount = source.ActiveCount,
|
||||||
|
RevenueAmount = source.RevenueAmount,
|
||||||
|
UpdatedAt = ToDateTime(source.UpdatedAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PunchCardStatsResponse MapTemplateStats(PunchCardStatsDto source)
|
||||||
|
{
|
||||||
|
return new PunchCardStatsResponse
|
||||||
|
{
|
||||||
|
OnSaleCount = source.OnSaleCount,
|
||||||
|
TotalSoldCount = source.TotalSoldCount,
|
||||||
|
TotalRevenueAmount = source.TotalRevenueAmount,
|
||||||
|
ActiveInUseCount = source.ActiveInUseCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PunchCardDetailResponse MapDetail(PunchCardDetailDto source)
|
||||||
|
{
|
||||||
|
return new PunchCardDetailResponse
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
StoreId = source.StoreId.ToString(),
|
||||||
|
Name = source.Name,
|
||||||
|
CoverImageUrl = source.CoverImageUrl,
|
||||||
|
SalePrice = source.SalePrice,
|
||||||
|
OriginalPrice = source.OriginalPrice,
|
||||||
|
TotalTimes = source.TotalTimes,
|
||||||
|
ValidityType = source.ValidityType,
|
||||||
|
ValidityDays = source.ValidityDays,
|
||||||
|
ValidFrom = ToDateOnly(source.ValidFrom),
|
||||||
|
ValidTo = ToDateOnly(source.ValidTo),
|
||||||
|
Scope = new PunchCardScopeResponse
|
||||||
|
{
|
||||||
|
ScopeType = source.Scope.ScopeType,
|
||||||
|
CategoryIds = source.Scope.CategoryIds.Select(item => item.ToString()).ToList(),
|
||||||
|
TagIds = source.Scope.TagIds.Select(item => item.ToString()).ToList(),
|
||||||
|
ProductIds = source.Scope.ProductIds.Select(item => item.ToString()).ToList()
|
||||||
|
},
|
||||||
|
UsageMode = source.UsageMode,
|
||||||
|
UsageCapAmount = source.UsageCapAmount,
|
||||||
|
DailyLimit = source.DailyLimit,
|
||||||
|
PerOrderLimit = source.PerOrderLimit,
|
||||||
|
PerUserPurchaseLimit = source.PerUserPurchaseLimit,
|
||||||
|
AllowTransfer = source.AllowTransfer,
|
||||||
|
ExpireStrategy = source.ExpireStrategy,
|
||||||
|
Description = source.Description,
|
||||||
|
NotifyChannels = source.NotifyChannels.ToList(),
|
||||||
|
Status = source.Status,
|
||||||
|
SoldCount = source.SoldCount,
|
||||||
|
ActiveCount = source.ActiveCount,
|
||||||
|
RevenueAmount = source.RevenueAmount,
|
||||||
|
UpdatedAt = ToDateTime(source.UpdatedAt)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PunchCardUsageRecordResponse MapUsageRecord(PunchCardUsageRecordDto source)
|
||||||
|
{
|
||||||
|
return new PunchCardUsageRecordResponse
|
||||||
|
{
|
||||||
|
Id = source.Id.ToString(),
|
||||||
|
RecordNo = source.RecordNo,
|
||||||
|
PunchCardId = source.PunchCardTemplateId.ToString(),
|
||||||
|
PunchCardName = source.PunchCardName,
|
||||||
|
PunchCardInstanceId = source.PunchCardInstanceId.ToString(),
|
||||||
|
MemberName = source.MemberName,
|
||||||
|
MemberPhoneMasked = source.MemberPhoneMasked,
|
||||||
|
ProductName = source.ProductName,
|
||||||
|
UsedAt = ToDateTime(source.UsedAt),
|
||||||
|
UsedTimes = source.UsedTimes,
|
||||||
|
RemainingTimesAfterUse = source.RemainingTimesAfterUse,
|
||||||
|
TotalTimes = source.TotalTimes,
|
||||||
|
DisplayStatus = source.DisplayStatus,
|
||||||
|
ExtraPayAmount = source.ExtraPayAmount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ToDateOnly(DateTime? value)
|
||||||
|
{
|
||||||
|
return value.HasValue
|
||||||
|
? value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToDateTime(DateTime value)
|
||||||
|
{
|
||||||
|
return value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,12 @@ using TakeoutSaaS.TenantApi.Contracts.Product;
|
|||||||
|
|
||||||
namespace TakeoutSaaS.TenantApi.Controllers;
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供商品批量工具能力,包括批量调价、上下架、移类、跨店同步、导入与导出。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbContext">应用数据库上下文。</param>
|
||||||
|
/// <param name="storeContextService">门店上下文服务。</param>
|
||||||
|
/// <param name="idGenerator">雪花 ID 生成器。</param>
|
||||||
[ApiVersion("1.0")]
|
[ApiVersion("1.0")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[Route("api/tenant/v{version:apiVersion}/product/batch")]
|
[Route("api/tenant/v{version:apiVersion}/product/batch")]
|
||||||
@@ -39,6 +45,12 @@ public sealed class ProductBatchToolController(
|
|||||||
"状态"
|
"状态"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预览批量调价结果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">调价预览请求。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>调价预览结果。</returns>
|
||||||
[HttpPost("price-adjust/preview")]
|
[HttpPost("price-adjust/preview")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchPricePreviewResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchPricePreviewResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<BatchPricePreviewResponse>> PreviewPriceAdjust(
|
public async Task<ApiResponse<BatchPricePreviewResponse>> PreviewPriceAdjust(
|
||||||
@@ -81,6 +93,12 @@ public sealed class ProductBatchToolController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行批量调价。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">调价请求。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>批量工具执行结果。</returns>
|
||||||
[HttpPost("price-adjust")]
|
[HttpPost("price-adjust")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<BatchToolResultResponse>> PriceAdjust(
|
public async Task<ApiResponse<BatchToolResultResponse>> PriceAdjust(
|
||||||
@@ -123,6 +141,12 @@ public sealed class ProductBatchToolController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量切换商品上下架状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">上下架切换请求。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>批量工具执行结果。</returns>
|
||||||
[HttpPost("sale-switch")]
|
[HttpPost("sale-switch")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<BatchToolResultResponse>> SaleSwitch(
|
public async Task<ApiResponse<BatchToolResultResponse>> SaleSwitch(
|
||||||
@@ -186,6 +210,12 @@ public sealed class ProductBatchToolController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量移动商品分类。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">移类请求。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>批量工具执行结果。</returns>
|
||||||
[HttpPost("move-category")]
|
[HttpPost("move-category")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<BatchToolResultResponse>> MoveCategory(
|
public async Task<ApiResponse<BatchToolResultResponse>> MoveCategory(
|
||||||
@@ -256,6 +286,12 @@ public sealed class ProductBatchToolController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将源门店商品批量同步到目标门店。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">跨店同步请求。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>批量工具执行结果。</returns>
|
||||||
[HttpPost("store-sync")]
|
[HttpPost("store-sync")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchToolResultResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<BatchToolResultResponse>> SyncStore(
|
public async Task<ApiResponse<BatchToolResultResponse>> SyncStore(
|
||||||
@@ -479,6 +515,12 @@ public sealed class ProductBatchToolController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载商品批量导入模板。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="storeId">门店 ID。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>导入模板文件。</returns>
|
||||||
[HttpGet("import/template")]
|
[HttpGet("import/template")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchExcelFileResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchExcelFileResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<BatchExcelFileResponse>> DownloadImportTemplate(
|
public async Task<ApiResponse<BatchExcelFileResponse>> DownloadImportTemplate(
|
||||||
@@ -519,6 +561,12 @@ public sealed class ProductBatchToolController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量导入商品。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">导入请求。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>导入结果。</returns>
|
||||||
[HttpPost("import")]
|
[HttpPost("import")]
|
||||||
[Consumes("multipart/form-data")]
|
[Consumes("multipart/form-data")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchImportResultResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchImportResultResponse>), StatusCodes.Status200OK)]
|
||||||
@@ -858,6 +906,12 @@ public sealed class ProductBatchToolController(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按范围导出商品 Excel。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">导出请求。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>导出文件。</returns>
|
||||||
[HttpPost("export")]
|
[HttpPost("export")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<BatchExcelFileResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<BatchExcelFileResponse>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<BatchExcelFileResponse>> Export(
|
public async Task<ApiResponse<BatchExcelFileResponse>> Export(
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存新客有礼配置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveNewCustomerSettingsCommand : IRequest<NewCustomerSettingsDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启新客礼包。
|
||||||
|
/// </summary>
|
||||||
|
public bool GiftEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包类型(coupon/direct)。
|
||||||
|
/// </summary>
|
||||||
|
public string GiftType { get; init; } = "coupon";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectReduceAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectMinimumSpend { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启邀请分享。
|
||||||
|
/// </summary>
|
||||||
|
public bool InviteEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分享渠道。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<string> ShareChannels { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客礼包券。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<NewCustomerSaveCouponRuleInputDto> WelcomeCoupons { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人奖励券。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<NewCustomerSaveCouponRuleInputDto> InviterCoupons { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人奖励券。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<NewCustomerSaveCouponRuleInputDto> InviteeCoupons { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客成长记录。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteNewCustomerGrowthRecordCommand : IRequest<NewCustomerGrowthRecordDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客业务唯一键。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerKey { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RegisteredAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? GiftClaimedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FirstOrderAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客邀请记录。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteNewCustomerInviteRecordCommand : IRequest<NewCustomerInviteRecordDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviterName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviteeName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime InviteTime { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态(pending_order/ordered)。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderStatus { get; init; } = "pending_order";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励状态(pending/issued)。
|
||||||
|
/// </summary>
|
||||||
|
public string RewardStatus { get; init; } = "pending";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励发放时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RewardIssuedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 来源渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼券规则 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerCouponRuleDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 规则 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景(welcome/inviter/invitee)。
|
||||||
|
/// </summary>
|
||||||
|
public string Scene { get; init; } = "welcome";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 券类型(amount_off/discount/free_shipping)。
|
||||||
|
/// </summary>
|
||||||
|
public string CouponType { get; init; } = "amount_off";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 面值或折扣值。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Value { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用门槛。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MinimumSpend { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期天数。
|
||||||
|
/// </summary>
|
||||||
|
public int ValidDays { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序值。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼详情 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerDetailDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置详情。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerSettingsDto Settings { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计数据。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerStatsDto Stats { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录分页结果。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerInviteRecordListResultDto InviteRecords { get; init; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客成长记录 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerGrowthRecordDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客业务键。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerKey { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RegisteredAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? GiftClaimedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FirstOrderAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道来源。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客邀请记录 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerInviteRecordDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviterName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviteeName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime InviteTime { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(pending_order/ordered)。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderStatus { get; init; } = "pending_order";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励发放状态(pending/issued)。
|
||||||
|
/// </summary>
|
||||||
|
public string RewardStatus { get; init; } = "pending";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励发放时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RewardIssuedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道来源。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客邀请记录分页结果 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerInviteRecordListResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<NewCustomerInviteRecordDto> Items { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存新客有礼券规则输入 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerSaveCouponRuleInputDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 券类型(amount_off/discount/free_shipping)。
|
||||||
|
/// </summary>
|
||||||
|
public string CouponType { get; init; } = "amount_off";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 面值或折扣值。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Value { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MinimumSpend { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期天数。
|
||||||
|
/// </summary>
|
||||||
|
public int ValidDays { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼配置 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerSettingsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启新客礼包。
|
||||||
|
/// </summary>
|
||||||
|
public bool GiftEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包类型(coupon/direct)。
|
||||||
|
/// </summary>
|
||||||
|
public string GiftType { get; init; } = "coupon";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectReduceAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectMinimumSpend { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启老带新分享。
|
||||||
|
/// </summary>
|
||||||
|
public bool InviteEnabled { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分享渠道。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> ShareChannels { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客礼包券列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<NewCustomerCouponRuleDto> WelcomeCoupons { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人奖励券列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<NewCustomerCouponRuleDto> InviterCoupons { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人奖励券列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<NewCustomerCouponRuleDto> InviteeCoupons { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UpdatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼统计 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerStatsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本月新客数。
|
||||||
|
/// </summary>
|
||||||
|
public int MonthlyNewCustomers { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 较上月增长人数。
|
||||||
|
/// </summary>
|
||||||
|
public int MonthlyGrowthCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 较上月增长百分比。
|
||||||
|
/// </summary>
|
||||||
|
public decimal MonthlyGrowthRatePercent { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月礼包领取率。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftClaimRate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月礼包已领取人数。
|
||||||
|
/// </summary>
|
||||||
|
public int GiftClaimedCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月首单转化率。
|
||||||
|
/// </summary>
|
||||||
|
public decimal FirstOrderConversionRate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月首单完成人数。
|
||||||
|
/// </summary>
|
||||||
|
public int FirstOrderedCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询新客有礼详情处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetNewCustomerDetailQueryHandler(
|
||||||
|
INewCustomerGiftRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetNewCustomerDetailQuery, NewCustomerDetailDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<NewCustomerDetailDto> Handle(GetNewCustomerDetailQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var normalizedPage = Math.Max(1, request.RecordPage);
|
||||||
|
var normalizedPageSize = Math.Clamp(request.RecordPageSize, 1, 200);
|
||||||
|
|
||||||
|
var setting = await repository.FindSettingByStoreIdAsync(tenantId, request.StoreId, cancellationToken);
|
||||||
|
var rules = await repository.GetCouponRulesByStoreIdAsync(tenantId, request.StoreId, cancellationToken);
|
||||||
|
var settingsDto = BuildSettingsDto(request.StoreId, setting, rules);
|
||||||
|
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var currentMonthStart = NewCustomerMapping.StartOfMonthUtc(nowUtc);
|
||||||
|
var nextMonthStart = currentMonthStart.AddMonths(1);
|
||||||
|
var previousMonthStart = currentMonthStart.AddMonths(-1);
|
||||||
|
|
||||||
|
var currentMonthNewCustomerCount = await repository.CountRegisteredCustomersAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
currentMonthStart,
|
||||||
|
nextMonthStart,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var previousMonthNewCustomerCount = await repository.CountRegisteredCustomersAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
previousMonthStart,
|
||||||
|
currentMonthStart,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var currentMonthGiftClaimedCount = await repository.CountGiftClaimedCustomersAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
currentMonthStart,
|
||||||
|
nextMonthStart,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var currentMonthFirstOrderedCount = await repository.CountFirstOrderedCustomersAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
currentMonthStart,
|
||||||
|
nextMonthStart,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var stats = new NewCustomerStatsDto
|
||||||
|
{
|
||||||
|
MonthlyNewCustomers = currentMonthNewCustomerCount,
|
||||||
|
MonthlyGrowthCount = currentMonthNewCustomerCount - previousMonthNewCustomerCount,
|
||||||
|
MonthlyGrowthRatePercent = NewCustomerMapping.ToGrowthRatePercent(
|
||||||
|
currentMonthNewCustomerCount,
|
||||||
|
previousMonthNewCustomerCount),
|
||||||
|
GiftClaimRate = NewCustomerMapping.ToRatePercent(
|
||||||
|
currentMonthGiftClaimedCount,
|
||||||
|
currentMonthNewCustomerCount),
|
||||||
|
GiftClaimedCount = currentMonthGiftClaimedCount,
|
||||||
|
FirstOrderConversionRate = NewCustomerMapping.ToRatePercent(
|
||||||
|
currentMonthFirstOrderedCount,
|
||||||
|
currentMonthNewCustomerCount),
|
||||||
|
FirstOrderedCount = currentMonthFirstOrderedCount
|
||||||
|
};
|
||||||
|
|
||||||
|
var (records, totalCount) = await repository.GetInviteRecordsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
normalizedPage,
|
||||||
|
normalizedPageSize,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return new NewCustomerDetailDto
|
||||||
|
{
|
||||||
|
Settings = settingsDto,
|
||||||
|
Stats = stats,
|
||||||
|
InviteRecords = new NewCustomerInviteRecordListResultDto
|
||||||
|
{
|
||||||
|
Items = records.Select(NewCustomerMapping.ToInviteRecordDto).ToList(),
|
||||||
|
Page = normalizedPage,
|
||||||
|
PageSize = normalizedPageSize,
|
||||||
|
TotalCount = totalCount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerSettingsDto BuildSettingsDto(
|
||||||
|
long storeId,
|
||||||
|
NewCustomerGiftSetting? setting,
|
||||||
|
IReadOnlyList<NewCustomerCouponRule> rules)
|
||||||
|
{
|
||||||
|
var welcomeCoupons = rules
|
||||||
|
.Where(item => item.Scene == NewCustomerCouponScene.Welcome)
|
||||||
|
.Select(NewCustomerMapping.ToCouponRuleDto)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var inviterCoupons = rules
|
||||||
|
.Where(item => item.Scene == NewCustomerCouponScene.InviterReward)
|
||||||
|
.Select(NewCustomerMapping.ToCouponRuleDto)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var inviteeCoupons = rules
|
||||||
|
.Where(item => item.Scene == NewCustomerCouponScene.InviteeReward)
|
||||||
|
.Select(NewCustomerMapping.ToCouponRuleDto)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (setting is null)
|
||||||
|
{
|
||||||
|
return new NewCustomerSettingsDto
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
GiftEnabled = true,
|
||||||
|
GiftType = "coupon",
|
||||||
|
InviteEnabled = true,
|
||||||
|
ShareChannels = ["wechat_friend", "moments"],
|
||||||
|
WelcomeCoupons = welcomeCoupons,
|
||||||
|
InviterCoupons = inviterCoupons,
|
||||||
|
InviteeCoupons = inviteeCoupons,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NewCustomerSettingsDto
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
GiftEnabled = setting.GiftEnabled,
|
||||||
|
GiftType = NewCustomerMapping.ToGiftTypeText(setting.GiftType),
|
||||||
|
DirectReduceAmount = setting.DirectReduceAmount,
|
||||||
|
DirectMinimumSpend = setting.DirectMinimumSpend,
|
||||||
|
InviteEnabled = setting.InviteEnabled,
|
||||||
|
ShareChannels = NewCustomerMapping.DeserializeShareChannels(setting.ShareChannelsJson),
|
||||||
|
WelcomeCoupons = welcomeCoupons,
|
||||||
|
InviterCoupons = inviterCoupons,
|
||||||
|
InviteeCoupons = inviteeCoupons,
|
||||||
|
UpdatedAt = setting.UpdatedAt ?? setting.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询新客邀请记录分页处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetNewCustomerInviteRecordListQueryHandler(
|
||||||
|
INewCustomerGiftRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetNewCustomerInviteRecordListQuery, NewCustomerInviteRecordListResultDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<NewCustomerInviteRecordListResultDto> Handle(
|
||||||
|
GetNewCustomerInviteRecordListQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var normalizedPage = Math.Max(1, request.Page);
|
||||||
|
var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||||
|
|
||||||
|
var (items, totalCount) = await repository.GetInviteRecordsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
normalizedPage,
|
||||||
|
normalizedPageSize,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return new NewCustomerInviteRecordListResultDto
|
||||||
|
{
|
||||||
|
Items = items.Select(NewCustomerMapping.ToInviteRecordDto).ToList(),
|
||||||
|
Page = normalizedPage,
|
||||||
|
PageSize = normalizedPageSize,
|
||||||
|
TotalCount = totalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存新客有礼配置处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveNewCustomerSettingsCommandHandler(
|
||||||
|
INewCustomerGiftRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SaveNewCustomerSettingsCommand, NewCustomerSettingsDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<NewCustomerSettingsDto> Handle(
|
||||||
|
SaveNewCustomerSettingsCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.StoreId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "storeId 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var giftType = NewCustomerMapping.ParseGiftType(request.GiftType);
|
||||||
|
var shareChannels = NewCustomerMapping.NormalizeShareChannels(request.ShareChannels);
|
||||||
|
|
||||||
|
var welcomeRules = NewCustomerMapping.NormalizeCouponRulesForSave(
|
||||||
|
request.StoreId,
|
||||||
|
NewCustomerCouponScene.Welcome,
|
||||||
|
request.WelcomeCoupons);
|
||||||
|
|
||||||
|
var inviterRules = NewCustomerMapping.NormalizeCouponRulesForSave(
|
||||||
|
request.StoreId,
|
||||||
|
NewCustomerCouponScene.InviterReward,
|
||||||
|
request.InviterCoupons);
|
||||||
|
|
||||||
|
var inviteeRules = NewCustomerMapping.NormalizeCouponRulesForSave(
|
||||||
|
request.StoreId,
|
||||||
|
NewCustomerCouponScene.InviteeReward,
|
||||||
|
request.InviteeCoupons);
|
||||||
|
|
||||||
|
if (giftType == NewCustomerGiftType.Coupon && welcomeRules.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "优惠券包至少需要一张券");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (giftType == NewCustomerGiftType.Direct)
|
||||||
|
{
|
||||||
|
if (!request.DirectReduceAmount.HasValue || request.DirectReduceAmount.Value <= 0m)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "directReduceAmount 必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.DirectMinimumSpend.HasValue || request.DirectMinimumSpend.Value < 0m)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "directMinimumSpend 不能小于 0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.InviteEnabled && (inviterRules.Count == 0 || inviteeRules.Count == 0))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "开启邀请后必须配置邀请人和被邀请人奖励券");
|
||||||
|
}
|
||||||
|
|
||||||
|
var setting = await repository.FindSettingByStoreIdAsync(tenantId, request.StoreId, cancellationToken);
|
||||||
|
var isNewSetting = setting is null;
|
||||||
|
if (setting is null)
|
||||||
|
{
|
||||||
|
setting = new NewCustomerGiftSetting
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId
|
||||||
|
};
|
||||||
|
|
||||||
|
await repository.AddSettingAsync(setting, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
setting.GiftEnabled = request.GiftEnabled;
|
||||||
|
setting.GiftType = giftType;
|
||||||
|
setting.DirectReduceAmount = giftType == NewCustomerGiftType.Direct
|
||||||
|
? decimal.Round(request.DirectReduceAmount!.Value, 2, MidpointRounding.AwayFromZero)
|
||||||
|
: null;
|
||||||
|
setting.DirectMinimumSpend = giftType == NewCustomerGiftType.Direct
|
||||||
|
? decimal.Round(request.DirectMinimumSpend!.Value, 2, MidpointRounding.AwayFromZero)
|
||||||
|
: null;
|
||||||
|
setting.InviteEnabled = request.InviteEnabled;
|
||||||
|
setting.ShareChannelsJson = NewCustomerMapping.SerializeShareChannels(shareChannels);
|
||||||
|
|
||||||
|
if (!isNewSetting)
|
||||||
|
{
|
||||||
|
await repository.UpdateSettingAsync(setting, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var allRules = new List<NewCustomerCouponRule>(welcomeRules.Count + inviterRules.Count + inviteeRules.Count);
|
||||||
|
allRules.AddRange(welcomeRules);
|
||||||
|
allRules.AddRange(inviterRules);
|
||||||
|
allRules.AddRange(inviteeRules);
|
||||||
|
|
||||||
|
await repository.ReplaceCouponRulesAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
allRules,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new NewCustomerSettingsDto
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
GiftEnabled = setting.GiftEnabled,
|
||||||
|
GiftType = NewCustomerMapping.ToGiftTypeText(setting.GiftType),
|
||||||
|
DirectReduceAmount = setting.DirectReduceAmount,
|
||||||
|
DirectMinimumSpend = setting.DirectMinimumSpend,
|
||||||
|
InviteEnabled = setting.InviteEnabled,
|
||||||
|
ShareChannels = shareChannels,
|
||||||
|
WelcomeCoupons = welcomeRules.Select(NewCustomerMapping.ToCouponRuleDto).ToList(),
|
||||||
|
InviterCoupons = inviterRules.Select(NewCustomerMapping.ToCouponRuleDto).ToList(),
|
||||||
|
InviteeCoupons = inviteeRules.Select(NewCustomerMapping.ToCouponRuleDto).ToList(),
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客成长记录处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteNewCustomerGrowthRecordCommandHandler(
|
||||||
|
INewCustomerGiftRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<WriteNewCustomerGrowthRecordCommand, NewCustomerGrowthRecordDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<NewCustomerGrowthRecordDto> Handle(
|
||||||
|
WriteNewCustomerGrowthRecordCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.StoreId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "storeId 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var customerKey = NewCustomerMapping.NormalizeCustomerKey(request.CustomerKey);
|
||||||
|
var customerName = NewCustomerMapping.NormalizeOptionalText(request.CustomerName, "customerName", 64);
|
||||||
|
var sourceChannel = NewCustomerMapping.NormalizeOptionalText(request.SourceChannel, "sourceChannel", 32);
|
||||||
|
var registeredAt = NewCustomerMapping.NormalizeUtc(request.RegisteredAt);
|
||||||
|
DateTime? giftClaimedAt = request.GiftClaimedAt.HasValue
|
||||||
|
? NewCustomerMapping.NormalizeUtc(request.GiftClaimedAt.Value)
|
||||||
|
: null;
|
||||||
|
DateTime? firstOrderAt = request.FirstOrderAt.HasValue
|
||||||
|
? NewCustomerMapping.NormalizeUtc(request.FirstOrderAt.Value)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var entity = await repository.FindGrowthRecordByCustomerKeyAsync(
|
||||||
|
tenantId: tenantId,
|
||||||
|
storeId: request.StoreId,
|
||||||
|
customerKey: customerKey,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
if (entity is null)
|
||||||
|
{
|
||||||
|
entity = new NewCustomerGrowthRecord
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
CustomerKey = customerKey,
|
||||||
|
CustomerName = customerName,
|
||||||
|
RegisteredAt = registeredAt,
|
||||||
|
GiftClaimedAt = giftClaimedAt,
|
||||||
|
FirstOrderAt = firstOrderAt,
|
||||||
|
SourceChannel = sourceChannel
|
||||||
|
};
|
||||||
|
|
||||||
|
await repository.AddGrowthRecordAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
return NewCustomerMapping.ToGrowthRecordDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(customerName))
|
||||||
|
{
|
||||||
|
entity.CustomerName = customerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(sourceChannel))
|
||||||
|
{
|
||||||
|
entity.SourceChannel = sourceChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.RegisteredAt = registeredAt < entity.RegisteredAt
|
||||||
|
? registeredAt
|
||||||
|
: entity.RegisteredAt;
|
||||||
|
entity.GiftClaimedAt = MergeNullableDate(entity.GiftClaimedAt, giftClaimedAt);
|
||||||
|
entity.FirstOrderAt = MergeNullableDate(entity.FirstOrderAt, firstOrderAt);
|
||||||
|
|
||||||
|
await repository.UpdateGrowthRecordAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
return NewCustomerMapping.ToGrowthRecordDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? MergeNullableDate(DateTime? existing, DateTime? incoming)
|
||||||
|
{
|
||||||
|
if (!existing.HasValue)
|
||||||
|
{
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!incoming.HasValue)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return incoming.Value < existing.Value ? incoming : existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入新客邀请记录处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteNewCustomerInviteRecordCommandHandler(
|
||||||
|
INewCustomerGiftRepository repository)
|
||||||
|
: IRequestHandler<WriteNewCustomerInviteRecordCommand, NewCustomerInviteRecordDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<NewCustomerInviteRecordDto> Handle(
|
||||||
|
WriteNewCustomerInviteRecordCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.StoreId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "storeId 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
var inviterName = NewCustomerMapping.NormalizeDisplayName(request.InviterName, "inviterName");
|
||||||
|
var inviteeName = NewCustomerMapping.NormalizeDisplayName(request.InviteeName, "inviteeName");
|
||||||
|
var orderStatus = NewCustomerMapping.ParseInviteOrderStatus(request.OrderStatus);
|
||||||
|
var rewardStatus = NewCustomerMapping.ParseInviteRewardStatus(request.RewardStatus);
|
||||||
|
var sourceChannel = NewCustomerMapping.NormalizeOptionalText(request.SourceChannel, "sourceChannel", 32);
|
||||||
|
|
||||||
|
var inviteTime = NewCustomerMapping.NormalizeUtc(request.InviteTime);
|
||||||
|
DateTime? rewardIssuedAt = rewardStatus == NewCustomerInviteRewardStatus.Issued
|
||||||
|
? request.RewardIssuedAt.HasValue
|
||||||
|
? NewCustomerMapping.NormalizeUtc(request.RewardIssuedAt.Value)
|
||||||
|
: DateTime.UtcNow
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var entity = new NewCustomerInviteRecord
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
InviterName = inviterName,
|
||||||
|
InviteeName = inviteeName,
|
||||||
|
InviteTime = inviteTime,
|
||||||
|
OrderStatus = orderStatus,
|
||||||
|
RewardStatus = rewardStatus,
|
||||||
|
RewardIssuedAt = rewardIssuedAt,
|
||||||
|
SourceChannel = sourceChannel
|
||||||
|
};
|
||||||
|
|
||||||
|
await repository.AddInviteRecordAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return NewCustomerMapping.ToInviteRecordDto(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼映射与规则校验。
|
||||||
|
/// </summary>
|
||||||
|
internal static class NewCustomerMapping
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> AllowedShareChannels =
|
||||||
|
[
|
||||||
|
"wechat_friend",
|
||||||
|
"moments",
|
||||||
|
"sms"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public static NewCustomerGiftType ParseGiftType(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"coupon" => NewCustomerGiftType.Coupon,
|
||||||
|
"direct" => NewCustomerGiftType.Direct,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "giftType 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToGiftTypeText(NewCustomerGiftType value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
NewCustomerGiftType.Coupon => "coupon",
|
||||||
|
NewCustomerGiftType.Direct => "direct",
|
||||||
|
_ => "coupon"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NewCustomerCouponScene ParseCouponScene(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"welcome" => NewCustomerCouponScene.Welcome,
|
||||||
|
"inviter" => NewCustomerCouponScene.InviterReward,
|
||||||
|
"invitee" => NewCustomerCouponScene.InviteeReward,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "scene 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToCouponSceneText(NewCustomerCouponScene value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
NewCustomerCouponScene.Welcome => "welcome",
|
||||||
|
NewCustomerCouponScene.InviterReward => "inviter",
|
||||||
|
NewCustomerCouponScene.InviteeReward => "invitee",
|
||||||
|
_ => "welcome"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NewCustomerCouponType ParseCouponType(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"amount_off" => NewCustomerCouponType.AmountOff,
|
||||||
|
"discount" => NewCustomerCouponType.Discount,
|
||||||
|
"free_shipping" => NewCustomerCouponType.FreeShipping,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "couponType 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToCouponTypeText(NewCustomerCouponType value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
NewCustomerCouponType.AmountOff => "amount_off",
|
||||||
|
NewCustomerCouponType.Discount => "discount",
|
||||||
|
NewCustomerCouponType.FreeShipping => "free_shipping",
|
||||||
|
_ => "amount_off"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NewCustomerInviteOrderStatus ParseInviteOrderStatus(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"pending_order" => NewCustomerInviteOrderStatus.PendingOrder,
|
||||||
|
"ordered" => NewCustomerInviteOrderStatus.Ordered,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "orderStatus 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToInviteOrderStatusText(NewCustomerInviteOrderStatus value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
NewCustomerInviteOrderStatus.PendingOrder => "pending_order",
|
||||||
|
NewCustomerInviteOrderStatus.Ordered => "ordered",
|
||||||
|
_ => "pending_order"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NewCustomerInviteRewardStatus ParseInviteRewardStatus(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"pending" => NewCustomerInviteRewardStatus.Pending,
|
||||||
|
"issued" => NewCustomerInviteRewardStatus.Issued,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "rewardStatus 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToInviteRewardStatusText(NewCustomerInviteRewardStatus value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
NewCustomerInviteRewardStatus.Pending => "pending",
|
||||||
|
NewCustomerInviteRewardStatus.Issued => "issued",
|
||||||
|
_ => "pending"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime NormalizeUtc(DateTime value)
|
||||||
|
{
|
||||||
|
return value.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Utc => value,
|
||||||
|
DateTimeKind.Local => value.ToUniversalTime(),
|
||||||
|
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime StartOfMonthUtc(DateTime nowUtc)
|
||||||
|
{
|
||||||
|
var utc = NormalizeUtc(nowUtc);
|
||||||
|
return new DateTime(utc.Year, utc.Month, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal ToRatePercent(int numerator, int denominator)
|
||||||
|
{
|
||||||
|
if (denominator <= 0 || numerator <= 0)
|
||||||
|
{
|
||||||
|
return 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(numerator * 100m / denominator, 1, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal ToGrowthRatePercent(int currentValue, int previousValue)
|
||||||
|
{
|
||||||
|
if (previousValue <= 0)
|
||||||
|
{
|
||||||
|
return currentValue > 0 ? 100m : 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(
|
||||||
|
(currentValue - previousValue) * 100m / previousValue,
|
||||||
|
1,
|
||||||
|
MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SerializeShareChannels(IReadOnlyCollection<string> channels)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(channels, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> DeserializeShareChannels(string? payload)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = JsonSerializer.Deserialize<List<string>>(payload, JsonOptions) ?? [];
|
||||||
|
return NormalizeShareChannels(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> NormalizeShareChannels(IEnumerable<string>? values)
|
||||||
|
{
|
||||||
|
var normalized = (values ?? [])
|
||||||
|
.Select(item => (item ?? string.Empty).Trim().ToLowerInvariant())
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (normalized.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "shareChannels 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Any(item => !AllowedShareChannels.Contains(item)))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "shareChannels 存在非法值");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<NewCustomerCouponRule> NormalizeCouponRulesForSave(
|
||||||
|
long storeId,
|
||||||
|
NewCustomerCouponScene scene,
|
||||||
|
IReadOnlyCollection<NewCustomerSaveCouponRuleInputDto>? values)
|
||||||
|
{
|
||||||
|
var rules = (values ?? [])
|
||||||
|
.Select((item, index) => NormalizeCouponRuleForSave(storeId, scene, item, index + 1))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NewCustomerCouponRuleDto ToCouponRuleDto(NewCustomerCouponRule source)
|
||||||
|
{
|
||||||
|
return new NewCustomerCouponRuleDto
|
||||||
|
{
|
||||||
|
Id = source.Id,
|
||||||
|
Scene = ToCouponSceneText(source.Scene),
|
||||||
|
CouponType = ToCouponTypeText(source.CouponType),
|
||||||
|
Value = source.Value,
|
||||||
|
MinimumSpend = source.MinimumSpend,
|
||||||
|
ValidDays = source.ValidDays,
|
||||||
|
SortOrder = source.SortOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NewCustomerInviteRecordDto ToInviteRecordDto(NewCustomerInviteRecord source)
|
||||||
|
{
|
||||||
|
return new NewCustomerInviteRecordDto
|
||||||
|
{
|
||||||
|
Id = source.Id,
|
||||||
|
InviterName = source.InviterName,
|
||||||
|
InviteeName = source.InviteeName,
|
||||||
|
InviteTime = source.InviteTime,
|
||||||
|
OrderStatus = ToInviteOrderStatusText(source.OrderStatus),
|
||||||
|
RewardStatus = ToInviteRewardStatusText(source.RewardStatus),
|
||||||
|
RewardIssuedAt = source.RewardIssuedAt,
|
||||||
|
SourceChannel = source.SourceChannel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NewCustomerGrowthRecordDto ToGrowthRecordDto(NewCustomerGrowthRecord source)
|
||||||
|
{
|
||||||
|
return new NewCustomerGrowthRecordDto
|
||||||
|
{
|
||||||
|
Id = source.Id,
|
||||||
|
CustomerKey = source.CustomerKey,
|
||||||
|
CustomerName = source.CustomerName,
|
||||||
|
RegisteredAt = source.RegisteredAt,
|
||||||
|
GiftClaimedAt = source.GiftClaimedAt,
|
||||||
|
FirstOrderAt = source.FirstOrderAt,
|
||||||
|
SourceChannel = source.SourceChannel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeDisplayName(string? value, string fieldName)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 64)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 长度不能超过 64");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeCustomerKey(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "customerKey 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 64)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "customerKey 长度不能超过 64");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? NormalizeOptionalText(string? value, string fieldName, int maxLength)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (normalized.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > maxLength)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 长度不能超过 {maxLength}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NewCustomerCouponRule NormalizeCouponRuleForSave(
|
||||||
|
long storeId,
|
||||||
|
NewCustomerCouponScene scene,
|
||||||
|
NewCustomerSaveCouponRuleInputDto value,
|
||||||
|
int sortOrder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
||||||
|
var couponType = ParseCouponType(value.CouponType);
|
||||||
|
var minimumSpend = NormalizeNonNegativeMoney(value.MinimumSpend, "minimumSpend");
|
||||||
|
decimal? normalizedValue = couponType switch
|
||||||
|
{
|
||||||
|
NewCustomerCouponType.AmountOff => NormalizePositiveMoney(value.Value, "value"),
|
||||||
|
NewCustomerCouponType.Discount => NormalizeDiscount(value.Value),
|
||||||
|
NewCustomerCouponType.FreeShipping => null,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "couponType 参数不合法")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (couponType == NewCustomerCouponType.AmountOff &&
|
||||||
|
minimumSpend.HasValue &&
|
||||||
|
normalizedValue.HasValue &&
|
||||||
|
normalizedValue.Value >= minimumSpend.Value &&
|
||||||
|
minimumSpend.Value > 0m)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "满减券 value 必须小于 minimumSpend");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.ValidDays is < 1 or > 365)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "validDays 必须在 1-365 之间");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NewCustomerCouponRule
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
Scene = scene,
|
||||||
|
CouponType = couponType,
|
||||||
|
Value = normalizedValue,
|
||||||
|
MinimumSpend = minimumSpend,
|
||||||
|
ValidDays = value.ValidDays,
|
||||||
|
SortOrder = sortOrder
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal? NormalizeNonNegativeMoney(decimal? value, string fieldName)
|
||||||
|
{
|
||||||
|
if (!value.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Value < 0m)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 不能小于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(value.Value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal NormalizePositiveMoney(decimal? value, string fieldName)
|
||||||
|
{
|
||||||
|
if (!value.HasValue || value.Value <= 0m)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 必须大于 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(value.Value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal NormalizeDiscount(decimal? value)
|
||||||
|
{
|
||||||
|
if (!value.HasValue || value.Value <= 0m || value.Value >= 10m)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "折扣券 value 必须在 0-10 之间");
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(value.Value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询新客有礼详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetNewCustomerDetailQuery : IRequest<NewCustomerDetailDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录页码。
|
||||||
|
/// </summary>
|
||||||
|
public int RecordPage { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int RecordPageSize { get; init; } = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.NewCustomer.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.NewCustomer.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询新客邀请记录分页。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetNewCustomerInviteRecordListQuery : IRequest<NewCustomerInviteRecordListResultDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改次卡模板状态命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChangePunchCardTemplateStatusCommand : IRequest<PunchCardDetailDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TemplateId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; init; } = "disabled";
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除次卡模板命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeletePunchCardTemplateCommand : IRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TemplateId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存次卡模板命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SavePunchCardTemplateCommand : IRequest<PunchCardDetailDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID(编辑时传)。
|
||||||
|
/// </summary>
|
||||||
|
public long? TemplateId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 封面图。
|
||||||
|
/// </summary>
|
||||||
|
public string? CoverImageUrl { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 售价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal SalePrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? OriginalPrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTimes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期类型(days/range)。
|
||||||
|
/// </summary>
|
||||||
|
public string ValidityType { get; init; } = "days";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定天数。
|
||||||
|
/// </summary>
|
||||||
|
public int? ValidityDays { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定开始日期。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ValidFrom { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定结束日期。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ValidTo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 范围类型(all/category/tag/product)。
|
||||||
|
/// </summary>
|
||||||
|
public string ScopeType { get; init; } = "all";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<long> ScopeCategoryIds { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定标签 ID。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<long> ScopeTagIds { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<long> ScopeProductIds { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用模式(free/cap)。
|
||||||
|
/// </summary>
|
||||||
|
public string UsageMode { get; init; } = "free";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次上限金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? UsageCapAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每日限用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int? DailyLimit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每单限用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int? PerOrderLimit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每人限购张数。
|
||||||
|
/// </summary>
|
||||||
|
public int? PerUserPurchaseLimit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否允许转赠。
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowTransfer { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过期策略(invalidate/refund)。
|
||||||
|
/// </summary>
|
||||||
|
public string ExpireStrategy { get; init; } = "invalidate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡说明。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通知渠道(in_app/sms)。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<string> NotifyChannels { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入次卡使用记录命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WritePunchCardUsageRecordCommand : IRequest<PunchCardUsageRecordDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TemplateId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡实例 ID(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public long? InstanceId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡实例编号(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public string? InstanceNo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员名称(当未指定实例时用于创建实例)。
|
||||||
|
/// </summary>
|
||||||
|
public string? MemberName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员手机号(脱敏,当未指定实例时用于创建实例)。
|
||||||
|
/// </summary>
|
||||||
|
public string? MemberPhoneMasked { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兑换商品名称。
|
||||||
|
/// </summary>
|
||||||
|
public string ProductName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用时间(可空,空则取当前 UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? UsedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int UsedTimes { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超额补差金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? ExtraPayAmount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardDetailDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 封面图。
|
||||||
|
/// </summary>
|
||||||
|
public string? CoverImageUrl { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 售价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal SalePrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? OriginalPrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTimes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期类型(days/range)。
|
||||||
|
/// </summary>
|
||||||
|
public string ValidityType { get; init; } = "days";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定天数。
|
||||||
|
/// </summary>
|
||||||
|
public int? ValidityDays { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定开始日期(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ValidFrom { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固定结束日期(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ValidTo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用范围。
|
||||||
|
/// </summary>
|
||||||
|
public PunchCardScopeDto Scope { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用模式(free/cap)。
|
||||||
|
/// </summary>
|
||||||
|
public string UsageMode { get; init; } = "free";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额上限。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? UsageCapAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每日限用。
|
||||||
|
/// </summary>
|
||||||
|
public int? DailyLimit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每单限用。
|
||||||
|
/// </summary>
|
||||||
|
public int? PerOrderLimit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每人限购。
|
||||||
|
/// </summary>
|
||||||
|
public int? PerUserPurchaseLimit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否允许转赠。
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowTransfer { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过期策略(invalidate/refund)。
|
||||||
|
/// </summary>
|
||||||
|
public string ExpireStrategy { get; init; } = "invalidate";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通知渠道(in_app/sms)。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> NotifyChannels { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; init; } = "enabled";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已售数量。
|
||||||
|
/// </summary>
|
||||||
|
public int SoldCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用中数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ActiveCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计收入。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RevenueAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UpdatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡列表项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardListItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 封面图。
|
||||||
|
/// </summary>
|
||||||
|
public string? CoverImageUrl { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 售价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal SalePrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原价。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? OriginalPrice { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTimes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期展示文案。
|
||||||
|
/// </summary>
|
||||||
|
public string ValiditySummary { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 适用范围类型(all/category/tag/product)。
|
||||||
|
/// </summary>
|
||||||
|
public string ScopeType { get; init; } = "all";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用模式(free/cap)。
|
||||||
|
/// </summary>
|
||||||
|
public string UsageMode { get; init; } = "free";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单次使用上限金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? UsageCapAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每日限用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int? DailyLimit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; init; } = "enabled";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否弱化展示。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDimmed { 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 DateTime UpdatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板列表结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardListResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PunchCardListItemDto> Items { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前页。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计数据。
|
||||||
|
/// </summary>
|
||||||
|
public PunchCardStatsDto Stats { get; init; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡范围规则。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardScopeDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 范围类型(all/category/tag/product)。
|
||||||
|
/// </summary>
|
||||||
|
public string ScopeType { get; init; } = "all";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定分类 ID。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<long> CategoryIds { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定标签 ID。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<long> TagIds { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定商品 ID。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<long> ProductIds { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板统计。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardStatsDto
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡过滤选项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardTemplateOptionDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TemplateId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录项。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageRecordDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 使用记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用单号。
|
||||||
|
/// </summary>
|
||||||
|
public string RecordNo { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long PunchCardTemplateId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡名称。
|
||||||
|
/// </summary>
|
||||||
|
public string PunchCardName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡实例 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long PunchCardInstanceId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员名称。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员手机号(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberPhoneMasked { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兑换商品名称。
|
||||||
|
/// </summary>
|
||||||
|
public string ProductName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UsedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本次使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int UsedTimes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用后剩余次数。
|
||||||
|
/// </summary>
|
||||||
|
public int RemainingTimesAfterUse { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalTimes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(normal/almost_used_up/used_up/expired)。
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayStatus { get; init; } = "normal";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 超额补差金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? ExtraPayAmount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录导出结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageRecordExportDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件内容(Base64)。
|
||||||
|
/// </summary>
|
||||||
|
public string FileContentBase64 { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录分页结果。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageRecordListResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表数据。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PunchCardUsageRecordDto> Items { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计数据。
|
||||||
|
/// </summary>
|
||||||
|
public PunchCardUsageStatsDto Stats { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡筛选选项。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<PunchCardTemplateOptionDto> TemplateOptions { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录统计。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PunchCardUsageStatsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 今日使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int TodayUsedCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月使用次数。
|
||||||
|
/// </summary>
|
||||||
|
public int MonthUsedCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 7 天内即将过期数量。
|
||||||
|
/// </summary>
|
||||||
|
public int ExpiringSoonCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡状态变更处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChangePunchCardTemplateStatusCommandHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<ChangePunchCardTemplateStatusCommand, PunchCardDetailDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardDetailDto> Handle(
|
||||||
|
ChangePunchCardTemplateStatusCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var normalizedStatus = PunchCardMapping.ParseTemplateStatus(request.Status);
|
||||||
|
|
||||||
|
var entity = await repository.FindTemplateByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在");
|
||||||
|
|
||||||
|
entity.Status = normalizedStatus;
|
||||||
|
|
||||||
|
await repository.UpdateTemplateAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var aggregateMap = await repository.GetTemplateAggregateByTemplateIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
[entity.Id],
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var aggregate = aggregateMap.TryGetValue(entity.Id, out var value)
|
||||||
|
? value
|
||||||
|
: PunchCardDtoFactory.EmptyAggregate(entity.Id);
|
||||||
|
|
||||||
|
return PunchCardDtoFactory.ToDetailDto(entity, aggregate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除次卡模板处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeletePunchCardTemplateCommandHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<DeletePunchCardTemplateCommand>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Handle(DeletePunchCardTemplateCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var entity = await repository.FindTemplateByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在");
|
||||||
|
|
||||||
|
var aggregate = await repository.GetTemplateAggregateByTemplateIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
[entity.Id],
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (aggregate.TryGetValue(entity.Id, out var snapshot) && snapshot.SoldCount > 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "已售出的次卡不可删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
await repository.DeleteTemplateAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using System.Text;
|
||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出次卡使用记录处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportPunchCardUsageRecordCsvQueryHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<ExportPunchCardUsageRecordCsvQuery, PunchCardUsageRecordExportDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardUsageRecordExportDto> Handle(
|
||||||
|
ExportPunchCardUsageRecordCsvQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var normalizedStatus = PunchCardMapping.ParseUsageStatusFilter(request.Status);
|
||||||
|
|
||||||
|
var records = await repository.ListUsageRecordsForExportAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId,
|
||||||
|
request.Keyword,
|
||||||
|
normalizedStatus,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (records.Count == 0)
|
||||||
|
{
|
||||||
|
return new PunchCardUsageRecordExportDto
|
||||||
|
{
|
||||||
|
FileName = $"次卡使用记录_{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
|
||||||
|
FileContentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes("\uFEFF使用单号,会员,手机号,次卡,兑换商品,使用时间,剩余次数,总次数,状态\n")),
|
||||||
|
TotalCount = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var instanceIds = records.Select(item => item.PunchCardInstanceId).Distinct().ToList();
|
||||||
|
var instances = await repository.GetInstancesByIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
instanceIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var instanceMap = instances.ToDictionary(item => item.Id, item => item);
|
||||||
|
|
||||||
|
var templateIds = records.Select(item => item.PunchCardTemplateId)
|
||||||
|
.Concat(instances.Select(item => item.PunchCardTemplateId))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var templates = await repository.GetTemplatesByIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
templateIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var templateMap = templates.ToDictionary(item => item.Id, item => item);
|
||||||
|
|
||||||
|
var csv = BuildCsv(records, instanceMap, templateMap);
|
||||||
|
var bytes = Encoding.UTF8.GetBytes($"\uFEFF{csv}");
|
||||||
|
|
||||||
|
return new PunchCardUsageRecordExportDto
|
||||||
|
{
|
||||||
|
FileName = $"次卡使用记录_{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
|
||||||
|
FileContentBase64 = Convert.ToBase64String(bytes),
|
||||||
|
TotalCount = records.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildCsv(
|
||||||
|
IReadOnlyCollection<Domain.Coupons.Entities.PunchCardUsageRecord> records,
|
||||||
|
IReadOnlyDictionary<long, Domain.Coupons.Entities.PunchCardInstance> instanceMap,
|
||||||
|
IReadOnlyDictionary<long, Domain.Coupons.Entities.PunchCardTemplate> templateMap)
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
"使用单号,会员,手机号,次卡,兑换商品,使用时间,剩余次数,总次数,状态"
|
||||||
|
};
|
||||||
|
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
foreach (var record in records)
|
||||||
|
{
|
||||||
|
instanceMap.TryGetValue(record.PunchCardInstanceId, out var instance);
|
||||||
|
templateMap.TryGetValue(record.PunchCardTemplateId, out var template);
|
||||||
|
|
||||||
|
var dto = PunchCardDtoFactory.ToUsageRecordDto(record, instance, template, nowUtc);
|
||||||
|
var statusText = ResolveStatusText(dto.DisplayStatus);
|
||||||
|
|
||||||
|
lines.Add(string.Join(",",
|
||||||
|
Escape(dto.RecordNo),
|
||||||
|
Escape(dto.MemberName),
|
||||||
|
Escape(dto.MemberPhoneMasked),
|
||||||
|
Escape(dto.PunchCardName),
|
||||||
|
Escape(dto.ProductName),
|
||||||
|
Escape(dto.UsedAt.ToString("yyyy-MM-dd HH:mm:ss")),
|
||||||
|
dto.RemainingTimesAfterUse,
|
||||||
|
dto.TotalTimes,
|
||||||
|
Escape(statusText)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join('\n', lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveStatusText(string value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
"normal" => "正常使用",
|
||||||
|
"almost_used_up" => "即将用完",
|
||||||
|
"used_up" => "已用完",
|
||||||
|
"expired" => "已过期",
|
||||||
|
_ => "正常使用"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Escape(string value)
|
||||||
|
{
|
||||||
|
var text = value.Replace("\"", "\"\"");
|
||||||
|
return $"\"{text}\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板详情查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPunchCardTemplateDetailQueryHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetPunchCardTemplateDetailQuery, PunchCardDetailDto?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardDetailDto?> Handle(
|
||||||
|
GetPunchCardTemplateDetailQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
|
||||||
|
var template = await repository.FindTemplateByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (template is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aggregate = await repository.GetTemplateAggregateByTemplateIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
[request.TemplateId],
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var snapshot = aggregate.TryGetValue(template.Id, out var value)
|
||||||
|
? value
|
||||||
|
: PunchCardDtoFactory.EmptyAggregate(template.Id);
|
||||||
|
|
||||||
|
return PunchCardDtoFactory.ToDetailDto(template, snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPunchCardTemplateListQueryHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetPunchCardTemplateListQuery, PunchCardListResultDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardListResultDto> Handle(
|
||||||
|
GetPunchCardTemplateListQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var page = Math.Max(1, request.Page);
|
||||||
|
var pageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||||
|
var status = PunchCardMapping.ParseTemplateStatusFilter(request.Status);
|
||||||
|
|
||||||
|
var (items, totalCount) = await repository.SearchTemplatesAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.Keyword,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var templateIds = items.Select(item => item.Id).ToList();
|
||||||
|
var aggregates = await repository.GetTemplateAggregateByTemplateIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
templateIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var mappedItems = items
|
||||||
|
.Select(item =>
|
||||||
|
{
|
||||||
|
var aggregate = aggregates.TryGetValue(item.Id, out var value)
|
||||||
|
? value
|
||||||
|
: PunchCardDtoFactory.EmptyAggregate(item.Id);
|
||||||
|
return PunchCardDtoFactory.ToListItemDto(item, aggregate);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var statsSnapshot = await repository.GetTemplateStatsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return new PunchCardListResultDto
|
||||||
|
{
|
||||||
|
Items = mappedItems,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Stats = PunchCardDtoFactory.ToStatsDto(statsSnapshot)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPunchCardUsageRecordListQueryHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetPunchCardUsageRecordListQuery, PunchCardUsageRecordListResultDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardUsageRecordListResultDto> Handle(
|
||||||
|
GetPunchCardUsageRecordListQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var page = Math.Max(1, request.Page);
|
||||||
|
var pageSize = Math.Clamp(request.PageSize, 1, 500);
|
||||||
|
var normalizedStatus = PunchCardMapping.ParseUsageStatusFilter(request.Status);
|
||||||
|
|
||||||
|
var (records, totalCount) = await repository.SearchUsageRecordsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId,
|
||||||
|
request.Keyword,
|
||||||
|
normalizedStatus,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var instanceIds = records.Select(item => item.PunchCardInstanceId).Distinct().ToList();
|
||||||
|
var instances = await repository.GetInstancesByIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
instanceIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var instanceMap = instances.ToDictionary(item => item.Id, item => item);
|
||||||
|
|
||||||
|
var templateIds = records.Select(item => item.PunchCardTemplateId)
|
||||||
|
.Concat(instances.Select(item => item.PunchCardTemplateId))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var templates = await repository.GetTemplatesByIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
templateIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var templateMap = templates.ToDictionary(item => item.Id, item => item);
|
||||||
|
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var mappedRecords = records
|
||||||
|
.Select(record =>
|
||||||
|
{
|
||||||
|
instanceMap.TryGetValue(record.PunchCardInstanceId, out var instance);
|
||||||
|
templateMap.TryGetValue(record.PunchCardTemplateId, out var template);
|
||||||
|
return PunchCardDtoFactory.ToUsageRecordDto(record, instance, template, nowUtc);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var usageStats = await repository.GetUsageStatsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId,
|
||||||
|
nowUtc,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var (templateRows, _) = await repository.SearchTemplatesAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
500,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var templateOptions = templateRows
|
||||||
|
.OrderBy(item => item.Name, StringComparer.Ordinal)
|
||||||
|
.Select(item => new PunchCardTemplateOptionDto
|
||||||
|
{
|
||||||
|
TemplateId = item.Id,
|
||||||
|
Name = item.Name
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new PunchCardUsageRecordListResultDto
|
||||||
|
{
|
||||||
|
Items = mappedRecords,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Stats = PunchCardDtoFactory.ToUsageStatsDto(usageStats),
|
||||||
|
TemplateOptions = templateOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板保存处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SavePunchCardTemplateCommandHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SavePunchCardTemplateCommand, PunchCardDetailDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardDetailDto> Handle(
|
||||||
|
SavePunchCardTemplateCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
|
||||||
|
var normalizedName = PunchCardMapping.NormalizeName(request.Name);
|
||||||
|
var normalizedCoverImageUrl = PunchCardMapping.NormalizeOptionalCoverUrl(request.CoverImageUrl);
|
||||||
|
var normalizedSalePrice = PunchCardMapping.NormalizeAmount(request.SalePrice, "salePrice", false);
|
||||||
|
var normalizedOriginalPrice = PunchCardMapping.NormalizeOptionalAmount(request.OriginalPrice, "originalPrice", true);
|
||||||
|
var normalizedTotalTimes = PunchCardMapping.NormalizeRequiredPositiveInt(request.TotalTimes, "totalTimes", 10_000);
|
||||||
|
|
||||||
|
if (normalizedOriginalPrice.HasValue && normalizedOriginalPrice.Value < normalizedSalePrice)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "originalPrice 不能小于 salePrice");
|
||||||
|
}
|
||||||
|
|
||||||
|
var validityType = PunchCardMapping.ParseValidityType(request.ValidityType);
|
||||||
|
var (normalizedValidityDays, normalizedValidFrom, normalizedValidTo) = PunchCardMapping.NormalizeValidity(
|
||||||
|
validityType,
|
||||||
|
request.ValidityDays,
|
||||||
|
request.ValidFrom,
|
||||||
|
request.ValidTo);
|
||||||
|
|
||||||
|
var scopeType = PunchCardMapping.ParseScopeType(request.ScopeType);
|
||||||
|
var (normalizedCategoryIds, normalizedTagIds, normalizedProductIds) = PunchCardMapping.NormalizeScopeIds(
|
||||||
|
scopeType,
|
||||||
|
request.ScopeCategoryIds,
|
||||||
|
request.ScopeTagIds,
|
||||||
|
request.ScopeProductIds);
|
||||||
|
|
||||||
|
var usageMode = PunchCardMapping.ParseUsageMode(request.UsageMode);
|
||||||
|
var normalizedUsageCapAmount = usageMode switch
|
||||||
|
{
|
||||||
|
PunchCardUsageMode.Free => null,
|
||||||
|
PunchCardUsageMode.Cap => PunchCardMapping.NormalizeOptionalAmount(request.UsageCapAmount, "usageCapAmount", false),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (usageMode == PunchCardUsageMode.Cap && !normalizedUsageCapAmount.HasValue)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "usageCapAmount 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedDailyLimit = PunchCardMapping.NormalizeOptionalLimit(request.DailyLimit, "dailyLimit", normalizedTotalTimes);
|
||||||
|
var normalizedPerOrderLimit = PunchCardMapping.NormalizeOptionalLimit(request.PerOrderLimit, "perOrderLimit", normalizedTotalTimes);
|
||||||
|
var normalizedPerUserPurchaseLimit = PunchCardMapping.NormalizeOptionalLimit(request.PerUserPurchaseLimit, "perUserPurchaseLimit", 1000);
|
||||||
|
|
||||||
|
var expireStrategy = PunchCardMapping.ParseExpireStrategy(request.ExpireStrategy);
|
||||||
|
var normalizedDescription = PunchCardMapping.NormalizeOptionalDescription(request.Description);
|
||||||
|
var normalizedNotifyChannelsJson = PunchCardMapping.SerializeNotifyChannels(request.NotifyChannels);
|
||||||
|
|
||||||
|
var normalizedCategoryIdsJson = PunchCardMapping.SerializeSnowflakeIds(normalizedCategoryIds);
|
||||||
|
var normalizedTagIdsJson = PunchCardMapping.SerializeSnowflakeIds(normalizedTagIds);
|
||||||
|
var normalizedProductIdsJson = PunchCardMapping.SerializeSnowflakeIds(normalizedProductIds);
|
||||||
|
|
||||||
|
if (!request.TemplateId.HasValue)
|
||||||
|
{
|
||||||
|
var newEntity = PunchCardDtoFactory.CreateTemplateEntity(
|
||||||
|
request,
|
||||||
|
normalizedName,
|
||||||
|
normalizedCoverImageUrl,
|
||||||
|
normalizedSalePrice,
|
||||||
|
normalizedOriginalPrice,
|
||||||
|
normalizedTotalTimes,
|
||||||
|
validityType,
|
||||||
|
normalizedValidityDays,
|
||||||
|
normalizedValidFrom,
|
||||||
|
normalizedValidTo,
|
||||||
|
scopeType,
|
||||||
|
normalizedCategoryIdsJson,
|
||||||
|
normalizedTagIdsJson,
|
||||||
|
normalizedProductIdsJson,
|
||||||
|
usageMode,
|
||||||
|
normalizedUsageCapAmount,
|
||||||
|
normalizedDailyLimit,
|
||||||
|
normalizedPerOrderLimit,
|
||||||
|
normalizedPerUserPurchaseLimit,
|
||||||
|
expireStrategy,
|
||||||
|
normalizedDescription,
|
||||||
|
normalizedNotifyChannelsJson);
|
||||||
|
|
||||||
|
await repository.AddTemplateAsync(newEntity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return PunchCardDtoFactory.ToDetailDto(
|
||||||
|
newEntity,
|
||||||
|
PunchCardDtoFactory.EmptyAggregate(newEntity.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var entity = await repository.FindTemplateByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId.Value,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在");
|
||||||
|
|
||||||
|
entity.Name = normalizedName;
|
||||||
|
entity.CoverImageUrl = string.IsNullOrWhiteSpace(normalizedCoverImageUrl)
|
||||||
|
? null
|
||||||
|
: normalizedCoverImageUrl;
|
||||||
|
entity.SalePrice = normalizedSalePrice;
|
||||||
|
entity.OriginalPrice = normalizedOriginalPrice;
|
||||||
|
entity.TotalTimes = normalizedTotalTimes;
|
||||||
|
entity.ValidityType = validityType;
|
||||||
|
entity.ValidityDays = normalizedValidityDays;
|
||||||
|
entity.ValidFrom = normalizedValidFrom;
|
||||||
|
entity.ValidTo = normalizedValidTo;
|
||||||
|
entity.ScopeType = scopeType;
|
||||||
|
entity.ScopeCategoryIdsJson = normalizedCategoryIdsJson;
|
||||||
|
entity.ScopeTagIdsJson = normalizedTagIdsJson;
|
||||||
|
entity.ScopeProductIdsJson = normalizedProductIdsJson;
|
||||||
|
entity.UsageMode = usageMode;
|
||||||
|
entity.UsageCapAmount = normalizedUsageCapAmount;
|
||||||
|
entity.DailyLimit = normalizedDailyLimit;
|
||||||
|
entity.PerOrderLimit = normalizedPerOrderLimit;
|
||||||
|
entity.PerUserPurchaseLimit = normalizedPerUserPurchaseLimit;
|
||||||
|
entity.AllowTransfer = request.AllowTransfer;
|
||||||
|
entity.ExpireStrategy = expireStrategy;
|
||||||
|
entity.Description = normalizedDescription;
|
||||||
|
entity.NotifyChannelsJson = normalizedNotifyChannelsJson;
|
||||||
|
|
||||||
|
await repository.UpdateTemplateAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var aggregateMap = await repository.GetTemplateAggregateByTemplateIdsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
[entity.Id],
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var aggregate = aggregateMap.TryGetValue(entity.Id, out var value)
|
||||||
|
? value
|
||||||
|
: PunchCardDtoFactory.EmptyAggregate(entity.Id);
|
||||||
|
|
||||||
|
return PunchCardDtoFactory.ToDetailDto(entity, aggregate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入次卡使用记录处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WritePunchCardUsageRecordCommandHandler(
|
||||||
|
IPunchCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<WritePunchCardUsageRecordCommand, PunchCardUsageRecordDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardUsageRecordDto> Handle(
|
||||||
|
WritePunchCardUsageRecordCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var usedAt = request.UsedAt.HasValue
|
||||||
|
? PunchCardMapping.NormalizeUtc(request.UsedAt.Value)
|
||||||
|
: DateTime.UtcNow;
|
||||||
|
|
||||||
|
var template = await repository.FindTemplateByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.TemplateId,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "次卡不存在");
|
||||||
|
|
||||||
|
if (template.Status != PunchCardStatus.Enabled)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "次卡已下架,无法使用");
|
||||||
|
}
|
||||||
|
|
||||||
|
var productName = PunchCardMapping.NormalizeProductName(request.ProductName);
|
||||||
|
var usedTimes = PunchCardMapping.NormalizeRequiredPositiveInt(request.UsedTimes, "usedTimes", template.TotalTimes);
|
||||||
|
var extraPayAmount = PunchCardMapping.NormalizeOptionalAmount(request.ExtraPayAmount, "extraPayAmount", true);
|
||||||
|
|
||||||
|
PunchCardInstance? instance = null;
|
||||||
|
if (request.InstanceId.HasValue && request.InstanceId.Value > 0)
|
||||||
|
{
|
||||||
|
instance = await repository.FindInstanceByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.InstanceId.Value,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(request.InstanceNo))
|
||||||
|
{
|
||||||
|
var normalizedInstanceNo = PunchCardMapping.NormalizeInstanceNo(request.InstanceNo);
|
||||||
|
instance = await repository.FindInstanceByNoAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
normalizedInstanceNo,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance is not null && instance.PunchCardTemplateId != template.Id)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "次卡实例与模板不匹配");
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNewInstance = false;
|
||||||
|
if (instance is null)
|
||||||
|
{
|
||||||
|
var memberName = PunchCardMapping.NormalizeMemberName(request.MemberName);
|
||||||
|
var memberPhoneMasked = PunchCardMapping.NormalizeMemberPhoneMasked(request.MemberPhoneMasked);
|
||||||
|
var purchasedAt = usedAt;
|
||||||
|
|
||||||
|
instance = new PunchCardInstance
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
PunchCardTemplateId = template.Id,
|
||||||
|
InstanceNo = PunchCardDtoFactory.GenerateInstanceNo(usedAt),
|
||||||
|
MemberName = memberName,
|
||||||
|
MemberPhoneMasked = memberPhoneMasked,
|
||||||
|
PurchasedAt = purchasedAt,
|
||||||
|
ExpiresAt = PunchCardMapping.ResolveInstanceExpireAt(template, purchasedAt),
|
||||||
|
TotalTimes = template.TotalTimes,
|
||||||
|
RemainingTimes = template.TotalTimes,
|
||||||
|
PaidAmount = template.SalePrice,
|
||||||
|
Status = PunchCardInstanceStatus.Active
|
||||||
|
};
|
||||||
|
isNewInstance = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PunchCardMapping.IsInstanceExpired(instance, usedAt))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "次卡已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.Status == PunchCardInstanceStatus.Refunded)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "次卡已退款");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instance.RemainingTimes <= 0 || instance.Status == PunchCardInstanceStatus.UsedUp)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "次卡已用完");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.PerOrderLimit.HasValue && usedTimes > template.PerOrderLimit.Value)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "超出每单限用次数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedTimes > instance.RemainingTimes)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "超出次卡剩余次数");
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingTimes = instance.RemainingTimes - usedTimes;
|
||||||
|
var statusAfterUse = PunchCardMapping.ResolveUsageRecordStatus(instance, remainingTimes, usedAt);
|
||||||
|
|
||||||
|
instance.RemainingTimes = remainingTimes;
|
||||||
|
instance.Status = statusAfterUse switch
|
||||||
|
{
|
||||||
|
PunchCardUsageRecordStatus.UsedUp => PunchCardInstanceStatus.UsedUp,
|
||||||
|
PunchCardUsageRecordStatus.Expired => PunchCardInstanceStatus.Expired,
|
||||||
|
_ => PunchCardInstanceStatus.Active
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNewInstance)
|
||||||
|
{
|
||||||
|
await repository.AddInstanceAsync(instance, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await repository.UpdateInstanceAsync(instance, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = new PunchCardUsageRecord
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
PunchCardTemplateId = template.Id,
|
||||||
|
PunchCardInstanceId = instance.Id,
|
||||||
|
RecordNo = PunchCardDtoFactory.GenerateRecordNo(usedAt),
|
||||||
|
ProductName = productName,
|
||||||
|
UsedAt = usedAt,
|
||||||
|
UsedTimes = usedTimes,
|
||||||
|
RemainingTimesAfterUse = remainingTimes,
|
||||||
|
StatusAfterUse = statusAfterUse,
|
||||||
|
ExtraPayAmount = extraPayAmount
|
||||||
|
};
|
||||||
|
|
||||||
|
await repository.AddUsageRecordAsync(record, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return PunchCardDtoFactory.ToUsageRecordDto(record, instance, template, usedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡 DTO 构造器。
|
||||||
|
/// </summary>
|
||||||
|
internal static class PunchCardDtoFactory
|
||||||
|
{
|
||||||
|
public static PunchCardTemplateAggregateSnapshot EmptyAggregate(long templateId)
|
||||||
|
{
|
||||||
|
return new PunchCardTemplateAggregateSnapshot
|
||||||
|
{
|
||||||
|
TemplateId = templateId,
|
||||||
|
SoldCount = 0,
|
||||||
|
ActiveCount = 0,
|
||||||
|
RevenueAmount = 0m
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardListItemDto ToListItemDto(
|
||||||
|
PunchCardTemplate template,
|
||||||
|
PunchCardTemplateAggregateSnapshot aggregate)
|
||||||
|
{
|
||||||
|
return new PunchCardListItemDto
|
||||||
|
{
|
||||||
|
Id = template.Id,
|
||||||
|
Name = template.Name,
|
||||||
|
CoverImageUrl = template.CoverImageUrl,
|
||||||
|
SalePrice = template.SalePrice,
|
||||||
|
OriginalPrice = template.OriginalPrice,
|
||||||
|
TotalTimes = template.TotalTimes,
|
||||||
|
ValiditySummary = PunchCardMapping.BuildValiditySummary(template),
|
||||||
|
ScopeType = PunchCardMapping.ToScopeTypeText(template.ScopeType),
|
||||||
|
UsageMode = PunchCardMapping.ToUsageModeText(template.UsageMode),
|
||||||
|
UsageCapAmount = template.UsageCapAmount,
|
||||||
|
DailyLimit = template.DailyLimit,
|
||||||
|
Status = PunchCardMapping.ToTemplateStatusText(template.Status),
|
||||||
|
IsDimmed = template.Status == PunchCardStatus.Disabled,
|
||||||
|
SoldCount = aggregate.SoldCount,
|
||||||
|
ActiveCount = aggregate.ActiveCount,
|
||||||
|
RevenueAmount = decimal.Round(aggregate.RevenueAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
UpdatedAt = template.UpdatedAt ?? template.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardDetailDto ToDetailDto(
|
||||||
|
PunchCardTemplate template,
|
||||||
|
PunchCardTemplateAggregateSnapshot aggregate)
|
||||||
|
{
|
||||||
|
return new PunchCardDetailDto
|
||||||
|
{
|
||||||
|
Id = template.Id,
|
||||||
|
StoreId = template.StoreId,
|
||||||
|
Name = template.Name,
|
||||||
|
CoverImageUrl = template.CoverImageUrl,
|
||||||
|
SalePrice = template.SalePrice,
|
||||||
|
OriginalPrice = template.OriginalPrice,
|
||||||
|
TotalTimes = template.TotalTimes,
|
||||||
|
ValidityType = PunchCardMapping.ToValidityTypeText(template.ValidityType),
|
||||||
|
ValidityDays = template.ValidityDays,
|
||||||
|
ValidFrom = template.ValidFrom,
|
||||||
|
ValidTo = template.ValidTo,
|
||||||
|
Scope = new PunchCardScopeDto
|
||||||
|
{
|
||||||
|
ScopeType = PunchCardMapping.ToScopeTypeText(template.ScopeType),
|
||||||
|
CategoryIds = PunchCardMapping.DeserializeSnowflakeIds(template.ScopeCategoryIdsJson),
|
||||||
|
TagIds = PunchCardMapping.DeserializeSnowflakeIds(template.ScopeTagIdsJson),
|
||||||
|
ProductIds = PunchCardMapping.DeserializeSnowflakeIds(template.ScopeProductIdsJson)
|
||||||
|
},
|
||||||
|
UsageMode = PunchCardMapping.ToUsageModeText(template.UsageMode),
|
||||||
|
UsageCapAmount = template.UsageCapAmount,
|
||||||
|
DailyLimit = template.DailyLimit,
|
||||||
|
PerOrderLimit = template.PerOrderLimit,
|
||||||
|
PerUserPurchaseLimit = template.PerUserPurchaseLimit,
|
||||||
|
AllowTransfer = template.AllowTransfer,
|
||||||
|
ExpireStrategy = PunchCardMapping.ToExpireStrategyText(template.ExpireStrategy),
|
||||||
|
Description = template.Description,
|
||||||
|
NotifyChannels = PunchCardMapping.DeserializeNotifyChannels(template.NotifyChannelsJson),
|
||||||
|
Status = PunchCardMapping.ToTemplateStatusText(template.Status),
|
||||||
|
SoldCount = aggregate.SoldCount,
|
||||||
|
ActiveCount = aggregate.ActiveCount,
|
||||||
|
RevenueAmount = decimal.Round(aggregate.RevenueAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
UpdatedAt = template.UpdatedAt ?? template.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardStatsDto ToStatsDto(PunchCardTemplateStatsSnapshot source)
|
||||||
|
{
|
||||||
|
return new PunchCardStatsDto
|
||||||
|
{
|
||||||
|
OnSaleCount = source.OnSaleCount,
|
||||||
|
TotalSoldCount = source.TotalSoldCount,
|
||||||
|
TotalRevenueAmount = decimal.Round(source.TotalRevenueAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
ActiveInUseCount = source.ActiveInUseCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardUsageStatsDto ToUsageStatsDto(PunchCardUsageStatsSnapshot source)
|
||||||
|
{
|
||||||
|
return new PunchCardUsageStatsDto
|
||||||
|
{
|
||||||
|
TodayUsedCount = source.TodayUsedCount,
|
||||||
|
MonthUsedCount = source.MonthUsedCount,
|
||||||
|
ExpiringSoonCount = source.ExpiringSoonCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardUsageRecordDto ToUsageRecordDto(
|
||||||
|
PunchCardUsageRecord record,
|
||||||
|
PunchCardInstance? instance,
|
||||||
|
PunchCardTemplate? template,
|
||||||
|
DateTime nowUtc)
|
||||||
|
{
|
||||||
|
var resolvedTotalTimes = instance?.TotalTimes ?? template?.TotalTimes ?? 0;
|
||||||
|
var status = record.StatusAfterUse;
|
||||||
|
|
||||||
|
if (instance is not null)
|
||||||
|
{
|
||||||
|
status = PunchCardMapping.ResolveUsageRecordStatus(instance, record.RemainingTimesAfterUse, nowUtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PunchCardUsageRecordDto
|
||||||
|
{
|
||||||
|
Id = record.Id,
|
||||||
|
RecordNo = record.RecordNo,
|
||||||
|
PunchCardTemplateId = record.PunchCardTemplateId,
|
||||||
|
PunchCardName = template?.Name ?? string.Empty,
|
||||||
|
PunchCardInstanceId = record.PunchCardInstanceId,
|
||||||
|
MemberName = instance?.MemberName ?? string.Empty,
|
||||||
|
MemberPhoneMasked = instance?.MemberPhoneMasked ?? string.Empty,
|
||||||
|
ProductName = record.ProductName,
|
||||||
|
UsedAt = record.UsedAt,
|
||||||
|
UsedTimes = record.UsedTimes,
|
||||||
|
RemainingTimesAfterUse = record.RemainingTimesAfterUse,
|
||||||
|
TotalTimes = resolvedTotalTimes,
|
||||||
|
DisplayStatus = PunchCardMapping.ToUsageDisplayStatusText(status),
|
||||||
|
ExtraPayAmount = record.ExtraPayAmount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardTemplate CreateTemplateEntity(
|
||||||
|
SavePunchCardTemplateCommand request,
|
||||||
|
string normalizedName,
|
||||||
|
string normalizedCoverImageUrl,
|
||||||
|
decimal normalizedSalePrice,
|
||||||
|
decimal? normalizedOriginalPrice,
|
||||||
|
int normalizedTotalTimes,
|
||||||
|
PunchCardValidityType validityType,
|
||||||
|
int? normalizedValidityDays,
|
||||||
|
DateTime? normalizedValidFrom,
|
||||||
|
DateTime? normalizedValidTo,
|
||||||
|
PunchCardScopeType scopeType,
|
||||||
|
string normalizedCategoryIdsJson,
|
||||||
|
string normalizedTagIdsJson,
|
||||||
|
string normalizedProductIdsJson,
|
||||||
|
PunchCardUsageMode usageMode,
|
||||||
|
decimal? normalizedUsageCapAmount,
|
||||||
|
int? normalizedDailyLimit,
|
||||||
|
int? normalizedPerOrderLimit,
|
||||||
|
int? normalizedPerUserPurchaseLimit,
|
||||||
|
PunchCardExpireStrategy expireStrategy,
|
||||||
|
string? normalizedDescription,
|
||||||
|
string normalizedNotifyChannelsJson)
|
||||||
|
{
|
||||||
|
return new PunchCardTemplate
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
Name = normalizedName,
|
||||||
|
CoverImageUrl = string.IsNullOrWhiteSpace(normalizedCoverImageUrl)
|
||||||
|
? null
|
||||||
|
: normalizedCoverImageUrl,
|
||||||
|
SalePrice = normalizedSalePrice,
|
||||||
|
OriginalPrice = normalizedOriginalPrice,
|
||||||
|
TotalTimes = normalizedTotalTimes,
|
||||||
|
ValidityType = validityType,
|
||||||
|
ValidityDays = normalizedValidityDays,
|
||||||
|
ValidFrom = normalizedValidFrom,
|
||||||
|
ValidTo = normalizedValidTo,
|
||||||
|
ScopeType = scopeType,
|
||||||
|
ScopeCategoryIdsJson = normalizedCategoryIdsJson,
|
||||||
|
ScopeTagIdsJson = normalizedTagIdsJson,
|
||||||
|
ScopeProductIdsJson = normalizedProductIdsJson,
|
||||||
|
UsageMode = usageMode,
|
||||||
|
UsageCapAmount = normalizedUsageCapAmount,
|
||||||
|
DailyLimit = normalizedDailyLimit,
|
||||||
|
PerOrderLimit = normalizedPerOrderLimit,
|
||||||
|
PerUserPurchaseLimit = normalizedPerUserPurchaseLimit,
|
||||||
|
AllowTransfer = request.AllowTransfer,
|
||||||
|
ExpireStrategy = expireStrategy,
|
||||||
|
Description = normalizedDescription,
|
||||||
|
NotifyChannelsJson = normalizedNotifyChannelsJson,
|
||||||
|
Status = PunchCardStatus.Enabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GenerateInstanceNo(DateTime nowUtc)
|
||||||
|
{
|
||||||
|
return $"PKI{nowUtc:yyyyMMddHHmmssfff}{Random.Shared.Next(1000, 9999)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GenerateRecordNo(DateTime nowUtc)
|
||||||
|
{
|
||||||
|
return $"PK{nowUtc:yyyyMMddHHmmssfff}{Random.Shared.Next(1000, 9999)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模块映射与标准化。
|
||||||
|
/// </summary>
|
||||||
|
internal static class PunchCardMapping
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> AllowedNotifyChannels =
|
||||||
|
[
|
||||||
|
"in_app",
|
||||||
|
"sms"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public static PunchCardStatus? ParseTemplateStatusFilter(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"" => null,
|
||||||
|
"enabled" => PunchCardStatus.Enabled,
|
||||||
|
"disabled" => PunchCardStatus.Disabled,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardStatus ParseTemplateStatus(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"enabled" => PunchCardStatus.Enabled,
|
||||||
|
"disabled" => PunchCardStatus.Disabled,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToTemplateStatusText(PunchCardStatus value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PunchCardStatus.Enabled => "enabled",
|
||||||
|
PunchCardStatus.Disabled => "disabled",
|
||||||
|
_ => "disabled"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardValidityType ParseValidityType(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"days" => PunchCardValidityType.Days,
|
||||||
|
"range" => PunchCardValidityType.DateRange,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "validityType 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToValidityTypeText(PunchCardValidityType value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PunchCardValidityType.Days => "days",
|
||||||
|
PunchCardValidityType.DateRange => "range",
|
||||||
|
_ => "days"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardScopeType ParseScopeType(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"all" => PunchCardScopeType.All,
|
||||||
|
"category" => PunchCardScopeType.Category,
|
||||||
|
"tag" => PunchCardScopeType.Tag,
|
||||||
|
"product" => PunchCardScopeType.Product,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "scopeType 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToScopeTypeText(PunchCardScopeType value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PunchCardScopeType.All => "all",
|
||||||
|
PunchCardScopeType.Category => "category",
|
||||||
|
PunchCardScopeType.Tag => "tag",
|
||||||
|
PunchCardScopeType.Product => "product",
|
||||||
|
_ => "all"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardUsageMode ParseUsageMode(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"free" => PunchCardUsageMode.Free,
|
||||||
|
"cap" => PunchCardUsageMode.Cap,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "usageMode 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToUsageModeText(PunchCardUsageMode value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PunchCardUsageMode.Free => "free",
|
||||||
|
PunchCardUsageMode.Cap => "cap",
|
||||||
|
_ => "free"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardExpireStrategy ParseExpireStrategy(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"invalidate" => PunchCardExpireStrategy.Invalidate,
|
||||||
|
"refund" => PunchCardExpireStrategy.Refund,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "expireStrategy 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToExpireStrategyText(PunchCardExpireStrategy value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PunchCardExpireStrategy.Invalidate => "invalidate",
|
||||||
|
PunchCardExpireStrategy.Refund => "refund",
|
||||||
|
_ => "invalidate"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardUsageRecordFilterStatus? ParseUsageStatusFilter(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"" => null,
|
||||||
|
"normal" => PunchCardUsageRecordFilterStatus.Normal,
|
||||||
|
"used_up" => PunchCardUsageRecordFilterStatus.UsedUp,
|
||||||
|
"expired" => PunchCardUsageRecordFilterStatus.Expired,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToUsageDisplayStatusText(PunchCardUsageRecordStatus value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PunchCardUsageRecordStatus.Normal => "normal",
|
||||||
|
PunchCardUsageRecordStatus.AlmostUsedUp => "almost_used_up",
|
||||||
|
PunchCardUsageRecordStatus.UsedUp => "used_up",
|
||||||
|
PunchCardUsageRecordStatus.Expired => "expired",
|
||||||
|
_ => "normal"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime NormalizeUtc(DateTime value)
|
||||||
|
{
|
||||||
|
return value.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Utc => value,
|
||||||
|
DateTimeKind.Local => value.ToUniversalTime(),
|
||||||
|
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeName(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "name 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 64)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "name 长度不能超过 64");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeOptionalCoverUrl(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 512)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "coverImageUrl 长度不能超过 512");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? NormalizeOptionalDescription(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (normalized.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 512)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "description 长度不能超过 512");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeInstanceNo(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "punchCardInstanceNo 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 32)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "punchCardInstanceNo 长度不能超过 32");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeMemberName(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "memberName 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 64)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "memberName 长度不能超过 64");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeMemberPhoneMasked(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "memberPhoneMasked 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 32)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "memberPhoneMasked 长度不能超过 32");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NormalizeProductName(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "productName 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 128)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "productName 长度不能超过 128");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal NormalizeAmount(decimal value, string fieldName, bool allowZero = false)
|
||||||
|
{
|
||||||
|
if (value < 0 || (!allowZero && value <= 0))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal? NormalizeOptionalAmount(decimal? value, string fieldName, bool allowZero = true)
|
||||||
|
{
|
||||||
|
if (!value.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Value < 0 || (!allowZero && value.Value <= 0))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(value.Value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int NormalizeRequiredPositiveInt(int value, string fieldName, int max = 100_000)
|
||||||
|
{
|
||||||
|
if (value <= 0 || value > max)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int? NormalizeOptionalLimit(int? value, string fieldName, int max = 100_000)
|
||||||
|
{
|
||||||
|
if (!value.HasValue || value.Value <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Value > max)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (int? ValidityDays, DateTime? ValidFrom, DateTime? ValidTo) NormalizeValidity(
|
||||||
|
PunchCardValidityType validityType,
|
||||||
|
int? validityDays,
|
||||||
|
DateTime? validFrom,
|
||||||
|
DateTime? validTo)
|
||||||
|
{
|
||||||
|
return validityType switch
|
||||||
|
{
|
||||||
|
PunchCardValidityType.Days =>
|
||||||
|
(
|
||||||
|
NormalizeRequiredPositiveInt(validityDays ?? 0, "validityDays", 3650),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
),
|
||||||
|
PunchCardValidityType.DateRange => NormalizeRange(validFrom, validTo),
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "validityType 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (IReadOnlyList<long> CategoryIds, IReadOnlyList<long> TagIds, IReadOnlyList<long> ProductIds) NormalizeScopeIds(
|
||||||
|
PunchCardScopeType scopeType,
|
||||||
|
IReadOnlyCollection<long>? categoryIds,
|
||||||
|
IReadOnlyCollection<long>? tagIds,
|
||||||
|
IReadOnlyCollection<long>? productIds)
|
||||||
|
{
|
||||||
|
var normalizedCategoryIds = NormalizeSnowflakeIds(categoryIds, "scopeCategoryIds", false);
|
||||||
|
var normalizedTagIds = NormalizeSnowflakeIds(tagIds, "scopeTagIds", false);
|
||||||
|
var normalizedProductIds = NormalizeSnowflakeIds(productIds, "scopeProductIds", false);
|
||||||
|
|
||||||
|
return scopeType switch
|
||||||
|
{
|
||||||
|
PunchCardScopeType.All => ([], [], []),
|
||||||
|
PunchCardScopeType.Category =>
|
||||||
|
normalizedCategoryIds.Count == 0
|
||||||
|
? throw new BusinessException(ErrorCodes.BadRequest, "scopeCategoryIds 不能为空")
|
||||||
|
: (normalizedCategoryIds, [], []),
|
||||||
|
PunchCardScopeType.Tag =>
|
||||||
|
normalizedTagIds.Count == 0
|
||||||
|
? throw new BusinessException(ErrorCodes.BadRequest, "scopeTagIds 不能为空")
|
||||||
|
: ([], normalizedTagIds, []),
|
||||||
|
PunchCardScopeType.Product =>
|
||||||
|
normalizedProductIds.Count == 0
|
||||||
|
? throw new BusinessException(ErrorCodes.BadRequest, "scopeProductIds 不能为空")
|
||||||
|
: ([], [], normalizedProductIds),
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "scopeType 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> NormalizeNotifyChannels(IEnumerable<string>? values)
|
||||||
|
{
|
||||||
|
var normalized = (values ?? [])
|
||||||
|
.Select(item => (item ?? string.Empty).Trim().ToLowerInvariant())
|
||||||
|
.Where(item => !string.IsNullOrWhiteSpace(item))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (normalized.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "notifyChannels 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Any(item => !AllowedNotifyChannels.Contains(item)))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "notifyChannels 存在非法值");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string> DeserializeNotifyChannels(string? payload)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = JsonSerializer.Deserialize<List<string>>(payload, JsonOptions) ?? [];
|
||||||
|
return values
|
||||||
|
.Select(item => (item ?? string.Empty).Trim().ToLowerInvariant())
|
||||||
|
.Where(item => AllowedNotifyChannels.Contains(item))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SerializeNotifyChannels(IEnumerable<string>? values)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(NormalizeNotifyChannels(values), JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<long> DeserializeSnowflakeIds(string? payload)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = JsonSerializer.Deserialize<List<long>>(payload, JsonOptions) ?? [];
|
||||||
|
return values
|
||||||
|
.Where(id => id > 0)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(id => id)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SerializeSnowflakeIds(IEnumerable<long>? values)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(NormalizeSnowflakeIds(values, "ids", false), JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildValiditySummary(PunchCardTemplate template)
|
||||||
|
{
|
||||||
|
return template.ValidityType switch
|
||||||
|
{
|
||||||
|
PunchCardValidityType.Days => $"{template.ValidityDays ?? 0}天有效",
|
||||||
|
PunchCardValidityType.DateRange when template.ValidFrom.HasValue && template.ValidTo.HasValue =>
|
||||||
|
$"{template.ValidFrom.Value:yyyy-MM-dd} 至 {template.ValidTo.Value:yyyy-MM-dd}",
|
||||||
|
_ => "-"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime ResolveInstanceExpireAt(PunchCardTemplate template, DateTime purchasedAtUtc)
|
||||||
|
{
|
||||||
|
var purchasedAt = NormalizeUtc(purchasedAtUtc);
|
||||||
|
|
||||||
|
return template.ValidityType switch
|
||||||
|
{
|
||||||
|
PunchCardValidityType.Days => purchasedAt.Date.AddDays(template.ValidityDays ?? 0).AddTicks(-1),
|
||||||
|
PunchCardValidityType.DateRange => template.ValidTo ?? purchasedAt.Date.AddTicks(-1),
|
||||||
|
_ => purchasedAt.Date.AddTicks(-1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsInstanceExpired(PunchCardInstance instance, DateTime nowUtc)
|
||||||
|
{
|
||||||
|
var utcNow = NormalizeUtc(nowUtc);
|
||||||
|
if (instance.Status == PunchCardInstanceStatus.Expired)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance.ExpiresAt.HasValue && instance.ExpiresAt.Value < utcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PunchCardUsageRecordStatus ResolveUsageRecordStatus(
|
||||||
|
PunchCardInstance instance,
|
||||||
|
int remainingTimes,
|
||||||
|
DateTime usedAtUtc)
|
||||||
|
{
|
||||||
|
if (IsInstanceExpired(instance, usedAtUtc))
|
||||||
|
{
|
||||||
|
return PunchCardUsageRecordStatus.Expired;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingTimes <= 0)
|
||||||
|
{
|
||||||
|
return PunchCardUsageRecordStatus.UsedUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return remainingTimes <= 2
|
||||||
|
? PunchCardUsageRecordStatus.AlmostUsedUp
|
||||||
|
: PunchCardUsageRecordStatus.Normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<long> NormalizeSnowflakeIds(
|
||||||
|
IEnumerable<long>? values,
|
||||||
|
string fieldName,
|
||||||
|
bool required)
|
||||||
|
{
|
||||||
|
var normalized = (values ?? [])
|
||||||
|
.Where(id => id > 0)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(id => id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (required && normalized.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int? ValidityDays, DateTime? ValidFrom, DateTime? ValidTo) NormalizeRange(
|
||||||
|
DateTime? validFrom,
|
||||||
|
DateTime? validTo)
|
||||||
|
{
|
||||||
|
if (!validFrom.HasValue || !validTo.HasValue)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "validFrom / validTo 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedFrom = NormalizeUtc(validFrom.Value).Date;
|
||||||
|
var normalizedTo = NormalizeUtc(validTo.Value).Date.AddDays(1).AddTicks(-1);
|
||||||
|
|
||||||
|
if (normalizedFrom > normalizedTo)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "validFrom 不能晚于 validTo");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (null, normalizedFrom, normalizedTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出次卡使用记录 CSV。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportPunchCardUsageRecordCsvQuery : IRequest<PunchCardUsageRecordExportDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板筛选 ID(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public long? TemplateId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态筛选(normal/used_up/expired)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(会员/商品)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询次卡模板详情。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPunchCardTemplateDetailQuery : IRequest<PunchCardDetailDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long TemplateId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询次卡模板列表。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPunchCardTemplateListQuery : IRequest<PunchCardListResultDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 名称关键字。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态筛选(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Coupons.PunchCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Coupons.PunchCard.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询次卡使用记录列表。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetPunchCardUsageRecordListQuery : IRequest<PunchCardUsageRecordListResultDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板筛选 ID(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public long? TemplateId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态筛选(normal/used_up/expired)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Status { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(会员/商品)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 10;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼券规则。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerCouponRule : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 券规则场景。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerCouponScene Scene { get; set; } = NewCustomerCouponScene.Welcome;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 券类型。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerCouponType CouponType { get; set; } = NewCustomerCouponType.AmountOff;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 面值或折扣值。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Value { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用门槛。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? MinimumSpend { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 有效期天数。
|
||||||
|
/// </summary>
|
||||||
|
public int ValidDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序值(同场景内递增)。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼门店配置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerGiftSetting : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启新客礼包。
|
||||||
|
/// </summary>
|
||||||
|
public bool GiftEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包类型。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerGiftType GiftType { get; set; } = NewCustomerGiftType.Coupon;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectReduceAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减门槛金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal? DirectMinimumSpend { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启老带新分享。
|
||||||
|
/// </summary>
|
||||||
|
public bool InviteEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分享渠道(JSON)。
|
||||||
|
/// </summary>
|
||||||
|
public string ShareChannelsJson { get; set; } = "[]";
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客成长记录。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerGrowthRecord : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客业务唯一键。
|
||||||
|
/// </summary>
|
||||||
|
public string CustomerKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 顾客展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string? CustomerName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RegisteredAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 礼包领取时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? GiftClaimedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? FirstOrderAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道来源。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客邀请记录。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class NewCustomerInviteRecord : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人展示名。
|
||||||
|
/// </summary>
|
||||||
|
public string InviteeName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime InviteTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单状态。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerInviteOrderStatus OrderStatus { get; set; } = NewCustomerInviteOrderStatus.PendingOrder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励状态。
|
||||||
|
/// </summary>
|
||||||
|
public NewCustomerInviteRewardStatus RewardStatus { get; set; } = NewCustomerInviteRewardStatus.Pending;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 奖励发放时间。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RewardIssuedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请来源渠道。
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceChannel { get; set; }
|
||||||
|
}
|
||||||
@@ -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,22 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼券规则场景。
|
||||||
|
/// </summary>
|
||||||
|
public enum NewCustomerCouponScene
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 新客礼包。
|
||||||
|
/// </summary>
|
||||||
|
Welcome = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请人奖励。
|
||||||
|
/// </summary>
|
||||||
|
InviterReward = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 被邀请人奖励。
|
||||||
|
/// </summary>
|
||||||
|
InviteeReward = 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼券类型。
|
||||||
|
/// </summary>
|
||||||
|
public enum NewCustomerCouponType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 满减券。
|
||||||
|
/// </summary>
|
||||||
|
AmountOff = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 折扣券。
|
||||||
|
/// </summary>
|
||||||
|
Discount = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 免配送费券。
|
||||||
|
/// </summary>
|
||||||
|
FreeShipping = 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客礼包类型。
|
||||||
|
/// </summary>
|
||||||
|
public enum NewCustomerGiftType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 优惠券包。
|
||||||
|
/// </summary>
|
||||||
|
Coupon = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 首单直减。
|
||||||
|
/// </summary>
|
||||||
|
Direct = 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请记录订单状态。
|
||||||
|
/// </summary>
|
||||||
|
public enum NewCustomerInviteOrderStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待下单。
|
||||||
|
/// </summary>
|
||||||
|
PendingOrder = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已下单。
|
||||||
|
/// </summary>
|
||||||
|
Ordered = 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 邀请奖励发放状态。
|
||||||
|
/// </summary>
|
||||||
|
public enum NewCustomerInviteRewardStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 待触发。
|
||||||
|
/// </summary>
|
||||||
|
Pending = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已发放。
|
||||||
|
/// </summary>
|
||||||
|
Issued = 2
|
||||||
|
}
|
||||||
@@ -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,113 @@
|
|||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼仓储契约。
|
||||||
|
/// </summary>
|
||||||
|
public interface INewCustomerGiftRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询门店配置。
|
||||||
|
/// </summary>
|
||||||
|
Task<NewCustomerGiftSetting?> FindSettingByStoreIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增门店配置。
|
||||||
|
/// </summary>
|
||||||
|
Task AddSettingAsync(NewCustomerGiftSetting entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新门店配置。
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateSettingAsync(NewCustomerGiftSetting entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询门店全部券规则。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<NewCustomerCouponRule>> GetCouponRulesByStoreIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 替换门店券规则集合。
|
||||||
|
/// </summary>
|
||||||
|
Task ReplaceCouponRulesAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
IReadOnlyCollection<NewCustomerCouponRule> entities,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询邀请记录。
|
||||||
|
/// </summary>
|
||||||
|
Task<(IReadOnlyList<NewCustomerInviteRecord> Items, int TotalCount)> GetInviteRecordsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增邀请记录。
|
||||||
|
/// </summary>
|
||||||
|
Task AddInviteRecordAsync(NewCustomerInviteRecord entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按业务键查询成长记录。
|
||||||
|
/// </summary>
|
||||||
|
Task<NewCustomerGrowthRecord?> FindGrowthRecordByCustomerKeyAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
string customerKey,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增成长记录。
|
||||||
|
/// </summary>
|
||||||
|
Task AddGrowthRecordAsync(NewCustomerGrowthRecord entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新成长记录。
|
||||||
|
/// </summary>
|
||||||
|
Task UpdateGrowthRecordAsync(NewCustomerGrowthRecord entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计时间范围内注册新客数。
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CountRegisteredCustomersAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime startAt,
|
||||||
|
DateTime endAt,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计时间范围内礼包已领取数。
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CountGiftClaimedCustomersAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime startAt,
|
||||||
|
DateTime endAt,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统计时间范围内首单完成数。
|
||||||
|
/// </summary>
|
||||||
|
Task<int> CountFirstOrderedCustomersAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime startAt,
|
||||||
|
DateTime endAt,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 持久化变更。
|
||||||
|
/// </summary>
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -46,7 +46,9 @@ public static class AppServiceCollectionExtensions
|
|||||||
services.AddScoped<IStoreRepository, EfStoreRepository>();
|
services.AddScoped<IStoreRepository, EfStoreRepository>();
|
||||||
services.AddScoped<IProductRepository, EfProductRepository>();
|
services.AddScoped<IProductRepository, EfProductRepository>();
|
||||||
services.AddScoped<ICouponRepository, EfCouponRepository>();
|
services.AddScoped<ICouponRepository, EfCouponRepository>();
|
||||||
|
services.AddScoped<INewCustomerGiftRepository, EfNewCustomerGiftRepository>();
|
||||||
services.AddScoped<IPromotionCampaignRepository, EfPromotionCampaignRepository>();
|
services.AddScoped<IPromotionCampaignRepository, EfPromotionCampaignRepository>();
|
||||||
|
services.AddScoped<IPunchCardRepository, EfPunchCardRepository>();
|
||||||
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
||||||
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
||||||
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();
|
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();
|
||||||
|
|||||||
@@ -354,6 +354,34 @@ public sealed class TakeoutAppDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<PromotionCampaign> PromotionCampaigns => Set<PromotionCampaign>();
|
public DbSet<PromotionCampaign> PromotionCampaigns => Set<PromotionCampaign>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 新客有礼配置。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<NewCustomerGiftSetting> NewCustomerGiftSettings => Set<NewCustomerGiftSetting>();
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼券规则。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<NewCustomerCouponRule> NewCustomerCouponRules => Set<NewCustomerCouponRule>();
|
||||||
|
/// <summary>
|
||||||
|
/// 新客邀请记录。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<NewCustomerInviteRecord> NewCustomerInviteRecords => Set<NewCustomerInviteRecord>();
|
||||||
|
/// <summary>
|
||||||
|
/// 新客成长记录。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<NewCustomerGrowthRecord> NewCustomerGrowthRecords => Set<NewCustomerGrowthRecord>();
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡模板。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<PunchCardTemplate> PunchCardTemplates => Set<PunchCardTemplate>();
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡实例。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<PunchCardInstance> PunchCardInstances => Set<PunchCardInstance>();
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡使用记录。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<PunchCardUsageRecord> PunchCardUsageRecords => Set<PunchCardUsageRecord>();
|
||||||
|
/// <summary>
|
||||||
/// 会员档案。
|
/// 会员档案。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<MemberProfile> MemberProfiles => Set<MemberProfile>();
|
public DbSet<MemberProfile> MemberProfiles => Set<MemberProfile>();
|
||||||
@@ -520,6 +548,13 @@ public sealed class TakeoutAppDbContext(
|
|||||||
ConfigureCouponTemplate(modelBuilder.Entity<CouponTemplate>());
|
ConfigureCouponTemplate(modelBuilder.Entity<CouponTemplate>());
|
||||||
ConfigureCoupon(modelBuilder.Entity<Coupon>());
|
ConfigureCoupon(modelBuilder.Entity<Coupon>());
|
||||||
ConfigurePromotionCampaign(modelBuilder.Entity<PromotionCampaign>());
|
ConfigurePromotionCampaign(modelBuilder.Entity<PromotionCampaign>());
|
||||||
|
ConfigureNewCustomerGiftSetting(modelBuilder.Entity<NewCustomerGiftSetting>());
|
||||||
|
ConfigureNewCustomerCouponRule(modelBuilder.Entity<NewCustomerCouponRule>());
|
||||||
|
ConfigureNewCustomerInviteRecord(modelBuilder.Entity<NewCustomerInviteRecord>());
|
||||||
|
ConfigureNewCustomerGrowthRecord(modelBuilder.Entity<NewCustomerGrowthRecord>());
|
||||||
|
ConfigurePunchCardTemplate(modelBuilder.Entity<PunchCardTemplate>());
|
||||||
|
ConfigurePunchCardInstance(modelBuilder.Entity<PunchCardInstance>());
|
||||||
|
ConfigurePunchCardUsageRecord(modelBuilder.Entity<PunchCardUsageRecord>());
|
||||||
ConfigureMemberProfile(modelBuilder.Entity<MemberProfile>());
|
ConfigureMemberProfile(modelBuilder.Entity<MemberProfile>());
|
||||||
ConfigureMemberTier(modelBuilder.Entity<MemberTier>());
|
ConfigureMemberTier(modelBuilder.Entity<MemberTier>());
|
||||||
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
|
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
|
||||||
@@ -1619,6 +1654,130 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.Property(x => x.BannerUrl).HasMaxLength(512);
|
builder.Property(x => x.BannerUrl).HasMaxLength(512);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureNewCustomerGiftSetting(EntityTypeBuilder<NewCustomerGiftSetting> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("new_customer_gift_settings");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.GiftType).HasConversion<int>();
|
||||||
|
builder.Property(x => x.DirectReduceAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.DirectMinimumSpend).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.ShareChannelsJson).HasColumnType("text").IsRequired();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId }).IsUnique();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureNewCustomerCouponRule(EntityTypeBuilder<NewCustomerCouponRule> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("new_customer_coupon_rules");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.Scene).HasConversion<int>();
|
||||||
|
builder.Property(x => x.CouponType).HasConversion<int>();
|
||||||
|
builder.Property(x => x.Value).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.MinimumSpend).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.ValidDays).IsRequired();
|
||||||
|
builder.Property(x => x.SortOrder).IsRequired();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Scene, x.SortOrder });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureNewCustomerInviteRecord(EntityTypeBuilder<NewCustomerInviteRecord> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("new_customer_invite_records");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.InviterName).HasMaxLength(64).IsRequired();
|
||||||
|
builder.Property(x => x.InviteeName).HasMaxLength(64).IsRequired();
|
||||||
|
builder.Property(x => x.InviteTime).IsRequired();
|
||||||
|
builder.Property(x => x.OrderStatus).HasConversion<int>();
|
||||||
|
builder.Property(x => x.RewardStatus).HasConversion<int>();
|
||||||
|
builder.Property(x => x.SourceChannel).HasMaxLength(32);
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.InviteTime });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureNewCustomerGrowthRecord(EntityTypeBuilder<NewCustomerGrowthRecord> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("new_customer_growth_records");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.CustomerKey).HasMaxLength(64).IsRequired();
|
||||||
|
builder.Property(x => x.CustomerName).HasMaxLength(64);
|
||||||
|
builder.Property(x => x.RegisteredAt).IsRequired();
|
||||||
|
builder.Property(x => x.SourceChannel).HasMaxLength(32);
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.CustomerKey }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RegisteredAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigurePunchCardTemplate(EntityTypeBuilder<PunchCardTemplate> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("punch_card_templates");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
|
||||||
|
builder.Property(x => x.CoverImageUrl).HasMaxLength(512);
|
||||||
|
builder.Property(x => x.SalePrice).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.OriginalPrice).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.TotalTimes).IsRequired();
|
||||||
|
builder.Property(x => x.ValidityType).HasConversion<int>();
|
||||||
|
builder.Property(x => x.ValidityDays);
|
||||||
|
builder.Property(x => x.ValidFrom);
|
||||||
|
builder.Property(x => x.ValidTo);
|
||||||
|
builder.Property(x => x.ScopeType).HasConversion<int>();
|
||||||
|
builder.Property(x => x.ScopeCategoryIdsJson).HasColumnType("text").IsRequired();
|
||||||
|
builder.Property(x => x.ScopeTagIdsJson).HasColumnType("text").IsRequired();
|
||||||
|
builder.Property(x => x.ScopeProductIdsJson).HasColumnType("text").IsRequired();
|
||||||
|
builder.Property(x => x.UsageMode).HasConversion<int>();
|
||||||
|
builder.Property(x => x.UsageCapAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.DailyLimit);
|
||||||
|
builder.Property(x => x.PerOrderLimit);
|
||||||
|
builder.Property(x => x.PerUserPurchaseLimit);
|
||||||
|
builder.Property(x => x.AllowTransfer).IsRequired();
|
||||||
|
builder.Property(x => x.ExpireStrategy).HasConversion<int>();
|
||||||
|
builder.Property(x => x.Description).HasMaxLength(512);
|
||||||
|
builder.Property(x => x.NotifyChannelsJson).HasColumnType("text").IsRequired();
|
||||||
|
builder.Property(x => x.Status).HasConversion<int>();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigurePunchCardInstance(EntityTypeBuilder<PunchCardInstance> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("punch_card_instances");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.PunchCardTemplateId).IsRequired();
|
||||||
|
builder.Property(x => x.InstanceNo).HasMaxLength(32).IsRequired();
|
||||||
|
builder.Property(x => x.MemberName).HasMaxLength(64).IsRequired();
|
||||||
|
builder.Property(x => x.MemberPhoneMasked).HasMaxLength(32).IsRequired();
|
||||||
|
builder.Property(x => x.PurchasedAt).IsRequired();
|
||||||
|
builder.Property(x => x.ExpiresAt);
|
||||||
|
builder.Property(x => x.TotalTimes).IsRequired();
|
||||||
|
builder.Property(x => x.RemainingTimes).IsRequired();
|
||||||
|
builder.Property(x => x.PaidAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.Status).HasConversion<int>();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.InstanceNo }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PunchCardTemplateId });
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status, x.ExpiresAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigurePunchCardUsageRecord(EntityTypeBuilder<PunchCardUsageRecord> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("punch_card_usage_records");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.PunchCardTemplateId).IsRequired();
|
||||||
|
builder.Property(x => x.PunchCardInstanceId).IsRequired();
|
||||||
|
builder.Property(x => x.RecordNo).HasMaxLength(32).IsRequired();
|
||||||
|
builder.Property(x => x.ProductName).HasMaxLength(128).IsRequired();
|
||||||
|
builder.Property(x => x.UsedAt).IsRequired();
|
||||||
|
builder.Property(x => x.UsedTimes).IsRequired();
|
||||||
|
builder.Property(x => x.RemainingTimesAfterUse).IsRequired();
|
||||||
|
builder.Property(x => x.StatusAfterUse).HasConversion<int>();
|
||||||
|
builder.Property(x => x.ExtraPayAmount).HasPrecision(18, 2);
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RecordNo }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PunchCardTemplateId, x.UsedAt });
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PunchCardInstanceId, x.UsedAt });
|
||||||
|
}
|
||||||
|
|
||||||
private static void ConfigureMemberProfile(EntityTypeBuilder<MemberProfile> builder)
|
private static void ConfigureMemberProfile(EntityTypeBuilder<MemberProfile> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("member_profiles");
|
builder.ToTable("member_profiles");
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新客有礼仓储 EF Core 实现。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EfNewCustomerGiftRepository(TakeoutAppDbContext context) : INewCustomerGiftRepository
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<NewCustomerGiftSetting?> FindSettingByStoreIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerGiftSettings
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddSettingAsync(NewCustomerGiftSetting entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerGiftSettings.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task UpdateSettingAsync(NewCustomerGiftSetting entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.NewCustomerGiftSettings.Update(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<NewCustomerCouponRule>> GetCouponRulesByStoreIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await context.NewCustomerCouponRules
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
|
||||||
|
.OrderBy(item => item.Scene)
|
||||||
|
.ThenBy(item => item.SortOrder)
|
||||||
|
.ThenBy(item => item.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task ReplaceCouponRulesAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
IReadOnlyCollection<NewCustomerCouponRule> entities,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var existing = await context.NewCustomerCouponRules
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (existing.Count > 0)
|
||||||
|
{
|
||||||
|
context.NewCustomerCouponRules.RemoveRange(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entities.Count > 0)
|
||||||
|
{
|
||||||
|
await context.NewCustomerCouponRules.AddRangeAsync(entities, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(IReadOnlyList<NewCustomerInviteRecord> Items, int TotalCount)> GetInviteRecordsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalizedPage = Math.Max(1, page);
|
||||||
|
var normalizedPageSize = Math.Clamp(pageSize, 1, 200);
|
||||||
|
|
||||||
|
var query = context.NewCustomerInviteRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
if (totalCount == 0)
|
||||||
|
{
|
||||||
|
return ([], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(item => item.InviteTime)
|
||||||
|
.ThenByDescending(item => item.Id)
|
||||||
|
.Skip((normalizedPage - 1) * normalizedPageSize)
|
||||||
|
.Take(normalizedPageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return (items, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddInviteRecordAsync(NewCustomerInviteRecord entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerInviteRecords.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<NewCustomerGrowthRecord?> FindGrowthRecordByCustomerKeyAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
string customerKey,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerGrowthRecords
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.CustomerKey == customerKey)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddGrowthRecordAsync(NewCustomerGrowthRecord entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerGrowthRecords.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task UpdateGrowthRecordAsync(NewCustomerGrowthRecord entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.NewCustomerGrowthRecords.Update(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<int> CountRegisteredCustomersAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime startAt,
|
||||||
|
DateTime endAt,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerGrowthRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.RegisteredAt >= startAt &&
|
||||||
|
item.RegisteredAt < endAt)
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<int> CountGiftClaimedCustomersAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime startAt,
|
||||||
|
DateTime endAt,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerGrowthRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.RegisteredAt >= startAt &&
|
||||||
|
item.RegisteredAt < endAt &&
|
||||||
|
item.GiftClaimedAt != null)
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<int> CountFirstOrderedCustomersAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime startAt,
|
||||||
|
DateTime endAt,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.NewCustomerGrowthRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.RegisteredAt >= startAt &&
|
||||||
|
item.RegisteredAt < endAt &&
|
||||||
|
item.FirstOrderAt != null)
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Coupons.Repositories;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 次卡仓储 EF Core 实现。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EfPunchCardRepository(TakeoutAppDbContext context) : IPunchCardRepository
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(IReadOnlyList<PunchCardTemplate> Items, int TotalCount)> SearchTemplatesAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
string? keyword,
|
||||||
|
PunchCardStatus? status,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalizedPage = Math.Max(1, page);
|
||||||
|
var normalizedPageSize = Math.Clamp(pageSize, 1, 200);
|
||||||
|
|
||||||
|
var query = context.PunchCardTemplates
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
|
||||||
|
|
||||||
|
if (status.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(item => item.Status == status.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedKeyword = (keyword ?? string.Empty).Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||||
|
{
|
||||||
|
var keywordLike = $"%{normalizedKeyword}%";
|
||||||
|
query = query.Where(item => EF.Functions.ILike(item.Name, keywordLike));
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
if (totalCount == 0)
|
||||||
|
{
|
||||||
|
return ([], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(item => item.UpdatedAt ?? item.CreatedAt)
|
||||||
|
.ThenByDescending(item => item.Id)
|
||||||
|
.Skip((normalizedPage - 1) * normalizedPageSize)
|
||||||
|
.Take(normalizedPageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return (items, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<PunchCardTemplate?> FindTemplateByIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long templateId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.PunchCardTemplates
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.Id == templateId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<PunchCardTemplate>> GetTemplatesByIdsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
IReadOnlyCollection<long> templateIds,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (templateIds.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await context.PunchCardTemplates
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
templateIds.Contains(item.Id))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<Dictionary<long, PunchCardTemplateAggregateSnapshot>> GetTemplateAggregateByTemplateIdsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
IReadOnlyCollection<long> templateIds,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (templateIds.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var aggregates = await context.PunchCardInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
templateIds.Contains(item.PunchCardTemplateId))
|
||||||
|
.GroupBy(item => item.PunchCardTemplateId)
|
||||||
|
.Select(group => new PunchCardTemplateAggregateSnapshot
|
||||||
|
{
|
||||||
|
TemplateId = group.Key,
|
||||||
|
SoldCount = group.Count(),
|
||||||
|
ActiveCount = group.Count(item =>
|
||||||
|
item.Status == PunchCardInstanceStatus.Active &&
|
||||||
|
item.RemainingTimes > 0 &&
|
||||||
|
(!item.ExpiresAt.HasValue || item.ExpiresAt.Value >= nowUtc)),
|
||||||
|
RevenueAmount = group.Sum(item => item.PaidAmount)
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return aggregates.ToDictionary(item => item.TemplateId, item => item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardTemplateStatsSnapshot> GetTemplateStatsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var onSaleCount = await context.PunchCardTemplates
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.Status == PunchCardStatus.Enabled)
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var summary = await context.PunchCardInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
|
||||||
|
.GroupBy(_ => 1)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
TotalSoldCount = group.Count(),
|
||||||
|
TotalRevenueAmount = group.Sum(item => item.PaidAmount)
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
var activeInUseCount = await context.PunchCardInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.Status == PunchCardInstanceStatus.Active &&
|
||||||
|
item.RemainingTimes > 0 &&
|
||||||
|
(!item.ExpiresAt.HasValue || item.ExpiresAt.Value >= nowUtc))
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new PunchCardTemplateStatsSnapshot
|
||||||
|
{
|
||||||
|
OnSaleCount = onSaleCount,
|
||||||
|
TotalSoldCount = summary?.TotalSoldCount ?? 0,
|
||||||
|
TotalRevenueAmount = summary?.TotalRevenueAmount ?? 0m,
|
||||||
|
ActiveInUseCount = activeInUseCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.PunchCardTemplates.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task UpdateTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.PunchCardTemplates.Update(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task DeleteTemplateAsync(PunchCardTemplate entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.PunchCardTemplates.Remove(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<PunchCardInstance?> FindInstanceByNoAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
string instanceNo,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.PunchCardInstances
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.InstanceNo == instanceNo)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<PunchCardInstance?> FindInstanceByIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long instanceId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.PunchCardInstances
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.Id == instanceId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<PunchCardInstance>> GetInstancesByIdsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
IReadOnlyCollection<long> instanceIds,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (instanceIds.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await context.PunchCardInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
instanceIds.Contains(item.Id))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.PunchCardInstances.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task UpdateInstanceAsync(PunchCardInstance entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.PunchCardInstances.Update(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(IReadOnlyList<PunchCardUsageRecord> Items, int TotalCount)> SearchUsageRecordsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long? templateId,
|
||||||
|
string? keyword,
|
||||||
|
PunchCardUsageRecordFilterStatus? status,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalizedPage = Math.Max(1, page);
|
||||||
|
var normalizedPageSize = Math.Clamp(pageSize, 1, 500);
|
||||||
|
var query = BuildUsageRecordQuery(
|
||||||
|
tenantId,
|
||||||
|
storeId,
|
||||||
|
templateId,
|
||||||
|
keyword,
|
||||||
|
status,
|
||||||
|
DateTime.UtcNow);
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
if (totalCount == 0)
|
||||||
|
{
|
||||||
|
return ([], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(item => item.UsedAt)
|
||||||
|
.ThenByDescending(item => item.Id)
|
||||||
|
.Skip((normalizedPage - 1) * normalizedPageSize)
|
||||||
|
.Take(normalizedPageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return (items, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<PunchCardUsageRecord>> ListUsageRecordsForExportAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long? templateId,
|
||||||
|
string? keyword,
|
||||||
|
PunchCardUsageRecordFilterStatus? status,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await BuildUsageRecordQuery(
|
||||||
|
tenantId,
|
||||||
|
storeId,
|
||||||
|
templateId,
|
||||||
|
keyword,
|
||||||
|
status,
|
||||||
|
DateTime.UtcNow)
|
||||||
|
.OrderByDescending(item => item.UsedAt)
|
||||||
|
.ThenByDescending(item => item.Id)
|
||||||
|
.Take(20_000)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<PunchCardUsageStatsSnapshot> GetUsageStatsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long? templateId,
|
||||||
|
DateTime nowUtc,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var utcNow = NormalizeUtc(nowUtc);
|
||||||
|
var todayStart = utcNow.Date;
|
||||||
|
var monthStart = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var soonEnd = todayStart.AddDays(7);
|
||||||
|
|
||||||
|
var usageQuery = context.PunchCardUsageRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
|
||||||
|
|
||||||
|
if (templateId.HasValue)
|
||||||
|
{
|
||||||
|
usageQuery = usageQuery.Where(item => item.PunchCardTemplateId == templateId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var todayUsedCount = await usageQuery
|
||||||
|
.Where(item => item.UsedAt >= todayStart && item.UsedAt < todayStart.AddDays(1))
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var monthUsedCount = await usageQuery
|
||||||
|
.Where(item => item.UsedAt >= monthStart && item.UsedAt < monthStart.AddMonths(1))
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var instanceQuery = context.PunchCardInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.Status == PunchCardInstanceStatus.Active &&
|
||||||
|
item.RemainingTimes > 0 &&
|
||||||
|
item.ExpiresAt.HasValue &&
|
||||||
|
item.ExpiresAt.Value >= todayStart &&
|
||||||
|
item.ExpiresAt.Value < soonEnd);
|
||||||
|
|
||||||
|
if (templateId.HasValue)
|
||||||
|
{
|
||||||
|
instanceQuery = instanceQuery.Where(item => item.PunchCardTemplateId == templateId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiringSoonCount = await instanceQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
return new PunchCardUsageStatsSnapshot
|
||||||
|
{
|
||||||
|
TodayUsedCount = todayUsedCount,
|
||||||
|
MonthUsedCount = monthUsedCount,
|
||||||
|
ExpiringSoonCount = expiringSoonCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddUsageRecordAsync(PunchCardUsageRecord entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.PunchCardUsageRecords.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IQueryable<PunchCardUsageRecord> BuildUsageRecordQuery(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long? templateId,
|
||||||
|
string? keyword,
|
||||||
|
PunchCardUsageRecordFilterStatus? status,
|
||||||
|
DateTime nowUtc)
|
||||||
|
{
|
||||||
|
var query = context.PunchCardUsageRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
|
||||||
|
|
||||||
|
if (templateId.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(item => item.PunchCardTemplateId == templateId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var instanceQuery = context.PunchCardInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
|
||||||
|
|
||||||
|
if (templateId.HasValue)
|
||||||
|
{
|
||||||
|
instanceQuery = instanceQuery.Where(item => item.PunchCardTemplateId == templateId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var utcNow = NormalizeUtc(nowUtc);
|
||||||
|
if (status.HasValue)
|
||||||
|
{
|
||||||
|
instanceQuery = status.Value switch
|
||||||
|
{
|
||||||
|
PunchCardUsageRecordFilterStatus.Normal => instanceQuery.Where(item =>
|
||||||
|
item.Status == PunchCardInstanceStatus.Active &&
|
||||||
|
item.RemainingTimes > 0 &&
|
||||||
|
(!item.ExpiresAt.HasValue || item.ExpiresAt.Value >= utcNow)),
|
||||||
|
PunchCardUsageRecordFilterStatus.UsedUp => instanceQuery.Where(item =>
|
||||||
|
item.Status == PunchCardInstanceStatus.UsedUp ||
|
||||||
|
item.RemainingTimes <= 0),
|
||||||
|
PunchCardUsageRecordFilterStatus.Expired => instanceQuery.Where(item =>
|
||||||
|
(item.Status == PunchCardInstanceStatus.Expired ||
|
||||||
|
(item.ExpiresAt.HasValue && item.ExpiresAt.Value < utcNow)) &&
|
||||||
|
item.Status != PunchCardInstanceStatus.Refunded),
|
||||||
|
_ => instanceQuery
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
var normalizedKeyword = $"%{keyword.Trim()}%";
|
||||||
|
var matchedInstanceIds = context.PunchCardInstances
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
(EF.Functions.ILike(item.MemberName, normalizedKeyword) ||
|
||||||
|
EF.Functions.ILike(item.MemberPhoneMasked, normalizedKeyword) ||
|
||||||
|
EF.Functions.ILike(item.InstanceNo, normalizedKeyword)))
|
||||||
|
.Select(item => item.Id);
|
||||||
|
|
||||||
|
query = query.Where(item =>
|
||||||
|
EF.Functions.ILike(item.RecordNo, normalizedKeyword) ||
|
||||||
|
EF.Functions.ILike(item.ProductName, normalizedKeyword) ||
|
||||||
|
matchedInstanceIds.Contains(item.PunchCardInstanceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.HasValue)
|
||||||
|
{
|
||||||
|
var filteredInstanceIds = instanceQuery.Select(item => item.Id);
|
||||||
|
query = query.Where(item => filteredInstanceIds.Contains(item.PunchCardInstanceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime NormalizeUtc(DateTime value)
|
||||||
|
{
|
||||||
|
return value.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Utc => value,
|
||||||
|
DateTimeKind.Local => value.ToUniversalTime(),
|
||||||
|
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9190
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302065925_AddNewCustomerGiftModule.Designer.cs
generated
Normal file
9190
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302065925_AddNewCustomerGiftModule.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,168 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddNewCustomerGiftModule : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "new_customer_coupon_rules",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
Scene = table.Column<int>(type: "integer", nullable: false, comment: "券规则场景。"),
|
||||||
|
CouponType = table.Column<int>(type: "integer", nullable: false, comment: "券类型。"),
|
||||||
|
Value = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "面值或折扣值。"),
|
||||||
|
MinimumSpend = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "使用门槛。"),
|
||||||
|
ValidDays = table.Column<int>(type: "integer", nullable: false, comment: "有效期天数。"),
|
||||||
|
SortOrder = table.Column<int>(type: "integer", nullable: false, comment: "排序值(同场景内递增)。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_new_customer_coupon_rules", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "新客有礼券规则。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "new_customer_gift_settings",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
GiftEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否开启新客礼包。"),
|
||||||
|
GiftType = table.Column<int>(type: "integer", nullable: false, comment: "礼包类型。"),
|
||||||
|
DirectReduceAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "首单直减金额。"),
|
||||||
|
DirectMinimumSpend = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "首单直减门槛金额。"),
|
||||||
|
InviteEnabled = table.Column<bool>(type: "boolean", nullable: false, comment: "是否开启老带新分享。"),
|
||||||
|
ShareChannelsJson = table.Column<string>(type: "text", nullable: false, comment: "分享渠道(JSON)。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_new_customer_gift_settings", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "新客有礼门店配置。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "new_customer_growth_records",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
CustomerKey = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "顾客业务唯一键。"),
|
||||||
|
CustomerName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true, comment: "顾客展示名。"),
|
||||||
|
RegisteredAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "注册时间。"),
|
||||||
|
GiftClaimedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "礼包领取时间。"),
|
||||||
|
FirstOrderAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "首单时间。"),
|
||||||
|
SourceChannel = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true, comment: "渠道来源。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_new_customer_growth_records", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "新客成长记录。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "new_customer_invite_records",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
InviterName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "邀请人展示名。"),
|
||||||
|
InviteeName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "被邀请人展示名。"),
|
||||||
|
InviteTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "邀请时间。"),
|
||||||
|
OrderStatus = table.Column<int>(type: "integer", nullable: false, comment: "订单状态。"),
|
||||||
|
RewardStatus = table.Column<int>(type: "integer", nullable: false, comment: "奖励状态。"),
|
||||||
|
RewardIssuedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "奖励发放时间。"),
|
||||||
|
SourceChannel = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true, comment: "邀请来源渠道。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_new_customer_invite_records", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "新客邀请记录。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_new_customer_coupon_rules_TenantId_StoreId_Scene_SortOrder",
|
||||||
|
table: "new_customer_coupon_rules",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "Scene", "SortOrder" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_new_customer_gift_settings_TenantId_StoreId",
|
||||||
|
table: "new_customer_gift_settings",
|
||||||
|
columns: new[] { "TenantId", "StoreId" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_new_customer_growth_records_TenantId_StoreId_CustomerKey",
|
||||||
|
table: "new_customer_growth_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "CustomerKey" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_new_customer_growth_records_TenantId_StoreId_RegisteredAt",
|
||||||
|
table: "new_customer_growth_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "RegisteredAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_new_customer_invite_records_TenantId_StoreId_InviteTime",
|
||||||
|
table: "new_customer_invite_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "InviteTime" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "new_customer_coupon_rules");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "new_customer_gift_settings");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "new_customer_growth_records");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "new_customer_invite_records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9547
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs
generated
Normal file
9547
src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260302125930_AddPunchCardModule.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPunchCardModule : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "punch_card_instances",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
PunchCardTemplateId = table.Column<long>(type: "bigint", nullable: false, comment: "次卡模板 ID。"),
|
||||||
|
InstanceNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "实例编号(业务唯一)。"),
|
||||||
|
MemberName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "会员名称。"),
|
||||||
|
MemberPhoneMasked = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "会员手机号(脱敏)。"),
|
||||||
|
PurchasedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "购买时间(UTC)。"),
|
||||||
|
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "过期时间(UTC,可空)。"),
|
||||||
|
TotalTimes = table.Column<int>(type: "integer", nullable: false, comment: "总次数。"),
|
||||||
|
RemainingTimes = table.Column<int>(type: "integer", nullable: false, comment: "剩余次数。"),
|
||||||
|
PaidAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "实付金额。"),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false, comment: "实例状态。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_punch_card_instances", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "次卡实例(顾客购买后生成)。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "punch_card_templates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "次卡名称。"),
|
||||||
|
CoverImageUrl = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true, comment: "封面图片地址。"),
|
||||||
|
SalePrice = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "售价。"),
|
||||||
|
OriginalPrice = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "原价。"),
|
||||||
|
TotalTimes = table.Column<int>(type: "integer", nullable: false, comment: "总次数。"),
|
||||||
|
ValidityType = table.Column<int>(type: "integer", nullable: false, comment: "有效期类型。"),
|
||||||
|
ValidityDays = table.Column<int>(type: "integer", nullable: true, comment: "固定天数(ValidityType=Days 时有效)。"),
|
||||||
|
ValidFrom = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "固定开始日期(UTC,ValidityType=DateRange 时有效)。"),
|
||||||
|
ValidTo = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "固定结束日期(UTC,ValidityType=DateRange 时有效)。"),
|
||||||
|
ScopeType = table.Column<int>(type: "integer", nullable: false, comment: "适用范围类型。"),
|
||||||
|
ScopeCategoryIdsJson = table.Column<string>(type: "text", nullable: false, comment: "指定分类 ID JSON。"),
|
||||||
|
ScopeTagIdsJson = table.Column<string>(type: "text", nullable: false, comment: "指定标签 ID JSON。"),
|
||||||
|
ScopeProductIdsJson = table.Column<string>(type: "text", nullable: false, comment: "指定商品 ID JSON。"),
|
||||||
|
UsageMode = table.Column<int>(type: "integer", nullable: false, comment: "使用模式。"),
|
||||||
|
UsageCapAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "金额上限(UsageMode=Cap 时有效)。"),
|
||||||
|
DailyLimit = table.Column<int>(type: "integer", nullable: true, comment: "每日限用次数。"),
|
||||||
|
PerOrderLimit = table.Column<int>(type: "integer", nullable: true, comment: "每单限用次数。"),
|
||||||
|
PerUserPurchaseLimit = table.Column<int>(type: "integer", nullable: true, comment: "每人限购张数。"),
|
||||||
|
AllowTransfer = table.Column<bool>(type: "boolean", nullable: false, comment: "是否允许转赠。"),
|
||||||
|
ExpireStrategy = table.Column<int>(type: "integer", nullable: false, comment: "过期策略。"),
|
||||||
|
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true, comment: "次卡描述。"),
|
||||||
|
NotifyChannelsJson = table.Column<string>(type: "text", nullable: false, comment: "购买通知渠道 JSON。"),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false, comment: "次卡状态。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_punch_card_templates", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "次卡模板配置。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "punch_card_usage_records",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false, comment: "实体唯一标识。")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
StoreId = table.Column<long>(type: "bigint", nullable: false, comment: "门店 ID。"),
|
||||||
|
PunchCardTemplateId = table.Column<long>(type: "bigint", nullable: false, comment: "次卡模板 ID。"),
|
||||||
|
PunchCardInstanceId = table.Column<long>(type: "bigint", nullable: false, comment: "次卡实例 ID。"),
|
||||||
|
RecordNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "使用单号。"),
|
||||||
|
ProductName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false, comment: "兑换商品名称。"),
|
||||||
|
UsedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "使用时间(UTC)。"),
|
||||||
|
UsedTimes = table.Column<int>(type: "integer", nullable: false, comment: "本次使用次数。"),
|
||||||
|
RemainingTimesAfterUse = table.Column<int>(type: "integer", nullable: false, comment: "使用后剩余次数。"),
|
||||||
|
StatusAfterUse = table.Column<int>(type: "integer", nullable: false, comment: "本次记录状态。"),
|
||||||
|
ExtraPayAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "超额补差金额。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_punch_card_usage_records", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "次卡使用记录。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_instances_TenantId_StoreId_InstanceNo",
|
||||||
|
table: "punch_card_instances",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "InstanceNo" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_instances_TenantId_StoreId_PunchCardTemplateId",
|
||||||
|
table: "punch_card_instances",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "PunchCardTemplateId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_instances_TenantId_StoreId_Status_ExpiresAt",
|
||||||
|
table: "punch_card_instances",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "Status", "ExpiresAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_templates_TenantId_StoreId_Name",
|
||||||
|
table: "punch_card_templates",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "Name" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_templates_TenantId_StoreId_Status",
|
||||||
|
table: "punch_card_templates",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "Status" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_usage_records_TenantId_StoreId_PunchCardInstance~",
|
||||||
|
table: "punch_card_usage_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "PunchCardInstanceId", "UsedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_usage_records_TenantId_StoreId_PunchCardTemplate~",
|
||||||
|
table: "punch_card_usage_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "PunchCardTemplateId", "UsedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_punch_card_usage_records_TenantId_StoreId_RecordNo",
|
||||||
|
table: "punch_card_usage_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "RecordNo" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "punch_card_instances");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "punch_card_templates");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "punch_card_usage_records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -604,6 +604,328 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerCouponRule", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CouponType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("券类型。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("MinimumSpend")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("使用门槛。");
|
||||||
|
|
||||||
|
b.Property<int>("Scene")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("券规则场景。");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("排序值(同场景内递增)。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<int>("ValidDays")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("有效期天数。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Value")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("面值或折扣值。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "Scene", "SortOrder");
|
||||||
|
|
||||||
|
b.ToTable("new_customer_coupon_rules", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("新客有礼券规则。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerGiftSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DirectMinimumSpend")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("首单直减门槛金额。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DirectReduceAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("首单直减金额。");
|
||||||
|
|
||||||
|
b.Property<bool>("GiftEnabled")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否开启新客礼包。");
|
||||||
|
|
||||||
|
b.Property<int>("GiftType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("礼包类型。");
|
||||||
|
|
||||||
|
b.Property<bool>("InviteEnabled")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否开启老带新分享。");
|
||||||
|
|
||||||
|
b.Property<string>("ShareChannelsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("分享渠道(JSON)。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("new_customer_gift_settings", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("新客有礼门店配置。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerGrowthRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("顾客业务唯一键。");
|
||||||
|
|
||||||
|
b.Property<string>("CustomerName")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("顾客展示名。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("FirstOrderAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("首单时间。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("GiftClaimedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("礼包领取时间。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RegisteredAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("注册时间。");
|
||||||
|
|
||||||
|
b.Property<string>("SourceChannel")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("渠道来源。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "CustomerKey")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "RegisteredAt");
|
||||||
|
|
||||||
|
b.ToTable("new_customer_growth_records", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("新客成长记录。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerInviteRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("InviteTime")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("邀请时间。");
|
||||||
|
|
||||||
|
b.Property<string>("InviteeName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("被邀请人展示名。");
|
||||||
|
|
||||||
|
b.Property<string>("InviterName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("邀请人展示名。");
|
||||||
|
|
||||||
|
b.Property<int>("OrderStatus")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("订单状态。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RewardIssuedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("奖励发放时间。");
|
||||||
|
|
||||||
|
b.Property<int>("RewardStatus")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("奖励状态。");
|
||||||
|
|
||||||
|
b.Property<string>("SourceChannel")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("邀请来源渠道。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "InviteTime");
|
||||||
|
|
||||||
|
b.ToTable("new_customer_invite_records", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("新客邀请记录。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -690,6 +1012,363 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("过期时间(UTC,可空)。");
|
||||||
|
|
||||||
|
b.Property<string>("InstanceNo")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("实例编号(业务唯一)。");
|
||||||
|
|
||||||
|
b.Property<string>("MemberName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("会员名称。");
|
||||||
|
|
||||||
|
b.Property<string>("MemberPhoneMasked")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("会员手机号(脱敏)。");
|
||||||
|
|
||||||
|
b.Property<decimal>("PaidAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("实付金额。");
|
||||||
|
|
||||||
|
b.Property<long>("PunchCardTemplateId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("次卡模板 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("PurchasedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("购买时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<int>("RemainingTimes")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("剩余次数。");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("实例状态。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<int>("TotalTimes")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("总次数。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "InstanceNo")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "Status", "ExpiresAt");
|
||||||
|
|
||||||
|
b.ToTable("punch_card_instances", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("次卡实例(顾客购买后生成)。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardTemplate", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<bool>("AllowTransfer")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否允许转赠。");
|
||||||
|
|
||||||
|
b.Property<string>("CoverImageUrl")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasComment("封面图片地址。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<int?>("DailyLimit")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("每日限用次数。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)")
|
||||||
|
.HasComment("次卡描述。");
|
||||||
|
|
||||||
|
b.Property<int>("ExpireStrategy")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("过期策略。");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("次卡名称。");
|
||||||
|
|
||||||
|
b.Property<string>("NotifyChannelsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("购买通知渠道 JSON。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OriginalPrice")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("原价。");
|
||||||
|
|
||||||
|
b.Property<int?>("PerOrderLimit")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("每单限用次数。");
|
||||||
|
|
||||||
|
b.Property<int?>("PerUserPurchaseLimit")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("每人限购张数。");
|
||||||
|
|
||||||
|
b.Property<decimal>("SalePrice")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("售价。");
|
||||||
|
|
||||||
|
b.Property<string>("ScopeCategoryIdsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("指定分类 ID JSON。");
|
||||||
|
|
||||||
|
b.Property<string>("ScopeProductIdsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("指定商品 ID JSON。");
|
||||||
|
|
||||||
|
b.Property<string>("ScopeTagIdsJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasComment("指定标签 ID JSON。");
|
||||||
|
|
||||||
|
b.Property<int>("ScopeType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("适用范围类型。");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("次卡状态。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<int>("TotalTimes")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("总次数。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UsageCapAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("金额上限(UsageMode=Cap 时有效)。");
|
||||||
|
|
||||||
|
b.Property<int>("UsageMode")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("使用模式。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ValidFrom")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("固定开始日期(UTC,ValidityType=DateRange 时有效)。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ValidTo")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("固定结束日期(UTC,ValidityType=DateRange 时有效)。");
|
||||||
|
|
||||||
|
b.Property<int?>("ValidityDays")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("固定天数(ValidityType=Days 时有效)。");
|
||||||
|
|
||||||
|
b.Property<int>("ValidityType")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("有效期类型。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "Status");
|
||||||
|
|
||||||
|
b.ToTable("punch_card_templates", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("次卡模板配置。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardUsageRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("创建时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<long?>("CreatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("软删除时间(UTC),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("DeletedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ExtraPayAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("超额补差金额。");
|
||||||
|
|
||||||
|
b.Property<string>("ProductName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasComment("兑换商品名称。");
|
||||||
|
|
||||||
|
b.Property<long>("PunchCardInstanceId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("次卡实例 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("PunchCardTemplateId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("次卡模板 ID。");
|
||||||
|
|
||||||
|
b.Property<string>("RecordNo")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("使用单号。");
|
||||||
|
|
||||||
|
b.Property<int>("RemainingTimesAfterUse")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("使用后剩余次数。");
|
||||||
|
|
||||||
|
b.Property<int>("StatusAfterUse")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("本次记录状态。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店 ID。");
|
||||||
|
|
||||||
|
b.Property<long>("TenantId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("所属租户 ID。");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("最近一次更新时间(UTC),从未更新时为 null。");
|
||||||
|
|
||||||
|
b.Property<long?>("UpdatedBy")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UsedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("使用时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<int>("UsedTimes")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("本次使用次数。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "RecordNo")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "PunchCardInstanceId", "UsedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId", "UsedAt");
|
||||||
|
|
||||||
|
b.ToTable("punch_card_usage_records", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("次卡使用记录。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
|
|||||||
Reference in New Issue
Block a user