feat(marketing): add new customer gift backend module
This commit is contained in:
@@ -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,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,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,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,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,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);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public static class AppServiceCollectionExtensions
|
||||
services.AddScoped<IStoreRepository, EfStoreRepository>();
|
||||
services.AddScoped<IProductRepository, EfProductRepository>();
|
||||
services.AddScoped<ICouponRepository, EfCouponRepository>();
|
||||
services.AddScoped<INewCustomerGiftRepository, EfNewCustomerGiftRepository>();
|
||||
services.AddScoped<IPromotionCampaignRepository, EfPromotionCampaignRepository>();
|
||||
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
||||
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
||||
|
||||
@@ -354,6 +354,22 @@ public sealed class TakeoutAppDbContext(
|
||||
/// </summary>
|
||||
public DbSet<PromotionCampaign> PromotionCampaigns => Set<PromotionCampaign>();
|
||||
/// <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<MemberProfile> MemberProfiles => Set<MemberProfile>();
|
||||
@@ -520,6 +536,10 @@ public sealed class TakeoutAppDbContext(
|
||||
ConfigureCouponTemplate(modelBuilder.Entity<CouponTemplate>());
|
||||
ConfigureCoupon(modelBuilder.Entity<Coupon>());
|
||||
ConfigurePromotionCampaign(modelBuilder.Entity<PromotionCampaign>());
|
||||
ConfigureNewCustomerGiftSetting(modelBuilder.Entity<NewCustomerGiftSetting>());
|
||||
ConfigureNewCustomerCouponRule(modelBuilder.Entity<NewCustomerCouponRule>());
|
||||
ConfigureNewCustomerInviteRecord(modelBuilder.Entity<NewCustomerInviteRecord>());
|
||||
ConfigureNewCustomerGrowthRecord(modelBuilder.Entity<NewCustomerGrowthRecord>());
|
||||
ConfigureMemberProfile(modelBuilder.Entity<MemberProfile>());
|
||||
ConfigureMemberTier(modelBuilder.Entity<MemberTier>());
|
||||
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
|
||||
@@ -1619,6 +1639,59 @@ public sealed class TakeoutAppDbContext(
|
||||
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 ConfigureMemberProfile(EntityTypeBuilder<MemberProfile> builder)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
||||
Reference in New Issue
Block a user