Merge pull request #3 from msumshk/feature/member-points-mall-1to1
feat(member): implement points mall backend module
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 修改积分商城商品状态命令。
|
||||
/// </summary>
|
||||
public sealed class ChangePointMallProductStatusCommand : IRequest<MemberPointMallProductDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城商品标识。
|
||||
/// </summary>
|
||||
public long PointMallProductId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "disabled";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 删除积分商城商品命令。
|
||||
/// </summary>
|
||||
public sealed class DeletePointMallProductCommand : IRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城商品标识。
|
||||
/// </summary>
|
||||
public long PointMallProductId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 保存积分商城兑换商品命令。
|
||||
/// </summary>
|
||||
public sealed class SavePointMallProductCommand : IRequest<MemberPointMallProductDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城商品标识(编辑时传)。
|
||||
/// </summary>
|
||||
public long? PointMallProductId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示名称。
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 展示图片。
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换类型(product/coupon/physical)。
|
||||
/// </summary>
|
||||
public string RedeemType { get; init; } = "product";
|
||||
|
||||
/// <summary>
|
||||
/// 关联商品 ID。
|
||||
/// </summary>
|
||||
public long? ProductId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联优惠券模板 ID。
|
||||
/// </summary>
|
||||
public long? CouponTemplateId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 实物名称。
|
||||
/// </summary>
|
||||
public string? PhysicalName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 领取方式(store_pickup/delivery)。
|
||||
/// </summary>
|
||||
public string? PickupMethod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品描述。
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换方式(points/mixed)。
|
||||
/// </summary>
|
||||
public string ExchangeType { get; init; } = "points";
|
||||
|
||||
/// <summary>
|
||||
/// 所需积分。
|
||||
/// </summary>
|
||||
public int RequiredPoints { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 现金部分。
|
||||
/// </summary>
|
||||
public decimal CashAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 库存总量。
|
||||
/// </summary>
|
||||
public int StockTotal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 每人限兑次数(null 表示不限)。
|
||||
/// </summary>
|
||||
public int? PerMemberLimit { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 到账通知渠道。
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> NotifyChannels { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 状态(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string Status { get; init; } = "enabled";
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 保存积分商城规则命令。
|
||||
/// </summary>
|
||||
public sealed class SavePointMallRuleCommand : IRequest<MemberPointMallRuleDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用消费获取。
|
||||
/// </summary>
|
||||
public bool IsConsumeRewardEnabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 每消费多少元触发一次积分计算。
|
||||
/// </summary>
|
||||
public int ConsumeAmountPerStep { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 每步获得积分。
|
||||
/// </summary>
|
||||
public int ConsumeRewardPointsPerStep { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用评价奖励。
|
||||
/// </summary>
|
||||
public bool IsReviewRewardEnabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 评价奖励积分。
|
||||
/// </summary>
|
||||
public int ReviewRewardPoints { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用注册奖励。
|
||||
/// </summary>
|
||||
public bool IsRegisterRewardEnabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册奖励积分。
|
||||
/// </summary>
|
||||
public int RegisterRewardPoints { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用签到奖励。
|
||||
/// </summary>
|
||||
public bool IsSigninRewardEnabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到奖励积分。
|
||||
/// </summary>
|
||||
public int SigninRewardPoints { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 有效期模式(permanent/yearly_clear)。
|
||||
/// </summary>
|
||||
public string ExpiryMode { get; init; } = "yearly_clear";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 核销积分商城兑换记录命令。
|
||||
/// </summary>
|
||||
public sealed class VerifyPointMallRecordCommand : IRequest<MemberPointMallRecordDetailDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换记录标识。
|
||||
/// </summary>
|
||||
public long RecordId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 核销方式(scan/manual)。
|
||||
/// </summary>
|
||||
public string VerifyMethod { get; init; } = "manual";
|
||||
|
||||
/// <summary>
|
||||
/// 核销备注。
|
||||
/// </summary>
|
||||
public string? VerifyRemark { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 写入积分商城兑换记录命令。
|
||||
/// </summary>
|
||||
public sealed class WritePointMallRecordCommand : IRequest<MemberPointMallRecordDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城商品标识。
|
||||
/// </summary>
|
||||
public long PointMallProductId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员标识。
|
||||
/// </summary>
|
||||
public long MemberId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换时间(可空,默认当前 UTC)。
|
||||
/// </summary>
|
||||
public DateTime? RedeemedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城兑换商品数据。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallProductDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 积分商城商品标识。
|
||||
/// </summary>
|
||||
public long PointMallProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示名称。
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 展示图片。
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换类型编码(product/coupon/physical)。
|
||||
/// </summary>
|
||||
public string RedeemType { get; set; } = "product";
|
||||
|
||||
/// <summary>
|
||||
/// 关联商品 ID。
|
||||
/// </summary>
|
||||
public long? ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联优惠券模板 ID。
|
||||
/// </summary>
|
||||
public long? CouponTemplateId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实物名称。
|
||||
/// </summary>
|
||||
public string? PhysicalName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 领取方式编码(store_pickup/delivery)。
|
||||
/// </summary>
|
||||
public string? PickupMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品描述。
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换方式编码(points/mixed)。
|
||||
/// </summary>
|
||||
public string ExchangeType { get; set; } = "points";
|
||||
|
||||
/// <summary>
|
||||
/// 所需积分。
|
||||
/// </summary>
|
||||
public int RequiredPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 现金部分。
|
||||
/// </summary>
|
||||
public decimal CashAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始库存。
|
||||
/// </summary>
|
||||
public int StockTotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余库存。
|
||||
/// </summary>
|
||||
public int StockAvailable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已兑换数量。
|
||||
/// </summary>
|
||||
public int RedeemedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每人限兑次数。
|
||||
/// </summary>
|
||||
public int? PerMemberLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通知渠道列表(in_app/sms)。
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> NotifyChannels { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 状态(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "enabled";
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城兑换商品列表结果。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallProductListResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public IReadOnlyList<MemberPointMallProductDto> Items { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城兑换记录详情数据。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallRecordDetailDto : MemberPointMallRecordDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 核销方式(scan/manual)。
|
||||
/// </summary>
|
||||
public string? VerifyMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 核销备注。
|
||||
/// </summary>
|
||||
public string? VerifyRemark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 核销人标识。
|
||||
/// </summary>
|
||||
public long? VerifiedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城兑换记录数据。
|
||||
/// </summary>
|
||||
public class MemberPointMallRecordDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 兑换记录标识。
|
||||
/// </summary>
|
||||
public long RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换单号。
|
||||
/// </summary>
|
||||
public string RecordNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城商品标识。
|
||||
/// </summary>
|
||||
public long PointMallProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品名称。
|
||||
/// </summary>
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 兑换类型(product/coupon/physical)。
|
||||
/// </summary>
|
||||
public string RedeemType { get; set; } = "product";
|
||||
|
||||
/// <summary>
|
||||
/// 兑换方式(points/mixed)。
|
||||
/// </summary>
|
||||
public string ExchangeType { get; set; } = "points";
|
||||
|
||||
/// <summary>
|
||||
/// 会员标识。
|
||||
/// </summary>
|
||||
public long MemberId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员名称。
|
||||
/// </summary>
|
||||
public string MemberName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 会员手机号(脱敏)。
|
||||
/// </summary>
|
||||
public string MemberMobileMasked { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 消耗积分。
|
||||
/// </summary>
|
||||
public int UsedPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 现金部分。
|
||||
/// </summary>
|
||||
public decimal CashAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录状态(pending_pickup/issued/completed/canceled)。
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "issued";
|
||||
|
||||
/// <summary>
|
||||
/// 兑换时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime RedeemedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发放时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? IssuedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 核销时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? VerifiedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城兑换记录导出结果。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallRecordExportDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Base64 文件内容。
|
||||
/// </summary>
|
||||
public string FileContentBase64 { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 导出总条数。
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城兑换记录列表结果。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallRecordListResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 列表项。
|
||||
/// </summary>
|
||||
public IReadOnlyList<MemberPointMallRecordDto> 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 MemberPointMallRecordStatsDto Stats { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城兑换记录页统计。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallRecordStatsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 今日兑换。
|
||||
/// </summary>
|
||||
public int TodayRedeemCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 待领取实物。
|
||||
/// </summary>
|
||||
public int PendingPhysicalCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本月消耗积分。
|
||||
/// </summary>
|
||||
public int CurrentMonthUsedPoints { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分规则页详情结果。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallRuleDetailResultDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 规则配置。
|
||||
/// </summary>
|
||||
public MemberPointMallRuleDto Rule { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 统计数据。
|
||||
/// </summary>
|
||||
public MemberPointMallRuleStatsDto Stats { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分规则数据。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallRuleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用消费获取。
|
||||
/// </summary>
|
||||
public bool IsConsumeRewardEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每消费多少元触发一次积分计算。
|
||||
/// </summary>
|
||||
public int ConsumeAmountPerStep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每步获得积分。
|
||||
/// </summary>
|
||||
public int ConsumeRewardPointsPerStep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用评价奖励。
|
||||
/// </summary>
|
||||
public bool IsReviewRewardEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 评价奖励积分。
|
||||
/// </summary>
|
||||
public int ReviewRewardPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用注册奖励。
|
||||
/// </summary>
|
||||
public bool IsRegisterRewardEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 注册奖励积分。
|
||||
/// </summary>
|
||||
public int RegisterRewardPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用签到奖励。
|
||||
/// </summary>
|
||||
public bool IsSigninRewardEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到奖励积分。
|
||||
/// </summary>
|
||||
public int SigninRewardPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 有效期模式(permanent/yearly_clear)。
|
||||
/// </summary>
|
||||
public string ExpiryMode { get; set; } = "yearly_clear";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 积分规则页统计数据。
|
||||
/// </summary>
|
||||
public sealed class MemberPointMallRuleStatsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 累计发放积分。
|
||||
/// </summary>
|
||||
public int TotalIssuedPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已兑换积分。
|
||||
/// </summary>
|
||||
public int RedeemedPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 积分用户。
|
||||
/// </summary>
|
||||
public int PointMembers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换率(0-100)。
|
||||
/// </summary>
|
||||
public decimal RedeemRate { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 修改积分商城商品状态处理器。
|
||||
/// </summary>
|
||||
public sealed class ChangePointMallProductStatusCommandHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ChangePointMallProductStatusCommand, MemberPointMallProductDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallProductDto> Handle(
|
||||
ChangePointMallProductStatusCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var status = MemberPointMallMapping.ParseProductStatus(request.Status);
|
||||
|
||||
var product = await repository.FindProductByIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PointMallProductId,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "兑换商品不存在");
|
||||
|
||||
product.Status = status;
|
||||
await repository.UpdateProductAsync(product, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var aggregates = await repository.GetProductAggregatesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
[product.Id],
|
||||
cancellationToken);
|
||||
|
||||
var aggregate = aggregates.TryGetValue(product.Id, out var value)
|
||||
? value
|
||||
: MemberPointMallDtoFactory.EmptyProductAggregate(product.Id);
|
||||
|
||||
return MemberPointMallDtoFactory.ToProductDto(product, aggregate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 删除积分商城商品处理器。
|
||||
/// </summary>
|
||||
public sealed class DeletePointMallProductCommandHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<DeletePointMallProductCommand>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task Handle(DeletePointMallProductCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
var product = await repository.FindProductByIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PointMallProductId,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "兑换商品不存在");
|
||||
|
||||
var hasRecords = await repository.HasRecordsByProductIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PointMallProductId,
|
||||
cancellationToken);
|
||||
|
||||
if (hasRecords)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "存在兑换记录的商品不允许删除");
|
||||
}
|
||||
|
||||
await repository.DeleteProductAsync(product, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Text;
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
using TakeoutSaaS.Domain.Membership.Entities;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 导出积分商城兑换记录处理器。
|
||||
/// </summary>
|
||||
public sealed class ExportPointMallRecordCsvQueryHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<ExportPointMallRecordCsvQuery, MemberPointMallRecordExportDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallRecordExportDto> Handle(
|
||||
ExportPointMallRecordCsvQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var redeemType = MemberPointMallMapping.TryParseRedeemType(request.RedeemType);
|
||||
var status = MemberPointMallMapping.TryParseRecordStatus(request.Status);
|
||||
var keyword = MemberPointMallMapping.NormalizeKeyword(request.Keyword);
|
||||
var (startUtc, endUtc) = MemberPointMallMapping.NormalizeDateRange(
|
||||
request.StartDateUtc,
|
||||
request.EndDateUtc);
|
||||
|
||||
var records = await repository.ListRecordsForExportAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
redeemType,
|
||||
status,
|
||||
startUtc,
|
||||
endUtc,
|
||||
keyword,
|
||||
cancellationToken);
|
||||
|
||||
var csv = BuildCsv(records);
|
||||
var bytes = Encoding.UTF8.GetBytes($"\uFEFF{csv}");
|
||||
|
||||
return new MemberPointMallRecordExportDto
|
||||
{
|
||||
FileName = $"积分商城兑换记录_{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
|
||||
FileContentBase64 = Convert.ToBase64String(bytes),
|
||||
TotalCount = records.Count
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildCsv(IReadOnlyCollection<MemberPointMallRecord> records)
|
||||
{
|
||||
var lines = new List<string>
|
||||
{
|
||||
"兑换单号,会员,手机号,兑换商品,类型,消耗积分,现金部分,兑换时间,状态,核销时间"
|
||||
};
|
||||
|
||||
foreach (var item in records)
|
||||
{
|
||||
lines.Add(string.Join(",",
|
||||
Escape(item.RecordNo),
|
||||
Escape(item.MemberName),
|
||||
Escape(item.MemberMobileMasked),
|
||||
Escape(item.ProductName),
|
||||
Escape(MemberPointMallMapping.ToRedeemTypeDisplayText(item.RedeemType)),
|
||||
item.UsedPoints.ToString(),
|
||||
item.CashAmount.ToString("0.00"),
|
||||
Escape(item.RedeemedAt.ToString("yyyy-MM-dd HH:mm:ss")),
|
||||
Escape(MemberPointMallMapping.ToRecordStatusDisplayText(item.Status)),
|
||||
Escape(item.VerifiedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? string.Empty)));
|
||||
}
|
||||
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
private static string Escape(string value)
|
||||
{
|
||||
var normalized = value.Replace("\"", "\"\"");
|
||||
return $"\"{normalized}\"";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城商品详情处理器。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallProductDetailQueryHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetPointMallProductDetailQuery, MemberPointMallProductDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallProductDto> Handle(
|
||||
GetPointMallProductDetailQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
var product = await repository.GetProductByIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PointMallProductId,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "兑换商品不存在");
|
||||
|
||||
var aggregates = await repository.GetProductAggregatesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
[product.Id],
|
||||
cancellationToken);
|
||||
|
||||
var aggregate = aggregates.TryGetValue(product.Id, out var value)
|
||||
? value
|
||||
: MemberPointMallDtoFactory.EmptyProductAggregate(product.Id);
|
||||
|
||||
return MemberPointMallDtoFactory.ToProductDto(product, aggregate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城商品列表处理器。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallProductListQueryHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetPointMallProductListQuery, MemberPointMallProductListResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallProductListResultDto> Handle(
|
||||
GetPointMallProductListQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var status = MemberPointMallMapping.TryParseProductStatus(request.Status);
|
||||
var keyword = MemberPointMallMapping.NormalizeKeyword(request.Keyword);
|
||||
|
||||
var items = await repository.SearchProductsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
status,
|
||||
keyword,
|
||||
cancellationToken);
|
||||
|
||||
var productIds = items.Select(item => item.Id).ToList();
|
||||
var aggregates = await repository.GetProductAggregatesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
productIds,
|
||||
cancellationToken);
|
||||
|
||||
var rows = items
|
||||
.Select(item =>
|
||||
{
|
||||
var aggregate = aggregates.TryGetValue(item.Id, out var value)
|
||||
? value
|
||||
: MemberPointMallDtoFactory.EmptyProductAggregate(item.Id);
|
||||
return MemberPointMallDtoFactory.ToProductDto(item, aggregate);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new MemberPointMallProductListResultDto
|
||||
{
|
||||
Items = rows
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城兑换记录详情处理器。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallRecordDetailQueryHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetPointMallRecordDetailQuery, MemberPointMallRecordDetailDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallRecordDetailDto> Handle(
|
||||
GetPointMallRecordDetailQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
var record = await repository.GetRecordByIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.RecordId,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "兑换记录不存在");
|
||||
|
||||
return MemberPointMallDtoFactory.ToRecordDetailDto(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城兑换记录分页处理器。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallRecordListQueryHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetPointMallRecordListQuery, MemberPointMallRecordListResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallRecordListResultDto> Handle(
|
||||
GetPointMallRecordListQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var redeemType = MemberPointMallMapping.TryParseRedeemType(request.RedeemType);
|
||||
var status = MemberPointMallMapping.TryParseRecordStatus(request.Status);
|
||||
var keyword = MemberPointMallMapping.NormalizeKeyword(request.Keyword);
|
||||
var page = Math.Max(1, request.Page);
|
||||
var pageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||
var (startUtc, endUtc) = MemberPointMallMapping.NormalizeDateRange(
|
||||
request.StartDateUtc,
|
||||
request.EndDateUtc);
|
||||
|
||||
var (items, totalCount) = await repository.SearchRecordsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
redeemType,
|
||||
status,
|
||||
startUtc,
|
||||
endUtc,
|
||||
keyword,
|
||||
page,
|
||||
pageSize,
|
||||
cancellationToken);
|
||||
|
||||
var stats = await repository.GetRecordStatsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
DateTime.UtcNow,
|
||||
cancellationToken);
|
||||
|
||||
return new MemberPointMallRecordListResultDto
|
||||
{
|
||||
Items = items.Select(MemberPointMallDtoFactory.ToRecordDto).ToList(),
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
TotalCount = totalCount,
|
||||
Stats = MemberPointMallDtoFactory.ToRecordStatsDto(stats)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
using TakeoutSaaS.Domain.Membership.Entities;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城规则详情处理器。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallRuleDetailQueryHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<GetPointMallRuleDetailQuery, MemberPointMallRuleDetailResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallRuleDetailResultDto> Handle(
|
||||
GetPointMallRuleDetailQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
|
||||
var rule = await repository.GetRuleByStoreAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken) ?? new MemberPointMallRule
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
IsConsumeRewardEnabled = true,
|
||||
ConsumeAmountPerStep = 1,
|
||||
ConsumeRewardPointsPerStep = 1,
|
||||
IsReviewRewardEnabled = true,
|
||||
ReviewRewardPoints = 10,
|
||||
IsRegisterRewardEnabled = true,
|
||||
RegisterRewardPoints = 100,
|
||||
IsSigninRewardEnabled = false,
|
||||
SigninRewardPoints = 5
|
||||
};
|
||||
|
||||
var stats = await repository.GetRuleStatsAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken);
|
||||
|
||||
return new MemberPointMallRuleDetailResultDto
|
||||
{
|
||||
Rule = MemberPointMallDtoFactory.ToRuleDto(rule),
|
||||
Stats = MemberPointMallDtoFactory.ToRuleStatsDto(stats)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Domain.Membership.Entities;
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 保存积分商城商品处理器。
|
||||
/// </summary>
|
||||
public sealed class SavePointMallProductCommandHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SavePointMallProductCommand, MemberPointMallProductDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallProductDto> Handle(
|
||||
SavePointMallProductCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var redeemType = MemberPointMallMapping.ParseRedeemType(request.RedeemType);
|
||||
var exchangeType = MemberPointMallMapping.ParseExchangeType(request.ExchangeType);
|
||||
var status = MemberPointMallMapping.ParseProductStatus(request.Status);
|
||||
var name = MemberPointMallMapping.NormalizeName(request.Name);
|
||||
var imageUrl = MemberPointMallMapping.NormalizeImageUrl(request.ImageUrl);
|
||||
var description = MemberPointMallMapping.NormalizeDescription(request.Description);
|
||||
var requiredPoints = MemberPointMallMapping.NormalizeRequiredPoints(request.RequiredPoints);
|
||||
var cashAmount = MemberPointMallMapping.NormalizeCashAmount(request.CashAmount, exchangeType);
|
||||
var stockTotal = MemberPointMallMapping.NormalizeStockTotal(request.StockTotal);
|
||||
var perMemberLimit = MemberPointMallMapping.NormalizePerMemberLimit(request.PerMemberLimit);
|
||||
var notifyChannels = MemberPointMallMapping.ParseNotifyChannels(request.NotifyChannels);
|
||||
|
||||
var productId = (long?)null;
|
||||
var couponTemplateId = (long?)null;
|
||||
var physicalName = (string?)null;
|
||||
MemberPointMallPickupMethod? pickupMethod = null;
|
||||
|
||||
switch (redeemType)
|
||||
{
|
||||
case MemberPointMallRedeemType.Product:
|
||||
{
|
||||
productId = request.ProductId.HasValue && request.ProductId.Value > 0
|
||||
? request.ProductId.Value
|
||||
: throw new BusinessException(ErrorCodes.BadRequest, "兑换商品类型必须选择关联商品");
|
||||
break;
|
||||
}
|
||||
case MemberPointMallRedeemType.Coupon:
|
||||
{
|
||||
couponTemplateId = request.CouponTemplateId.HasValue && request.CouponTemplateId.Value > 0
|
||||
? request.CouponTemplateId.Value
|
||||
: throw new BusinessException(ErrorCodes.BadRequest, "兑换优惠券类型必须选择关联优惠券");
|
||||
break;
|
||||
}
|
||||
case MemberPointMallRedeemType.Physical:
|
||||
{
|
||||
physicalName = MemberPointMallMapping.NormalizePhysicalName(request.PhysicalName);
|
||||
pickupMethod = MemberPointMallMapping.ParsePickupMethod(request.PickupMethod);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "redeemType 参数不合法");
|
||||
}
|
||||
}
|
||||
|
||||
MemberPointMallProduct entity;
|
||||
if (request.PointMallProductId.HasValue && request.PointMallProductId.Value > 0)
|
||||
{
|
||||
entity = await repository.FindProductByIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PointMallProductId.Value,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "兑换商品不存在");
|
||||
|
||||
var redeemedCount = Math.Max(0, entity.StockTotal - entity.StockAvailable);
|
||||
if (stockTotal < redeemedCount)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "库存总量不能小于已兑换数量");
|
||||
}
|
||||
|
||||
entity.Name = name;
|
||||
entity.ImageUrl = imageUrl;
|
||||
entity.RedeemType = redeemType;
|
||||
entity.ProductId = productId;
|
||||
entity.CouponTemplateId = couponTemplateId;
|
||||
entity.PhysicalName = physicalName;
|
||||
entity.PickupMethod = pickupMethod;
|
||||
entity.Description = description;
|
||||
entity.ExchangeType = exchangeType;
|
||||
entity.RequiredPoints = requiredPoints;
|
||||
entity.CashAmount = cashAmount;
|
||||
entity.StockTotal = stockTotal;
|
||||
entity.StockAvailable = stockTotal - redeemedCount;
|
||||
entity.PerMemberLimit = perMemberLimit;
|
||||
entity.NotifyChannelsJson = MemberPointMallMapping.SerializeNotifyChannels(notifyChannels);
|
||||
entity.Status = status;
|
||||
|
||||
await repository.UpdateProductAsync(entity, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = new MemberPointMallProduct
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
Name = name,
|
||||
ImageUrl = imageUrl,
|
||||
RedeemType = redeemType,
|
||||
ProductId = productId,
|
||||
CouponTemplateId = couponTemplateId,
|
||||
PhysicalName = physicalName,
|
||||
PickupMethod = pickupMethod,
|
||||
Description = description,
|
||||
ExchangeType = exchangeType,
|
||||
RequiredPoints = requiredPoints,
|
||||
CashAmount = cashAmount,
|
||||
StockTotal = stockTotal,
|
||||
StockAvailable = stockTotal,
|
||||
PerMemberLimit = perMemberLimit,
|
||||
NotifyChannelsJson = MemberPointMallMapping.SerializeNotifyChannels(notifyChannels),
|
||||
Status = status
|
||||
};
|
||||
|
||||
await repository.AddProductAsync(entity, cancellationToken);
|
||||
}
|
||||
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
var aggregates = await repository.GetProductAggregatesAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
[entity.Id],
|
||||
cancellationToken);
|
||||
|
||||
var aggregate = aggregates.TryGetValue(entity.Id, out var value)
|
||||
? value
|
||||
: MemberPointMallDtoFactory.EmptyProductAggregate(entity.Id);
|
||||
|
||||
return MemberPointMallDtoFactory.ToProductDto(entity, aggregate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 保存积分商城规则处理器。
|
||||
/// </summary>
|
||||
public sealed class SavePointMallRuleCommandHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<SavePointMallRuleCommand, MemberPointMallRuleDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallRuleDto> Handle(
|
||||
SavePointMallRuleCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var expiryMode = MemberPointMallMapping.ParseExpiryMode(request.ExpiryMode);
|
||||
var consumeAmountPerStep = request.IsConsumeRewardEnabled
|
||||
? MemberPointMallMapping.NormalizePositiveInt(request.ConsumeAmountPerStep, "consumeAmountPerStep")
|
||||
: Math.Max(1, request.ConsumeAmountPerStep);
|
||||
var consumeRewardPointsPerStep = request.IsConsumeRewardEnabled
|
||||
? MemberPointMallMapping.NormalizePositiveInt(request.ConsumeRewardPointsPerStep, "consumeRewardPointsPerStep")
|
||||
: Math.Max(0, request.ConsumeRewardPointsPerStep);
|
||||
var reviewRewardPoints = request.IsReviewRewardEnabled
|
||||
? MemberPointMallMapping.NormalizePositiveInt(request.ReviewRewardPoints, "reviewRewardPoints")
|
||||
: Math.Max(0, request.ReviewRewardPoints);
|
||||
var registerRewardPoints = request.IsRegisterRewardEnabled
|
||||
? MemberPointMallMapping.NormalizePositiveInt(request.RegisterRewardPoints, "registerRewardPoints")
|
||||
: Math.Max(0, request.RegisterRewardPoints);
|
||||
var signinRewardPoints = request.IsSigninRewardEnabled
|
||||
? MemberPointMallMapping.NormalizePositiveInt(request.SigninRewardPoints, "signinRewardPoints")
|
||||
: Math.Max(0, request.SigninRewardPoints);
|
||||
|
||||
var existing = await repository.GetRuleByStoreAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
cancellationToken);
|
||||
|
||||
if (existing is null)
|
||||
{
|
||||
var created = MemberPointMallDtoFactory.CreateRuleEntity(request, expiryMode);
|
||||
created.ConsumeAmountPerStep = consumeAmountPerStep;
|
||||
created.ConsumeRewardPointsPerStep = consumeRewardPointsPerStep;
|
||||
created.ReviewRewardPoints = reviewRewardPoints;
|
||||
created.RegisterRewardPoints = registerRewardPoints;
|
||||
created.SigninRewardPoints = signinRewardPoints;
|
||||
await repository.AddRuleAsync(created, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return MemberPointMallDtoFactory.ToRuleDto(created);
|
||||
}
|
||||
|
||||
existing.IsConsumeRewardEnabled = request.IsConsumeRewardEnabled;
|
||||
existing.ConsumeAmountPerStep = consumeAmountPerStep;
|
||||
existing.ConsumeRewardPointsPerStep = consumeRewardPointsPerStep;
|
||||
existing.IsReviewRewardEnabled = request.IsReviewRewardEnabled;
|
||||
existing.ReviewRewardPoints = reviewRewardPoints;
|
||||
existing.IsRegisterRewardEnabled = request.IsRegisterRewardEnabled;
|
||||
existing.RegisterRewardPoints = registerRewardPoints;
|
||||
existing.IsSigninRewardEnabled = request.IsSigninRewardEnabled;
|
||||
existing.SigninRewardPoints = signinRewardPoints;
|
||||
existing.ExpiryMode = expiryMode;
|
||||
|
||||
await repository.UpdateRuleAsync(existing, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return MemberPointMallDtoFactory.ToRuleDto(existing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 核销积分商城兑换记录处理器。
|
||||
/// </summary>
|
||||
public sealed class VerifyPointMallRecordCommandHandler(
|
||||
IPointMallRepository repository,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUserAccessor)
|
||||
: IRequestHandler<VerifyPointMallRecordCommand, MemberPointMallRecordDetailDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallRecordDetailDto> Handle(
|
||||
VerifyPointMallRecordCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var verifyMethod = MemberPointMallMapping.ParseVerifyMethod(request.VerifyMethod);
|
||||
var verifyRemark = MemberPointMallMapping.NormalizeRemark(request.VerifyRemark, "verifyRemark");
|
||||
|
||||
var record = await repository.FindRecordByIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.RecordId,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "兑换记录不存在");
|
||||
|
||||
if (record.Status != MemberPointMallRecordStatus.PendingPickup)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "当前状态不可核销");
|
||||
}
|
||||
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
record.Status = MemberPointMallRecordStatus.Completed;
|
||||
record.IssuedAt ??= nowUtc;
|
||||
record.VerifiedAt = nowUtc;
|
||||
record.VerifyMethod = verifyMethod;
|
||||
record.VerifyRemark = verifyRemark;
|
||||
record.VerifiedBy = currentUserAccessor.IsAuthenticated && currentUserAccessor.UserId > 0
|
||||
? currentUserAccessor.UserId
|
||||
: null;
|
||||
|
||||
await repository.UpdateRecordAsync(record, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return MemberPointMallDtoFactory.ToRecordDetailDto(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Domain.Membership.Entities;
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 写入积分商城兑换记录处理器。
|
||||
/// </summary>
|
||||
public sealed class WritePointMallRecordCommandHandler(
|
||||
IPointMallRepository repository,
|
||||
IMemberRepository memberRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
: IRequestHandler<WritePointMallRecordCommand, MemberPointMallRecordDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberPointMallRecordDto> Handle(
|
||||
WritePointMallRecordCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var redeemedAt = request.RedeemedAt.HasValue
|
||||
? MemberPointMallMapping.NormalizeUtc(request.RedeemedAt.Value)
|
||||
: DateTime.UtcNow;
|
||||
|
||||
var product = await repository.FindProductByIdAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
request.PointMallProductId,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "兑换商品不存在");
|
||||
|
||||
if (product.Status != MemberPointMallProductStatus.Enabled)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "兑换商品未上架");
|
||||
}
|
||||
|
||||
if (product.StockAvailable <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "兑换商品库存不足");
|
||||
}
|
||||
|
||||
var member = await memberRepository.FindProfileByIdAsync(
|
||||
tenantId,
|
||||
request.MemberId,
|
||||
cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "会员不存在");
|
||||
|
||||
var usedPoints = MemberPointMallMapping.NormalizeRequiredPoints(product.RequiredPoints);
|
||||
if (member.PointsBalance < usedPoints)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "会员积分不足");
|
||||
}
|
||||
|
||||
if (product.PerMemberLimit.HasValue && product.PerMemberLimit.Value > 0)
|
||||
{
|
||||
var redeemedCount = await repository.CountMemberRedeemsByProductAsync(
|
||||
tenantId,
|
||||
request.StoreId,
|
||||
product.Id,
|
||||
member.Id,
|
||||
cancellationToken);
|
||||
|
||||
if (redeemedCount >= product.PerMemberLimit.Value)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "已达到每人限兑次数");
|
||||
}
|
||||
}
|
||||
|
||||
member.PointsBalance -= usedPoints;
|
||||
await memberRepository.UpdateProfileAsync(member, cancellationToken);
|
||||
|
||||
product.StockAvailable -= 1;
|
||||
await repository.UpdateProductAsync(product, cancellationToken);
|
||||
|
||||
var initialStatus = MemberPointMallMapping.ResolveRecordInitialStatus(product.RedeemType);
|
||||
var record = new MemberPointMallRecord
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
RecordNo = MemberPointMallMapping.BuildRecordNo(redeemedAt),
|
||||
PointMallProductId = product.Id,
|
||||
MemberId = member.Id,
|
||||
MemberName = MemberPointMallMapping.ResolveMemberName(member),
|
||||
MemberMobileMasked = MemberPointMallMapping.ResolveMemberMobileMasked(member),
|
||||
ProductName = product.Name,
|
||||
RedeemType = product.RedeemType,
|
||||
ExchangeType = product.ExchangeType,
|
||||
UsedPoints = usedPoints,
|
||||
CashAmount = product.CashAmount,
|
||||
Status = initialStatus,
|
||||
RedeemedAt = redeemedAt,
|
||||
IssuedAt = MemberPointMallMapping.ResolveRecordInitialIssuedAt(product.RedeemType, redeemedAt)
|
||||
};
|
||||
|
||||
await repository.AddRecordAsync(record, cancellationToken);
|
||||
|
||||
var ledger = new MemberPointLedger
|
||||
{
|
||||
MemberId = member.Id,
|
||||
ChangeAmount = -usedPoints,
|
||||
BalanceAfterChange = member.PointsBalance,
|
||||
Reason = PointChangeReason.Redeem,
|
||||
SourceId = product.Id,
|
||||
OccurredAt = redeemedAt
|
||||
};
|
||||
|
||||
await repository.AddPointLedgerAsync(ledger, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return MemberPointMallDtoFactory.ToRecordDto(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Commands;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
using TakeoutSaaS.Domain.Membership.Entities;
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城 DTO 构造器。
|
||||
/// </summary>
|
||||
internal static class MemberPointMallDtoFactory
|
||||
{
|
||||
public static MemberPointMallProductAggregateSnapshot EmptyProductAggregate(long pointMallProductId)
|
||||
{
|
||||
return new MemberPointMallProductAggregateSnapshot
|
||||
{
|
||||
PointMallProductId = pointMallProductId,
|
||||
RedeemedCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRuleDto ToRuleDto(MemberPointMallRule source)
|
||||
{
|
||||
return new MemberPointMallRuleDto
|
||||
{
|
||||
StoreId = source.StoreId,
|
||||
IsConsumeRewardEnabled = source.IsConsumeRewardEnabled,
|
||||
ConsumeAmountPerStep = source.ConsumeAmountPerStep,
|
||||
ConsumeRewardPointsPerStep = source.ConsumeRewardPointsPerStep,
|
||||
IsReviewRewardEnabled = source.IsReviewRewardEnabled,
|
||||
ReviewRewardPoints = source.ReviewRewardPoints,
|
||||
IsRegisterRewardEnabled = source.IsRegisterRewardEnabled,
|
||||
RegisterRewardPoints = source.RegisterRewardPoints,
|
||||
IsSigninRewardEnabled = source.IsSigninRewardEnabled,
|
||||
SigninRewardPoints = source.SigninRewardPoints,
|
||||
ExpiryMode = MemberPointMallMapping.ToExpiryModeText(source.ExpiryMode)
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRuleStatsDto ToRuleStatsDto(MemberPointMallRuleStatsSnapshot source)
|
||||
{
|
||||
return new MemberPointMallRuleStatsDto
|
||||
{
|
||||
TotalIssuedPoints = source.TotalIssuedPoints,
|
||||
RedeemedPoints = source.RedeemedPoints,
|
||||
PointMembers = source.PointMembers,
|
||||
RedeemRate = decimal.Round(source.RedeemRate, 1, MidpointRounding.AwayFromZero)
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallProductDto ToProductDto(
|
||||
MemberPointMallProduct source,
|
||||
MemberPointMallProductAggregateSnapshot aggregate)
|
||||
{
|
||||
var notifyChannels = MemberPointMallMapping.DeserializeNotifyChannels(source.NotifyChannelsJson)
|
||||
.Select(MemberPointMallMapping.ToNotifyChannelText)
|
||||
.ToList();
|
||||
|
||||
return new MemberPointMallProductDto
|
||||
{
|
||||
PointMallProductId = source.Id,
|
||||
StoreId = source.StoreId,
|
||||
Name = source.Name,
|
||||
ImageUrl = source.ImageUrl,
|
||||
RedeemType = MemberPointMallMapping.ToRedeemTypeText(source.RedeemType),
|
||||
ProductId = source.ProductId,
|
||||
CouponTemplateId = source.CouponTemplateId,
|
||||
PhysicalName = source.PhysicalName,
|
||||
PickupMethod = source.PickupMethod.HasValue
|
||||
? MemberPointMallMapping.ToPickupMethodText(source.PickupMethod.Value)
|
||||
: null,
|
||||
Description = source.Description,
|
||||
ExchangeType = MemberPointMallMapping.ToExchangeTypeText(source.ExchangeType),
|
||||
RequiredPoints = source.RequiredPoints,
|
||||
CashAmount = decimal.Round(source.CashAmount, 2, MidpointRounding.AwayFromZero),
|
||||
StockTotal = source.StockTotal,
|
||||
StockAvailable = source.StockAvailable,
|
||||
RedeemedCount = aggregate.RedeemedCount,
|
||||
PerMemberLimit = source.PerMemberLimit,
|
||||
NotifyChannels = notifyChannels,
|
||||
Status = MemberPointMallMapping.ToProductStatusText(source.Status),
|
||||
UpdatedAt = source.UpdatedAt ?? source.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRecordDto ToRecordDto(MemberPointMallRecord source)
|
||||
{
|
||||
return new MemberPointMallRecordDto
|
||||
{
|
||||
RecordId = source.Id,
|
||||
RecordNo = source.RecordNo,
|
||||
PointMallProductId = source.PointMallProductId,
|
||||
ProductName = source.ProductName,
|
||||
RedeemType = MemberPointMallMapping.ToRedeemTypeText(source.RedeemType),
|
||||
ExchangeType = MemberPointMallMapping.ToExchangeTypeText(source.ExchangeType),
|
||||
MemberId = source.MemberId,
|
||||
MemberName = source.MemberName,
|
||||
MemberMobileMasked = source.MemberMobileMasked,
|
||||
UsedPoints = source.UsedPoints,
|
||||
CashAmount = decimal.Round(source.CashAmount, 2, MidpointRounding.AwayFromZero),
|
||||
Status = MemberPointMallMapping.ToRecordStatusText(source.Status),
|
||||
RedeemedAt = source.RedeemedAt,
|
||||
IssuedAt = source.IssuedAt,
|
||||
VerifiedAt = source.VerifiedAt
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRecordDetailDto ToRecordDetailDto(MemberPointMallRecord source)
|
||||
{
|
||||
return new MemberPointMallRecordDetailDto
|
||||
{
|
||||
RecordId = source.Id,
|
||||
RecordNo = source.RecordNo,
|
||||
PointMallProductId = source.PointMallProductId,
|
||||
ProductName = source.ProductName,
|
||||
RedeemType = MemberPointMallMapping.ToRedeemTypeText(source.RedeemType),
|
||||
ExchangeType = MemberPointMallMapping.ToExchangeTypeText(source.ExchangeType),
|
||||
MemberId = source.MemberId,
|
||||
MemberName = source.MemberName,
|
||||
MemberMobileMasked = source.MemberMobileMasked,
|
||||
UsedPoints = source.UsedPoints,
|
||||
CashAmount = decimal.Round(source.CashAmount, 2, MidpointRounding.AwayFromZero),
|
||||
Status = MemberPointMallMapping.ToRecordStatusText(source.Status),
|
||||
RedeemedAt = source.RedeemedAt,
|
||||
IssuedAt = source.IssuedAt,
|
||||
VerifiedAt = source.VerifiedAt,
|
||||
VerifyMethod = source.VerifyMethod.HasValue
|
||||
? MemberPointMallMapping.ToVerifyMethodText(source.VerifyMethod.Value)
|
||||
: null,
|
||||
VerifyRemark = source.VerifyRemark,
|
||||
VerifiedBy = source.VerifiedBy
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRecordStatsDto ToRecordStatsDto(MemberPointMallRecordStatsSnapshot source)
|
||||
{
|
||||
return new MemberPointMallRecordStatsDto
|
||||
{
|
||||
TodayRedeemCount = source.TodayRedeemCount,
|
||||
PendingPhysicalCount = source.PendingPhysicalCount,
|
||||
CurrentMonthUsedPoints = source.CurrentMonthUsedPoints
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRule CreateRuleEntity(
|
||||
SavePointMallRuleCommand request,
|
||||
MemberPointMallExpiryMode expiryMode)
|
||||
{
|
||||
return new MemberPointMallRule
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
IsConsumeRewardEnabled = request.IsConsumeRewardEnabled,
|
||||
ConsumeAmountPerStep = request.ConsumeAmountPerStep,
|
||||
ConsumeRewardPointsPerStep = request.ConsumeRewardPointsPerStep,
|
||||
IsReviewRewardEnabled = request.IsReviewRewardEnabled,
|
||||
ReviewRewardPoints = request.ReviewRewardPoints,
|
||||
IsRegisterRewardEnabled = request.IsRegisterRewardEnabled,
|
||||
RegisterRewardPoints = request.RegisterRewardPoints,
|
||||
IsSigninRewardEnabled = request.IsSigninRewardEnabled,
|
||||
SigninRewardPoints = request.SigninRewardPoints,
|
||||
ExpiryMode = expiryMode
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallProduct CreateProductEntity(
|
||||
SavePointMallProductCommand request,
|
||||
MemberPointMallRedeemType redeemType,
|
||||
MemberPointMallExchangeType exchangeType,
|
||||
MemberPointMallProductStatus status,
|
||||
string name,
|
||||
string? imageUrl,
|
||||
string? physicalName,
|
||||
MemberPointMallPickupMethod? pickupMethod,
|
||||
string? description,
|
||||
int requiredPoints,
|
||||
decimal cashAmount,
|
||||
int stockTotal,
|
||||
int? perMemberLimit,
|
||||
IReadOnlyCollection<MemberPointMallNotifyChannel> notifyChannels)
|
||||
{
|
||||
return new MemberPointMallProduct
|
||||
{
|
||||
StoreId = request.StoreId,
|
||||
Name = name,
|
||||
ImageUrl = imageUrl,
|
||||
RedeemType = redeemType,
|
||||
ProductId = request.ProductId,
|
||||
CouponTemplateId = request.CouponTemplateId,
|
||||
PhysicalName = physicalName,
|
||||
PickupMethod = pickupMethod,
|
||||
Description = description,
|
||||
ExchangeType = exchangeType,
|
||||
RequiredPoints = requiredPoints,
|
||||
CashAmount = cashAmount,
|
||||
StockTotal = stockTotal,
|
||||
StockAvailable = stockTotal,
|
||||
PerMemberLimit = perMemberLimit,
|
||||
NotifyChannelsJson = MemberPointMallMapping.SerializeNotifyChannels(notifyChannels),
|
||||
Status = status
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Domain.Membership.Entities;
|
||||
using TakeoutSaaS.Domain.Membership.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall;
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城模块映射与标准化工具。
|
||||
/// </summary>
|
||||
internal static class MemberPointMallMapping
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public static MemberPointMallExpiryMode ParseExpiryMode(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"permanent" => MemberPointMallExpiryMode.Permanent,
|
||||
"yearly_clear" => MemberPointMallExpiryMode.YearlyClear,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "expiryMode 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToExpiryModeText(MemberPointMallExpiryMode value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallExpiryMode.Permanent => "permanent",
|
||||
MemberPointMallExpiryMode.YearlyClear => "yearly_clear",
|
||||
_ => "yearly_clear"
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRedeemType ParseRedeemType(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"product" => MemberPointMallRedeemType.Product,
|
||||
"coupon" => MemberPointMallRedeemType.Coupon,
|
||||
"physical" => MemberPointMallRedeemType.Physical,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "redeemType 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRedeemType? TryParseRedeemType(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ParseRedeemType(value);
|
||||
}
|
||||
|
||||
public static string ToRedeemTypeText(MemberPointMallRedeemType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallRedeemType.Product => "product",
|
||||
MemberPointMallRedeemType.Coupon => "coupon",
|
||||
MemberPointMallRedeemType.Physical => "physical",
|
||||
_ => "product"
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToRedeemTypeDisplayText(MemberPointMallRedeemType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallRedeemType.Product => "商品",
|
||||
MemberPointMallRedeemType.Coupon => "优惠券",
|
||||
MemberPointMallRedeemType.Physical => "实物",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallExchangeType ParseExchangeType(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"points" => MemberPointMallExchangeType.PointsOnly,
|
||||
"mixed" => MemberPointMallExchangeType.PointsAndCash,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "exchangeType 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToExchangeTypeText(MemberPointMallExchangeType value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallExchangeType.PointsOnly => "points",
|
||||
MemberPointMallExchangeType.PointsAndCash => "mixed",
|
||||
_ => "points"
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallProductStatus ParseProductStatus(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"enabled" => MemberPointMallProductStatus.Enabled,
|
||||
"disabled" => MemberPointMallProductStatus.Disabled,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallProductStatus? TryParseProductStatus(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ParseProductStatus(value);
|
||||
}
|
||||
|
||||
public static string ToProductStatusText(MemberPointMallProductStatus value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallProductStatus.Enabled => "enabled",
|
||||
MemberPointMallProductStatus.Disabled => "disabled",
|
||||
_ => "disabled"
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallPickupMethod ParsePickupMethod(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"store_pickup" => MemberPointMallPickupMethod.StorePickup,
|
||||
"delivery" => MemberPointMallPickupMethod.Delivery,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "pickupMethod 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToPickupMethodText(MemberPointMallPickupMethod value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallPickupMethod.StorePickup => "store_pickup",
|
||||
MemberPointMallPickupMethod.Delivery => "delivery",
|
||||
_ => "store_pickup"
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallVerifyMethod ParseVerifyMethod(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"scan" => MemberPointMallVerifyMethod.Scan,
|
||||
"manual" => MemberPointMallVerifyMethod.Manual,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "verifyMethod 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToVerifyMethodText(MemberPointMallVerifyMethod value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallVerifyMethod.Scan => "scan",
|
||||
MemberPointMallVerifyMethod.Manual => "manual",
|
||||
_ => "manual"
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToVerifyMethodDisplayText(MemberPointMallVerifyMethod value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallVerifyMethod.Scan => "扫码核销",
|
||||
MemberPointMallVerifyMethod.Manual => "手动核销",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRecordStatus ParseRecordStatus(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"pending_pickup" => MemberPointMallRecordStatus.PendingPickup,
|
||||
"issued" => MemberPointMallRecordStatus.Issued,
|
||||
"completed" => MemberPointMallRecordStatus.Completed,
|
||||
"canceled" => MemberPointMallRecordStatus.Canceled,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallRecordStatus? TryParseRecordStatus(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ParseRecordStatus(value);
|
||||
}
|
||||
|
||||
public static string ToRecordStatusText(MemberPointMallRecordStatus value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallRecordStatus.PendingPickup => "pending_pickup",
|
||||
MemberPointMallRecordStatus.Issued => "issued",
|
||||
MemberPointMallRecordStatus.Completed => "completed",
|
||||
MemberPointMallRecordStatus.Canceled => "canceled",
|
||||
_ => "issued"
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToRecordStatusDisplayText(MemberPointMallRecordStatus value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallRecordStatus.PendingPickup => "待领取",
|
||||
MemberPointMallRecordStatus.Issued => "已发放",
|
||||
MemberPointMallRecordStatus.Completed => "已完成",
|
||||
MemberPointMallRecordStatus.Canceled => "已取消",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
|
||||
public static MemberPointMallNotifyChannel ParseNotifyChannel(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"in_app" => MemberPointMallNotifyChannel.InApp,
|
||||
"sms" => MemberPointMallNotifyChannel.Sms,
|
||||
_ => throw new BusinessException(ErrorCodes.BadRequest, "notifyChannels 参数不合法")
|
||||
};
|
||||
}
|
||||
|
||||
public static string ToNotifyChannelText(MemberPointMallNotifyChannel value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MemberPointMallNotifyChannel.InApp => "in_app",
|
||||
MemberPointMallNotifyChannel.Sms => "sms",
|
||||
_ => "in_app"
|
||||
};
|
||||
}
|
||||
|
||||
public static IReadOnlyList<MemberPointMallNotifyChannel> ParseNotifyChannels(
|
||||
IReadOnlyCollection<string>? values)
|
||||
{
|
||||
var parsed = (values ?? Array.Empty<string>())
|
||||
.Select(ParseNotifyChannel)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (parsed.Count == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "notifyChannels 至少选择一项");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
public static IReadOnlyList<MemberPointMallNotifyChannel> DeserializeNotifyChannels(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var source = JsonSerializer.Deserialize<List<string>>(value, JsonOptions) ?? [];
|
||||
var channels = source
|
||||
.Select(item =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return (MemberPointMallNotifyChannel?)ParseNotifyChannel(item);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(item => item.HasValue)
|
||||
.Select(item => item!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return channels;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public static string SerializeNotifyChannels(IReadOnlyCollection<MemberPointMallNotifyChannel> values)
|
||||
{
|
||||
var payload = (values ?? Array.Empty<MemberPointMallNotifyChannel>())
|
||||
.Distinct()
|
||||
.OrderBy(item => item)
|
||||
.Select(ToNotifyChannelText)
|
||||
.ToList();
|
||||
|
||||
return JsonSerializer.Serialize(payload, JsonOptions);
|
||||
}
|
||||
|
||||
public static string NormalizeName(string? value, string fieldName = "name")
|
||||
{
|
||||
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? NormalizePhysicalName(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "physicalName 不能为空");
|
||||
}
|
||||
|
||||
if (normalized.Length > 64)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "physicalName 长度不能超过 64");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static string? NormalizeImageUrl(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized.Length > 512)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "imageUrl 长度不能超过 512");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static string? NormalizeDescription(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized.Length > 512)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "description 长度不能超过 512");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static string? NormalizeKeyword(string? value)
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized.Length > 64)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "keyword 长度不能超过 64");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static string? NormalizeRemark(string? value, string fieldName = "remark")
|
||||
{
|
||||
var normalized = (value ?? string.Empty).Trim();
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized.Length > 256)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 长度不能超过 256");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static int NormalizePositiveInt(int value, string fieldName)
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, $"{fieldName} 必须大于 0");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static int NormalizeRequiredPoints(int value)
|
||||
{
|
||||
if (value <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "requiredPoints 必须大于 0");
|
||||
}
|
||||
|
||||
if (value > 1_000_000)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "requiredPoints 不能超过 1000000");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static int NormalizeStockTotal(int value)
|
||||
{
|
||||
if (value < 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "stockTotal 不能小于 0");
|
||||
}
|
||||
|
||||
if (value > 10_000_000)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "stockTotal 不能超过 10000000");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static int? NormalizePerMemberLimit(int? value)
|
||||
{
|
||||
if (!value.HasValue || value.Value <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.Value > 9999)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "perMemberLimit 不能超过 9999");
|
||||
}
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
public static decimal NormalizeCashAmount(decimal value, MemberPointMallExchangeType exchangeType)
|
||||
{
|
||||
var normalized = decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||
if (exchangeType == MemberPointMallExchangeType.PointsOnly)
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
|
||||
if (normalized <= 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "cashAmount 必须大于 0");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
public static (DateTime? StartUtc, DateTime? EndUtc) NormalizeDateRange(DateTime? startUtc, DateTime? endUtc)
|
||||
{
|
||||
DateTime? normalizedStart = null;
|
||||
DateTime? normalizedEnd = null;
|
||||
|
||||
if (startUtc.HasValue)
|
||||
{
|
||||
var utc = NormalizeUtc(startUtc.Value);
|
||||
normalizedStart = new DateTime(utc.Year, utc.Month, utc.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (endUtc.HasValue)
|
||||
{
|
||||
var utc = NormalizeUtc(endUtc.Value);
|
||||
normalizedEnd = new DateTime(utc.Year, utc.Month, utc.Day, 0, 0, 0, DateTimeKind.Utc)
|
||||
.AddDays(1)
|
||||
.AddTicks(-1);
|
||||
}
|
||||
|
||||
if (normalizedStart.HasValue && normalizedEnd.HasValue && normalizedStart.Value > normalizedEnd.Value)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "开始日期不能晚于结束日期");
|
||||
}
|
||||
|
||||
return (normalizedStart, normalizedEnd);
|
||||
}
|
||||
|
||||
public static DateTime NormalizeUtc(DateTime value)
|
||||
{
|
||||
return value.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => value,
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||
};
|
||||
}
|
||||
|
||||
public static string ResolveMemberName(MemberProfile member)
|
||||
{
|
||||
var nickname = (member.Nickname ?? string.Empty).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(nickname))
|
||||
{
|
||||
return nickname.Length <= 64 ? nickname : nickname[..64];
|
||||
}
|
||||
|
||||
var mobile = NormalizePhone(member.Mobile);
|
||||
return mobile.Length >= 4 ? $"会员{mobile[^4..]}" : "会员";
|
||||
}
|
||||
|
||||
public static string ResolveMemberMobileMasked(MemberProfile member)
|
||||
{
|
||||
return MaskPhone(NormalizePhone(member.Mobile));
|
||||
}
|
||||
|
||||
public static string BuildRecordNo(DateTime nowUtc)
|
||||
{
|
||||
var utcNow = NormalizeUtc(nowUtc);
|
||||
return $"PT{utcNow:yyyyMMddHHmmssfff}{Random.Shared.Next(1000, 9999)}";
|
||||
}
|
||||
|
||||
public static MemberPointMallRecordStatus ResolveRecordInitialStatus(MemberPointMallRedeemType redeemType)
|
||||
{
|
||||
return redeemType == MemberPointMallRedeemType.Physical
|
||||
? MemberPointMallRecordStatus.PendingPickup
|
||||
: MemberPointMallRecordStatus.Issued;
|
||||
}
|
||||
|
||||
public static DateTime? ResolveRecordInitialIssuedAt(MemberPointMallRedeemType redeemType, DateTime redeemedAt)
|
||||
{
|
||||
return redeemType == MemberPointMallRedeemType.Physical ? null : redeemedAt;
|
||||
}
|
||||
|
||||
private static string NormalizePhone(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var chars = value.Where(char.IsDigit).ToArray();
|
||||
return chars.Length == 0 ? string.Empty : new string(chars);
|
||||
}
|
||||
|
||||
private static string MaskPhone(string normalizedPhone)
|
||||
{
|
||||
if (normalizedPhone.Length >= 11)
|
||||
{
|
||||
return $"{normalizedPhone[..3]}****{normalizedPhone[^4..]}";
|
||||
}
|
||||
|
||||
if (normalizedPhone.Length >= 7)
|
||||
{
|
||||
return $"{normalizedPhone[..3]}***{normalizedPhone[^2..]}";
|
||||
}
|
||||
|
||||
return normalizedPhone;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 导出积分商城兑换记录 CSV。
|
||||
/// </summary>
|
||||
public sealed class ExportPointMallRecordCsvQuery : IRequest<MemberPointMallRecordExportDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换类型(product/coupon/physical)。
|
||||
/// </summary>
|
||||
public string? RedeemType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(pending_pickup/issued/completed/canceled)。
|
||||
/// </summary>
|
||||
public string? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期(UTC,可空)。
|
||||
/// </summary>
|
||||
public DateTime? StartDateUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(UTC,可空)。
|
||||
/// </summary>
|
||||
public DateTime? EndDateUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城商品详情。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallProductDetailQuery : IRequest<MemberPointMallProductDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 积分商城商品标识。
|
||||
/// </summary>
|
||||
public long PointMallProductId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城商品列表。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallProductListQuery : IRequest<MemberPointMallProductListResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态筛选(enabled/disabled)。
|
||||
/// </summary>
|
||||
public string? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字(名称)。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城兑换记录详情。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallRecordDetailQuery : IRequest<MemberPointMallRecordDetailDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换记录标识。
|
||||
/// </summary>
|
||||
public long RecordId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城兑换记录分页。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallRecordListQuery : IRequest<MemberPointMallRecordListResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 兑换类型(product/coupon/physical)。
|
||||
/// </summary>
|
||||
public string? RedeemType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(pending_pickup/issued/completed/canceled)。
|
||||
/// </summary>
|
||||
public string? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始日期(UTC,可空)。
|
||||
/// </summary>
|
||||
public DateTime? StartDateUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 结束日期(UTC,可空)。
|
||||
/// </summary>
|
||||
public DateTime? EndDateUtc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 关键字。
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
public int Page { get; init; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; init; } = 10;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
using TakeoutSaaS.Application.App.Members.PointsMall.Dto;
|
||||
|
||||
namespace TakeoutSaaS.Application.App.Members.PointsMall.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// 查询积分商城规则详情。
|
||||
/// </summary>
|
||||
public sealed class GetPointMallRuleDetailQuery : IRequest<MemberPointMallRuleDetailResultDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// 门店标识。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user