feat: implement tenant member stored card module
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 2m24s
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 2m24s
This commit is contained in:
Submodule TakeoutSaaS.Docs updated: 9006c8a589...6680599912
@@ -0,0 +1,399 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Member;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案列表请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoredCardPlanListRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存储值卡方案请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoredCardPlanRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案 ID(编辑时传)。
|
||||||
|
/// </summary>
|
||||||
|
public string? PlanId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序值。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "enabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改方案状态请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChangeStoredCardPlanStatusRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PlanId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除方案请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteStoredCardPlanRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PlanId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值记录分页查询请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoredCardRechargeRecordListRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? StartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? EndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(会员名称/手机号/单号)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值记录导出请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportStoredCardRechargeRecordRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? StartDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string? EndDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关键字(会员名称/手机号/单号)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Keyword { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入充值记录请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteStoredCardRechargeRecordRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员 ID(必填)。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案 ID(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public string? PlanId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式(wechat/alipay/cash/card/balance)。
|
||||||
|
/// </summary>
|
||||||
|
public string PaymentMethod { get; set; } = "wechat";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值时间(可空,默认当前时间)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RechargedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案统计响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoredCardPlanStatsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 储值总额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalRechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠金总额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalGiftAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月充值。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CurrentMonthRechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值用户。
|
||||||
|
/// </summary>
|
||||||
|
public int RechargeMemberCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoredCardPlanResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 方案 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string PlanId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; } = "enabled";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计充值次数。
|
||||||
|
/// </summary>
|
||||||
|
public int RechargeCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalRechargeAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案列表响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoredCardPlanListResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 方案列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<StoredCardPlanResponse> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页面统计。
|
||||||
|
/// </summary>
|
||||||
|
public StoredCardPlanStatsResponse Stats { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值记录响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoredCardRechargeRecordResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string RecordId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值单号。
|
||||||
|
/// </summary>
|
||||||
|
public string RecordNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员名称。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberMobileMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式编码。
|
||||||
|
/// </summary>
|
||||||
|
public string PaymentMethod { get; set; } = "unknown";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式文案。
|
||||||
|
/// </summary>
|
||||||
|
public string PaymentMethodText { get; set; } = "未知";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值时间(本地显示字符串)。
|
||||||
|
/// </summary>
|
||||||
|
public string RechargedAt { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string? PlanId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值记录分页结果响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class StoredCardRechargeRecordListResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public List<StoredCardRechargeRecordResponse> 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 StoredCardRechargeRecordExportResponse
|
||||||
|
{
|
||||||
|
/// <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,283 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.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.Member;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.TenantApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员中心储值卡管理。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/tenant/v{version:apiVersion}/member/stored-card")]
|
||||||
|
public sealed class MemberStoredCardController(
|
||||||
|
IMediator mediator,
|
||||||
|
TakeoutAppDbContext dbContext,
|
||||||
|
StoreContextService storeContextService)
|
||||||
|
: BaseApiController
|
||||||
|
{
|
||||||
|
private const string ViewPermission = "tenant:member:stored-card:view";
|
||||||
|
private const string ManagePermission = "tenant:member:stored-card:manage";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取储值卡方案列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("plan/list")]
|
||||||
|
[PermissionAuthorize(ViewPermission, ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoredCardPlanListResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoredCardPlanListResultResponse>> PlanList(
|
||||||
|
[FromQuery] StoredCardPlanListRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new GetStoredCardPlanListQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoredCardPlanListResultResponse>.Ok(new StoredCardPlanListResultResponse
|
||||||
|
{
|
||||||
|
Items = result.Items.Select(MapPlan).ToList(),
|
||||||
|
Stats = new StoredCardPlanStatsResponse
|
||||||
|
{
|
||||||
|
TotalRechargeAmount = result.Stats.TotalRechargeAmount,
|
||||||
|
TotalGiftAmount = result.Stats.TotalGiftAmount,
|
||||||
|
CurrentMonthRechargeAmount = result.Stats.CurrentMonthRechargeAmount,
|
||||||
|
RechargeMemberCount = result.Stats.RechargeMemberCount
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存储值卡方案。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("plan/save")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoredCardPlanResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoredCardPlanResponse>> SavePlan(
|
||||||
|
[FromBody] SaveStoredCardPlanRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new SaveStoredCardPlanCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
PlanId = StoreApiHelpers.ParseSnowflakeOrNull(request.PlanId),
|
||||||
|
RechargeAmount = request.RechargeAmount,
|
||||||
|
GiftAmount = request.GiftAmount,
|
||||||
|
SortOrder = request.SortOrder,
|
||||||
|
Status = request.Status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoredCardPlanResponse>.Ok(MapPlan(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改储值卡方案状态。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("plan/status")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoredCardPlanResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoredCardPlanResponse>> ChangePlanStatus(
|
||||||
|
[FromBody] ChangeStoredCardPlanStatusRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new ChangeStoredCardPlanStatusCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
PlanId = StoreApiHelpers.ParseRequiredSnowflake(request.PlanId, nameof(request.PlanId)),
|
||||||
|
Status = request.Status
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoredCardPlanResponse>.Ok(MapPlan(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除储值卡方案。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("plan/delete")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<object>> DeletePlan(
|
||||||
|
[FromBody] DeleteStoredCardPlanRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
await mediator.Send(new DeleteStoredCardPlanCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
PlanId = StoreApiHelpers.ParseRequiredSnowflake(request.PlanId, nameof(request.PlanId))
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<object>.Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取充值记录分页。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("record/list")]
|
||||||
|
[PermissionAuthorize(ViewPermission, ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoredCardRechargeRecordListResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoredCardRechargeRecordListResultResponse>> RecordList(
|
||||||
|
[FromQuery] StoredCardRechargeRecordListRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new GetStoredCardRechargeRecordListQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
StartDateUtc = ParseDateOrNull(request.StartDate, nameof(request.StartDate)),
|
||||||
|
EndDateUtc = ParseDateOrNull(request.EndDate, nameof(request.EndDate)),
|
||||||
|
Keyword = request.Keyword,
|
||||||
|
Page = request.Page,
|
||||||
|
PageSize = request.PageSize
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoredCardRechargeRecordListResultResponse>.Ok(new StoredCardRechargeRecordListResultResponse
|
||||||
|
{
|
||||||
|
Items = result.Items.Select(MapRecord).ToList(),
|
||||||
|
Page = result.Page,
|
||||||
|
PageSize = result.PageSize,
|
||||||
|
TotalCount = result.TotalCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出充值记录 CSV。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("record/export")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoredCardRechargeRecordExportResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoredCardRechargeRecordExportResponse>> ExportRecord(
|
||||||
|
[FromQuery] ExportStoredCardRechargeRecordRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new ExportStoredCardRechargeRecordCsvQuery
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
StartDateUtc = ParseDateOrNull(request.StartDate, nameof(request.StartDate)),
|
||||||
|
EndDateUtc = ParseDateOrNull(request.EndDate, nameof(request.EndDate)),
|
||||||
|
Keyword = request.Keyword
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoredCardRechargeRecordExportResponse>.Ok(new StoredCardRechargeRecordExportResponse
|
||||||
|
{
|
||||||
|
FileName = result.FileName,
|
||||||
|
FileContentBase64 = result.FileContentBase64,
|
||||||
|
TotalCount = result.TotalCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入充值记录。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("record/write")]
|
||||||
|
[PermissionAuthorize(ManagePermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<StoredCardRechargeRecordResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<StoredCardRechargeRecordResponse>> WriteRecord(
|
||||||
|
[FromBody] WriteStoredCardRechargeRecordRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
|
||||||
|
await EnsureStoreAccessibleAsync(storeId, cancellationToken);
|
||||||
|
|
||||||
|
var result = await mediator.Send(new WriteStoredCardRechargeRecordCommand
|
||||||
|
{
|
||||||
|
StoreId = storeId,
|
||||||
|
MemberId = StoreApiHelpers.ParseRequiredSnowflake(request.MemberId, nameof(request.MemberId)),
|
||||||
|
PlanId = StoreApiHelpers.ParseSnowflakeOrNull(request.PlanId),
|
||||||
|
RechargeAmount = request.RechargeAmount,
|
||||||
|
GiftAmount = request.GiftAmount,
|
||||||
|
PaymentMethod = request.PaymentMethod,
|
||||||
|
RechargedAt = request.RechargedAt,
|
||||||
|
Remark = request.Remark
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return ApiResponse<StoredCardRechargeRecordResponse>.Ok(MapRecord(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService);
|
||||||
|
await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ParseDateOrNull(string? value, string fieldName)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value)
|
||||||
|
? null
|
||||||
|
: StoreApiHelpers.ParseDateOnly(value, fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoredCardPlanResponse MapPlan(MemberStoredCardPlanDto source)
|
||||||
|
{
|
||||||
|
return new StoredCardPlanResponse
|
||||||
|
{
|
||||||
|
PlanId = source.PlanId.ToString(),
|
||||||
|
RechargeAmount = source.RechargeAmount,
|
||||||
|
GiftAmount = source.GiftAmount,
|
||||||
|
ArrivedAmount = source.ArrivedAmount,
|
||||||
|
SortOrder = source.SortOrder,
|
||||||
|
Status = source.Status,
|
||||||
|
RechargeCount = source.RechargeCount,
|
||||||
|
TotalRechargeAmount = source.TotalRechargeAmount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StoredCardRechargeRecordResponse MapRecord(MemberStoredCardRechargeRecordDto source)
|
||||||
|
{
|
||||||
|
return new StoredCardRechargeRecordResponse
|
||||||
|
{
|
||||||
|
RecordId = source.RecordId.ToString(),
|
||||||
|
RecordNo = source.RecordNo,
|
||||||
|
MemberId = source.MemberId.ToString(),
|
||||||
|
MemberName = source.MemberName,
|
||||||
|
MemberMobileMasked = source.MemberMobileMasked,
|
||||||
|
RechargeAmount = source.RechargeAmount,
|
||||||
|
GiftAmount = source.GiftAmount,
|
||||||
|
ArrivedAmount = source.ArrivedAmount,
|
||||||
|
PaymentMethod = source.PaymentMethod,
|
||||||
|
PaymentMethodText = ResolvePaymentMethodText(source.PaymentMethod),
|
||||||
|
RechargedAt = source.RechargedAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||||
|
PlanId = source.PlanId?.ToString(),
|
||||||
|
Remark = source.Remark
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolvePaymentMethodText(string paymentMethod)
|
||||||
|
{
|
||||||
|
return (paymentMethod ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"wechat" => "微信支付",
|
||||||
|
"alipay" => "支付宝",
|
||||||
|
"cash" => "现金",
|
||||||
|
"card" => "刷卡",
|
||||||
|
"balance" => "余额",
|
||||||
|
_ => "未知"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改储值卡方案状态命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChangeStoredCardPlanStatusCommand : IRequest<MemberStoredCardPlanDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案标识。
|
||||||
|
/// </summary>
|
||||||
|
public long PlanId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; init; } = "disabled";
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除储值卡方案命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteStoredCardPlanCommand : IRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案标识。
|
||||||
|
/// </summary>
|
||||||
|
public long PlanId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存储值卡方案命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoredCardPlanCommand : IRequest<MemberStoredCardPlanDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案标识(编辑时传)。
|
||||||
|
/// </summary>
|
||||||
|
public long? PlanId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序值。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; init; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; init; } = "enabled";
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入储值卡充值记录命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteStoredCardRechargeRecordCommand : IRequest<MemberStoredCardRechargeRecordDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员标识。
|
||||||
|
/// </summary>
|
||||||
|
public long MemberId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案标识(可空)。
|
||||||
|
/// </summary>
|
||||||
|
public long? PlanId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式(wechat/alipay/cash/card/balance)。
|
||||||
|
/// </summary>
|
||||||
|
public string PaymentMethod { get; init; } = "wechat";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值时间(可空,默认当前时间)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RechargedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardPlanDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 方案标识。
|
||||||
|
/// </summary>
|
||||||
|
public long PlanId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额(充值+赠送)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ArrivedAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(enabled/disabled)。
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; init; } = "enabled";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计充值次数。
|
||||||
|
/// </summary>
|
||||||
|
public int RechargeCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 累计充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalRechargeAmount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案列表结果 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardPlanListResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 方案列表。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<MemberStoredCardPlanDto> Items { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页面统计。
|
||||||
|
/// </summary>
|
||||||
|
public MemberStoredCardPlanStatsDto Stats { get; init; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案页统计 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardPlanStatsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 储值总额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalRechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠金总额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalGiftAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月充值。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CurrentMonthRechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值用户数。
|
||||||
|
/// </summary>
|
||||||
|
public int RechargeMemberCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡充值记录 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardRechargeRecordDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 记录标识。
|
||||||
|
/// </summary>
|
||||||
|
public long RecordId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值单号。
|
||||||
|
/// </summary>
|
||||||
|
public string RecordNo { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员标识。
|
||||||
|
/// </summary>
|
||||||
|
public long MemberId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员名称。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberMobileMasked { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ArrivedAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式(wechat/alipay/cash/card/balance/unknown)。
|
||||||
|
/// </summary>
|
||||||
|
public string PaymentMethod { get; init; } = "unknown";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RechargedAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 方案标识。
|
||||||
|
/// </summary>
|
||||||
|
public long? PlanId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡充值记录导出 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardRechargeRecordExportDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件内容 Base64。
|
||||||
|
/// </summary>
|
||||||
|
public string FileContentBase64 { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出总数。
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡充值记录列表结果 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardRechargeRecordListResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<MemberStoredCardRechargeRecordDto> 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,50 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.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.StoredCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改储值卡方案状态处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChangeStoredCardPlanStatusCommandHandler(
|
||||||
|
IStoredCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<ChangeStoredCardPlanStatusCommand, MemberStoredCardPlanDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MemberStoredCardPlanDto> Handle(
|
||||||
|
ChangeStoredCardPlanStatusCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var status = MemberStoredCardMapping.ParsePlanStatus(request.Status);
|
||||||
|
|
||||||
|
var entity = await repository.FindPlanByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.PlanId,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "储值卡方案不存在");
|
||||||
|
|
||||||
|
entity.Status = status;
|
||||||
|
await repository.UpdatePlanAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var aggregates = await repository.GetPlanAggregatesAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
[entity.Id],
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var aggregate = aggregates.TryGetValue(entity.Id, out var value)
|
||||||
|
? value
|
||||||
|
: MemberStoredCardDtoFactory.EmptyAggregate(entity.Id);
|
||||||
|
|
||||||
|
return MemberStoredCardDtoFactory.ToPlanDto(entity, aggregate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.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.StoredCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除储值卡方案处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteStoredCardPlanCommandHandler(
|
||||||
|
IStoredCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<DeleteStoredCardPlanCommand>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Handle(DeleteStoredCardPlanCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
|
||||||
|
var entity = await repository.FindPlanByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.PlanId,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "储值卡方案不存在");
|
||||||
|
|
||||||
|
await repository.DeletePlanAsync(entity, cancellationToken);
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Text;
|
||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出储值卡充值记录处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportStoredCardRechargeRecordCsvQueryHandler(
|
||||||
|
IStoredCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<ExportStoredCardRechargeRecordCsvQuery, MemberStoredCardRechargeRecordExportDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MemberStoredCardRechargeRecordExportDto> Handle(
|
||||||
|
ExportStoredCardRechargeRecordCsvQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var normalizedKeyword = MemberStoredCardMapping.NormalizeKeyword(request.Keyword);
|
||||||
|
var (startUtc, endUtc) = MemberStoredCardMapping.NormalizeDateRange(request.StartDateUtc, request.EndDateUtc);
|
||||||
|
|
||||||
|
var records = await repository.ListRechargeRecordsForExportAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
startUtc,
|
||||||
|
endUtc,
|
||||||
|
normalizedKeyword,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var csv = BuildCsv(records);
|
||||||
|
var bytes = Encoding.UTF8.GetBytes($"\uFEFF{csv}");
|
||||||
|
|
||||||
|
return new MemberStoredCardRechargeRecordExportDto
|
||||||
|
{
|
||||||
|
FileName = $"储值卡充值记录_{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
|
||||||
|
FileContentBase64 = Convert.ToBase64String(bytes),
|
||||||
|
TotalCount = records.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildCsv(IReadOnlyCollection<Domain.Membership.Entities.MemberStoredCardRechargeRecord> records)
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
"充值单号,会员,手机号,充值金额,赠送金额,到账金额,支付方式,充值时间"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var item in records)
|
||||||
|
{
|
||||||
|
lines.Add(string.Join(",",
|
||||||
|
Escape(item.RecordNo),
|
||||||
|
Escape(item.MemberName),
|
||||||
|
Escape(item.MemberMobileMasked),
|
||||||
|
item.RechargeAmount.ToString("0.00"),
|
||||||
|
item.GiftAmount.ToString("0.00"),
|
||||||
|
item.ArrivedAmount.ToString("0.00"),
|
||||||
|
Escape(MemberStoredCardMapping.ToPaymentMethodDisplayText(item.PaymentMethod)),
|
||||||
|
Escape(item.RechargedAt.ToString("yyyy-MM-dd HH:mm:ss"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join('\n', lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Escape(string value)
|
||||||
|
{
|
||||||
|
var text = value.Replace("\"", "\"\"");
|
||||||
|
return $"\"{text}\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetStoredCardPlanListQueryHandler(
|
||||||
|
IStoredCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetStoredCardPlanListQuery, MemberStoredCardPlanListResultDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MemberStoredCardPlanListResultDto> Handle(
|
||||||
|
GetStoredCardPlanListQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
|
||||||
|
var plans = await repository.GetPlansByStoreAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var planIds = plans.Select(item => item.Id).ToList();
|
||||||
|
var aggregates = await repository.GetPlanAggregatesAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
planIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var items = plans
|
||||||
|
.Select(item =>
|
||||||
|
{
|
||||||
|
var aggregate = aggregates.TryGetValue(item.Id, out var value)
|
||||||
|
? value
|
||||||
|
: MemberStoredCardDtoFactory.EmptyAggregate(item.Id);
|
||||||
|
|
||||||
|
return MemberStoredCardDtoFactory.ToPlanDto(item, aggregate);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var stats = await repository.GetPlanStatsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
DateTime.UtcNow,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return new MemberStoredCardPlanListResultDto
|
||||||
|
{
|
||||||
|
Items = items,
|
||||||
|
Stats = MemberStoredCardDtoFactory.ToPlanStatsDto(stats)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡充值记录列表查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetStoredCardRechargeRecordListQueryHandler(
|
||||||
|
IStoredCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetStoredCardRechargeRecordListQuery, MemberStoredCardRechargeRecordListResultDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MemberStoredCardRechargeRecordListResultDto> Handle(
|
||||||
|
GetStoredCardRechargeRecordListQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var page = Math.Max(1, request.Page);
|
||||||
|
var pageSize = Math.Clamp(request.PageSize, 1, 200);
|
||||||
|
var normalizedKeyword = MemberStoredCardMapping.NormalizeKeyword(request.Keyword);
|
||||||
|
var (startUtc, endUtc) = MemberStoredCardMapping.NormalizeDateRange(request.StartDateUtc, request.EndDateUtc);
|
||||||
|
|
||||||
|
var (items, totalCount) = await repository.SearchRechargeRecordsAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
startUtc,
|
||||||
|
endUtc,
|
||||||
|
normalizedKeyword,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return new MemberStoredCardRechargeRecordListResultDto
|
||||||
|
{
|
||||||
|
Items = items.Select(MemberStoredCardDtoFactory.ToRechargeRecordDto).ToList(),
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalCount = totalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.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.StoredCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存储值卡方案处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SaveStoredCardPlanCommandHandler(
|
||||||
|
IStoredCardRepository repository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SaveStoredCardPlanCommand, MemberStoredCardPlanDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MemberStoredCardPlanDto> Handle(
|
||||||
|
SaveStoredCardPlanCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var normalizedRechargeAmount = MemberStoredCardMapping.NormalizeRechargeAmount(request.RechargeAmount);
|
||||||
|
var normalizedGiftAmount = MemberStoredCardMapping.NormalizeGiftAmount(request.GiftAmount);
|
||||||
|
var normalizedSortOrder = MemberStoredCardMapping.NormalizeSortOrder(request.SortOrder);
|
||||||
|
var status = MemberStoredCardMapping.ParsePlanStatus(request.Status);
|
||||||
|
|
||||||
|
var allPlans = await repository.GetPlansByStoreAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var duplicated = allPlans.Any(item =>
|
||||||
|
item.Id != request.PlanId &&
|
||||||
|
item.RechargeAmount == normalizedRechargeAmount &&
|
||||||
|
item.GiftAmount == normalizedGiftAmount);
|
||||||
|
|
||||||
|
if (duplicated)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "已存在相同充值金额与赠送金额的方案");
|
||||||
|
}
|
||||||
|
|
||||||
|
Domain.Membership.Entities.MemberStoredCardPlan entity;
|
||||||
|
if (request.PlanId.HasValue && request.PlanId.Value > 0)
|
||||||
|
{
|
||||||
|
entity = await repository.FindPlanByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.PlanId.Value,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "储值卡方案不存在");
|
||||||
|
|
||||||
|
entity.RechargeAmount = normalizedRechargeAmount;
|
||||||
|
entity.GiftAmount = normalizedGiftAmount;
|
||||||
|
entity.SortOrder = normalizedSortOrder;
|
||||||
|
entity.Status = status;
|
||||||
|
|
||||||
|
await repository.UpdatePlanAsync(entity, cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity = MemberStoredCardDtoFactory.CreatePlanEntity(
|
||||||
|
request,
|
||||||
|
normalizedRechargeAmount,
|
||||||
|
normalizedGiftAmount,
|
||||||
|
normalizedSortOrder,
|
||||||
|
status);
|
||||||
|
|
||||||
|
await repository.AddPlanAsync(entity, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
var aggregates = await repository.GetPlanAggregatesAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
[entity.Id],
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var aggregate = aggregates.TryGetValue(entity.Id, out var value)
|
||||||
|
? value
|
||||||
|
: MemberStoredCardDtoFactory.EmptyAggregate(entity.Id);
|
||||||
|
|
||||||
|
return MemberStoredCardDtoFactory.ToPlanDto(entity, aggregate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.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.StoredCard.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入储值卡充值记录处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WriteStoredCardRechargeRecordCommandHandler(
|
||||||
|
IStoredCardRepository storedCardRepository,
|
||||||
|
IMemberRepository memberRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<WriteStoredCardRechargeRecordCommand, MemberStoredCardRechargeRecordDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MemberStoredCardRechargeRecordDto> Handle(
|
||||||
|
WriteStoredCardRechargeRecordCommand request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
|
var rechargeAmount = MemberStoredCardMapping.NormalizeRechargeAmount(request.RechargeAmount);
|
||||||
|
var giftAmount = MemberStoredCardMapping.NormalizeGiftAmount(request.GiftAmount);
|
||||||
|
var paymentMethod = MemberStoredCardMapping.ParsePaymentMethod(request.PaymentMethod);
|
||||||
|
var remark = MemberStoredCardMapping.NormalizeOptionalRemark(request.Remark);
|
||||||
|
var rechargedAt = request.RechargedAt.HasValue
|
||||||
|
? MemberStoredCardMapping.NormalizeUtc(request.RechargedAt.Value)
|
||||||
|
: DateTime.UtcNow;
|
||||||
|
|
||||||
|
MemberStoredCardPlan? plan = null;
|
||||||
|
if (request.PlanId.HasValue && request.PlanId.Value > 0)
|
||||||
|
{
|
||||||
|
plan = await storedCardRepository.FindPlanByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.StoreId,
|
||||||
|
request.PlanId.Value,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "储值卡方案不存在");
|
||||||
|
|
||||||
|
if (plan.Status != MemberStoredCardPlanStatus.Enabled)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "储值卡方案未启用");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.RechargeAmount != rechargeAmount || plan.GiftAmount != giftAmount)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "充值金额与储值卡方案不一致");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var member = await memberRepository.FindProfileByIdAsync(
|
||||||
|
tenantId,
|
||||||
|
request.MemberId,
|
||||||
|
cancellationToken)
|
||||||
|
?? throw new BusinessException(ErrorCodes.NotFound, "会员不存在");
|
||||||
|
|
||||||
|
var arrivedAmount = decimal.Round(rechargeAmount + giftAmount, 2, MidpointRounding.AwayFromZero);
|
||||||
|
var record = new MemberStoredCardRechargeRecord
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
MemberId = member.Id,
|
||||||
|
PlanId = plan?.Id,
|
||||||
|
RecordNo = MemberStoredCardMapping.BuildRechargeRecordNo(rechargedAt),
|
||||||
|
MemberName = MemberStoredCardMapping.ResolveMemberName(member),
|
||||||
|
MemberMobileMasked = MemberStoredCardMapping.ResolveMemberMobileMasked(member),
|
||||||
|
RechargeAmount = rechargeAmount,
|
||||||
|
GiftAmount = giftAmount,
|
||||||
|
ArrivedAmount = arrivedAmount,
|
||||||
|
PaymentMethod = paymentMethod,
|
||||||
|
Remark = remark,
|
||||||
|
RechargedAt = rechargedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
member.StoredRechargeBalance = decimal.Round(member.StoredRechargeBalance + rechargeAmount, 2, MidpointRounding.AwayFromZero);
|
||||||
|
member.StoredGiftBalance = decimal.Round(member.StoredGiftBalance + giftAmount, 2, MidpointRounding.AwayFromZero);
|
||||||
|
member.StoredBalance = decimal.Round(member.StoredRechargeBalance + member.StoredGiftBalance, 2, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
await memberRepository.UpdateProfileAsync(member, cancellationToken);
|
||||||
|
await storedCardRepository.AddRechargeRecordAsync(record, cancellationToken);
|
||||||
|
await storedCardRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return MemberStoredCardDtoFactory.ToRechargeRecordDto(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Commands;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡 DTO 构造器。
|
||||||
|
/// </summary>
|
||||||
|
internal static class MemberStoredCardDtoFactory
|
||||||
|
{
|
||||||
|
public static MemberStoredCardPlanAggregateSnapshot EmptyAggregate(long planId)
|
||||||
|
{
|
||||||
|
return new MemberStoredCardPlanAggregateSnapshot
|
||||||
|
{
|
||||||
|
PlanId = planId,
|
||||||
|
RechargeCount = 0,
|
||||||
|
TotalRechargeAmount = 0m
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MemberStoredCardPlanDto ToPlanDto(
|
||||||
|
MemberStoredCardPlan source,
|
||||||
|
MemberStoredCardPlanAggregateSnapshot aggregate)
|
||||||
|
{
|
||||||
|
return new MemberStoredCardPlanDto
|
||||||
|
{
|
||||||
|
PlanId = source.Id,
|
||||||
|
RechargeAmount = decimal.Round(source.RechargeAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
GiftAmount = decimal.Round(source.GiftAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
ArrivedAmount = decimal.Round(source.RechargeAmount + source.GiftAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
SortOrder = source.SortOrder,
|
||||||
|
Status = MemberStoredCardMapping.ToPlanStatusText(source.Status),
|
||||||
|
RechargeCount = aggregate.RechargeCount,
|
||||||
|
TotalRechargeAmount = decimal.Round(aggregate.TotalRechargeAmount, 2, MidpointRounding.AwayFromZero)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MemberStoredCardPlanStatsDto ToPlanStatsDto(MemberStoredCardPlanStatsSnapshot source)
|
||||||
|
{
|
||||||
|
return new MemberStoredCardPlanStatsDto
|
||||||
|
{
|
||||||
|
TotalRechargeAmount = decimal.Round(source.TotalRechargeAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
TotalGiftAmount = decimal.Round(source.TotalGiftAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
CurrentMonthRechargeAmount = decimal.Round(source.CurrentMonthRechargeAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
RechargeMemberCount = source.RechargeMemberCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MemberStoredCardRechargeRecordDto ToRechargeRecordDto(MemberStoredCardRechargeRecord source)
|
||||||
|
{
|
||||||
|
return new MemberStoredCardRechargeRecordDto
|
||||||
|
{
|
||||||
|
RecordId = source.Id,
|
||||||
|
RecordNo = source.RecordNo,
|
||||||
|
MemberId = source.MemberId,
|
||||||
|
MemberName = source.MemberName,
|
||||||
|
MemberMobileMasked = source.MemberMobileMasked,
|
||||||
|
RechargeAmount = decimal.Round(source.RechargeAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
GiftAmount = decimal.Round(source.GiftAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
ArrivedAmount = decimal.Round(source.ArrivedAmount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
PaymentMethod = MemberStoredCardMapping.ToPaymentMethodText(source.PaymentMethod),
|
||||||
|
RechargedAt = source.RechargedAt,
|
||||||
|
PlanId = source.PlanId,
|
||||||
|
Remark = source.Remark
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MemberStoredCardPlan CreatePlanEntity(
|
||||||
|
SaveStoredCardPlanCommand request,
|
||||||
|
decimal normalizedRechargeAmount,
|
||||||
|
decimal normalizedGiftAmount,
|
||||||
|
int normalizedSortOrder,
|
||||||
|
MemberStoredCardPlanStatus status)
|
||||||
|
{
|
||||||
|
return new MemberStoredCardPlan
|
||||||
|
{
|
||||||
|
StoreId = request.StoreId,
|
||||||
|
RechargeAmount = normalizedRechargeAmount,
|
||||||
|
GiftAmount = normalizedGiftAmount,
|
||||||
|
SortOrder = normalizedSortOrder,
|
||||||
|
Status = status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
using TakeoutSaaS.Domain.Membership.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Enums;
|
||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡模块映射与标准化。
|
||||||
|
/// </summary>
|
||||||
|
internal static class MemberStoredCardMapping
|
||||||
|
{
|
||||||
|
public static MemberStoredCardPlanStatus ParsePlanStatus(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"enabled" => MemberStoredCardPlanStatus.Enabled,
|
||||||
|
"disabled" => MemberStoredCardPlanStatus.Disabled,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "status 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToPlanStatusText(MemberStoredCardPlanStatus value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
MemberStoredCardPlanStatus.Enabled => "enabled",
|
||||||
|
MemberStoredCardPlanStatus.Disabled => "disabled",
|
||||||
|
_ => "disabled"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PaymentMethod ParsePaymentMethod(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"wechat" => PaymentMethod.WeChatPay,
|
||||||
|
"alipay" => PaymentMethod.Alipay,
|
||||||
|
"cash" => PaymentMethod.Cash,
|
||||||
|
"card" => PaymentMethod.Card,
|
||||||
|
"balance" => PaymentMethod.Balance,
|
||||||
|
_ => throw new BusinessException(ErrorCodes.BadRequest, "paymentMethod 参数不合法")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToPaymentMethodText(PaymentMethod value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PaymentMethod.WeChatPay => "wechat",
|
||||||
|
PaymentMethod.Alipay => "alipay",
|
||||||
|
PaymentMethod.Cash => "cash",
|
||||||
|
PaymentMethod.Card => "card",
|
||||||
|
PaymentMethod.Balance => "balance",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToPaymentMethodDisplayText(PaymentMethod value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
PaymentMethod.WeChatPay => "微信支付",
|
||||||
|
PaymentMethod.Alipay => "支付宝",
|
||||||
|
PaymentMethod.Cash => "现金",
|
||||||
|
PaymentMethod.Card => "刷卡",
|
||||||
|
PaymentMethod.Balance => "余额",
|
||||||
|
_ => "未知"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal NormalizeRechargeAmount(decimal value)
|
||||||
|
{
|
||||||
|
if (value <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "rechargeAmount 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal NormalizeGiftAmount(decimal value)
|
||||||
|
{
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "giftAmount 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return decimal.Round(value, 2, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int NormalizeSortOrder(int value)
|
||||||
|
{
|
||||||
|
if (value < 0 || value > 9999)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "sortOrder 参数不合法");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? NormalizeKeyword(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 64)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "keyword 长度不能超过 64");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? NormalizeOptionalRemark(string? value)
|
||||||
|
{
|
||||||
|
var normalized = (value ?? string.Empty).Trim();
|
||||||
|
if (normalized.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Length > 256)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "remark 长度不能超过 256");
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (DateTime? StartUtc, DateTime? EndUtc) NormalizeDateRange(DateTime? startUtc, DateTime? endUtc)
|
||||||
|
{
|
||||||
|
DateTime? normalizedStart = null;
|
||||||
|
DateTime? normalizedEnd = null;
|
||||||
|
|
||||||
|
if (startUtc.HasValue)
|
||||||
|
{
|
||||||
|
var utcValue = NormalizeUtc(startUtc.Value);
|
||||||
|
normalizedStart = new DateTime(utcValue.Year, utcValue.Month, utcValue.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endUtc.HasValue)
|
||||||
|
{
|
||||||
|
var utcValue = NormalizeUtc(endUtc.Value);
|
||||||
|
normalizedEnd = new DateTime(utcValue.Year, utcValue.Month, utcValue.Day, 0, 0, 0, DateTimeKind.Utc)
|
||||||
|
.AddDays(1)
|
||||||
|
.AddTicks(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedStart.HasValue && normalizedEnd.HasValue && normalizedStart > normalizedEnd)
|
||||||
|
{
|
||||||
|
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 BuildRechargeRecordNo(DateTime nowUtc)
|
||||||
|
{
|
||||||
|
var utcNow = NormalizeUtc(nowUtc);
|
||||||
|
return $"CZ{utcNow:yyyyMMddHHmmssfff}{Random.Shared.Next(1000, 9999)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
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,30 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出储值卡充值记录 CSV。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportStoredCardRechargeRecordCsvQuery : IRequest<MemberStoredCardRechargeRecordExportDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { 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,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询储值卡方案列表。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetStoredCardPlanListQuery : IRequest<MemberStoredCardPlanListResultDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Members.StoredCard.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Members.StoredCard.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询储值卡充值记录分页。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetStoredCardRechargeRecordListQuery : IRequest<MemberStoredCardRechargeRecordListResultDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { 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; } = 8;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using TakeoutSaaS.Domain.Membership.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Membership.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员储值卡充值方案。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardPlan : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序值(越小越靠前)。
|
||||||
|
/// </summary>
|
||||||
|
public int SortOrder { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启用状态。
|
||||||
|
/// </summary>
|
||||||
|
public MemberStoredCardPlanStatus Status { get; set; } = MemberStoredCardPlanStatus.Enabled;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Membership.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员储值卡充值记录。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemberStoredCardRechargeRecord : MultiTenantEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员标识。
|
||||||
|
/// </summary>
|
||||||
|
public long MemberId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值方案标识(可空,表示非方案充值)。
|
||||||
|
/// </summary>
|
||||||
|
public long? PlanId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值单号。
|
||||||
|
/// </summary>
|
||||||
|
public string RecordNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员名称快照。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会员手机号快照(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public string MemberMobileMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal RechargeAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠送金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal GiftAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额(充值+赠送)。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod PaymentMethod { get; set; } = PaymentMethod.Unknown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime RechargedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace TakeoutSaaS.Domain.Membership.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡方案状态。
|
||||||
|
/// </summary>
|
||||||
|
public enum MemberStoredCardPlanStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 已停用。
|
||||||
|
/// </summary>
|
||||||
|
Disabled = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已启用。
|
||||||
|
/// </summary>
|
||||||
|
Enabled = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
using TakeoutSaaS.Domain.Membership.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Membership.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡模块仓储契约。
|
||||||
|
/// </summary>
|
||||||
|
public interface IStoredCardRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 查询门店下全部储值方案。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<MemberStoredCardPlan>> GetPlansByStoreAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按标识查询储值方案。
|
||||||
|
/// </summary>
|
||||||
|
Task<MemberStoredCardPlan?> FindPlanByIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long planId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增储值方案。
|
||||||
|
/// </summary>
|
||||||
|
Task AddPlanAsync(MemberStoredCardPlan entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新储值方案。
|
||||||
|
/// </summary>
|
||||||
|
Task UpdatePlanAsync(MemberStoredCardPlan entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除储值方案。
|
||||||
|
/// </summary>
|
||||||
|
Task DeletePlanAsync(MemberStoredCardPlan entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按方案聚合充值次数与金额。
|
||||||
|
/// </summary>
|
||||||
|
Task<Dictionary<long, MemberStoredCardPlanAggregateSnapshot>> GetPlanAggregatesAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
IReadOnlyCollection<long> planIds,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取储值方案页统计快照。
|
||||||
|
/// </summary>
|
||||||
|
Task<MemberStoredCardPlanStatsSnapshot> GetPlanStatsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime nowUtc,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询充值记录分页。
|
||||||
|
/// </summary>
|
||||||
|
Task<(IReadOnlyList<MemberStoredCardRechargeRecord> Items, int TotalCount)> SearchRechargeRecordsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime? startUtc,
|
||||||
|
DateTime? endUtc,
|
||||||
|
string? keyword,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询充值记录导出数据。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<MemberStoredCardRechargeRecord>> ListRechargeRecordsForExportAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime? startUtc,
|
||||||
|
DateTime? endUtc,
|
||||||
|
string? keyword,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增充值记录。
|
||||||
|
/// </summary>
|
||||||
|
Task AddRechargeRecordAsync(MemberStoredCardRechargeRecord entity, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 持久化变更。
|
||||||
|
/// </summary>
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值方案聚合快照。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record MemberStoredCardPlanAggregateSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 方案标识。
|
||||||
|
/// </summary>
|
||||||
|
public required long PlanId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值次数。
|
||||||
|
/// </summary>
|
||||||
|
public int RechargeCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 充值总额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalRechargeAmount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡统计快照。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record MemberStoredCardPlanStatsSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 储值总额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalRechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 赠金总额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TotalGiftAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月充值额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CurrentMonthRechargeAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值用户数。
|
||||||
|
/// </summary>
|
||||||
|
public int RechargeMemberCount { get; init; }
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ public static class AppServiceCollectionExtensions
|
|||||||
services.AddScoped<IPromotionCampaignRepository, EfPromotionCampaignRepository>();
|
services.AddScoped<IPromotionCampaignRepository, EfPromotionCampaignRepository>();
|
||||||
services.AddScoped<IPunchCardRepository, EfPunchCardRepository>();
|
services.AddScoped<IPunchCardRepository, EfPunchCardRepository>();
|
||||||
services.AddScoped<IMemberRepository, EfMemberRepository>();
|
services.AddScoped<IMemberRepository, EfMemberRepository>();
|
||||||
|
services.AddScoped<IStoredCardRepository, EfStoredCardRepository>();
|
||||||
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
||||||
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
||||||
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();
|
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();
|
||||||
|
|||||||
@@ -402,6 +402,14 @@ public sealed class TakeoutAppDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<MemberPointLedger> MemberPointLedgers => Set<MemberPointLedger>();
|
public DbSet<MemberPointLedger> MemberPointLedgers => Set<MemberPointLedger>();
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 会员储值方案。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<MemberStoredCardPlan> MemberStoredCardPlans => Set<MemberStoredCardPlan>();
|
||||||
|
/// <summary>
|
||||||
|
/// 会员储值充值记录。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<MemberStoredCardRechargeRecord> MemberStoredCardRechargeRecords => Set<MemberStoredCardRechargeRecord>();
|
||||||
|
/// <summary>
|
||||||
/// 会话记录。
|
/// 会话记录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<ChatSession> ChatSessions => Set<ChatSession>();
|
public DbSet<ChatSession> ChatSessions => Set<ChatSession>();
|
||||||
@@ -568,6 +576,8 @@ public sealed class TakeoutAppDbContext(
|
|||||||
ConfigureMemberProfileTag(modelBuilder.Entity<MemberProfileTag>());
|
ConfigureMemberProfileTag(modelBuilder.Entity<MemberProfileTag>());
|
||||||
ConfigureMemberDaySetting(modelBuilder.Entity<MemberDaySetting>());
|
ConfigureMemberDaySetting(modelBuilder.Entity<MemberDaySetting>());
|
||||||
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
|
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
|
||||||
|
ConfigureMemberStoredCardPlan(modelBuilder.Entity<MemberStoredCardPlan>());
|
||||||
|
ConfigureMemberStoredCardRechargeRecord(modelBuilder.Entity<MemberStoredCardRechargeRecord>());
|
||||||
ConfigureChatSession(modelBuilder.Entity<ChatSession>());
|
ConfigureChatSession(modelBuilder.Entity<ChatSession>());
|
||||||
ConfigureChatMessage(modelBuilder.Entity<ChatMessage>());
|
ConfigureChatMessage(modelBuilder.Entity<ChatMessage>());
|
||||||
ConfigureSupportTicket(modelBuilder.Entity<SupportTicket>());
|
ConfigureSupportTicket(modelBuilder.Entity<SupportTicket>());
|
||||||
@@ -1846,6 +1856,42 @@ public sealed class TakeoutAppDbContext(
|
|||||||
builder.HasIndex(x => new { x.TenantId, x.MemberId, x.OccurredAt });
|
builder.HasIndex(x => new { x.TenantId, x.MemberId, x.OccurredAt });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureMemberStoredCardPlan(EntityTypeBuilder<MemberStoredCardPlan> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("member_stored_card_plans");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.RechargeAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.GiftAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.SortOrder).IsRequired();
|
||||||
|
builder.Property(x => x.Status).HasConversion<int>();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RechargeAmount, x.GiftAmount }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.SortOrder });
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Status });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureMemberStoredCardRechargeRecord(EntityTypeBuilder<MemberStoredCardRechargeRecord> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("member_stored_card_recharge_records");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.StoreId).IsRequired();
|
||||||
|
builder.Property(x => x.MemberId).IsRequired();
|
||||||
|
builder.Property(x => x.PlanId);
|
||||||
|
builder.Property(x => x.RecordNo).HasMaxLength(32).IsRequired();
|
||||||
|
builder.Property(x => x.MemberName).HasMaxLength(64).IsRequired();
|
||||||
|
builder.Property(x => x.MemberMobileMasked).HasMaxLength(32).IsRequired();
|
||||||
|
builder.Property(x => x.RechargeAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.GiftAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.ArrivedAmount).HasPrecision(18, 2);
|
||||||
|
builder.Property(x => x.PaymentMethod).HasConversion<int>();
|
||||||
|
builder.Property(x => x.Remark).HasMaxLength(256);
|
||||||
|
builder.Property(x => x.RechargedAt).IsRequired();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RecordNo }).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.MemberId, x.RechargedAt });
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PlanId, x.RechargedAt });
|
||||||
|
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.RechargedAt });
|
||||||
|
}
|
||||||
|
|
||||||
private static void ConfigureChatSession(EntityTypeBuilder<ChatSession> builder)
|
private static void ConfigureChatSession(EntityTypeBuilder<ChatSession> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("chat_sessions");
|
builder.ToTable("chat_sessions");
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Membership.Repositories;
|
||||||
|
using TakeoutSaaS.Infrastructure.App.Persistence;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 储值卡模块 EF Core 仓储实现。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EfStoredCardRepository(TakeoutAppDbContext context) : IStoredCardRepository
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<MemberStoredCardPlan>> GetPlansByStoreAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await context.MemberStoredCardPlans
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
|
||||||
|
.OrderBy(item => item.SortOrder)
|
||||||
|
.ThenBy(item => item.RechargeAmount)
|
||||||
|
.ThenBy(item => item.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<MemberStoredCardPlan?> FindPlanByIdAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
long planId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.MemberStoredCardPlans
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.Id == planId)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddPlanAsync(MemberStoredCardPlan entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.MemberStoredCardPlans.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task UpdatePlanAsync(MemberStoredCardPlan entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.MemberStoredCardPlans.Update(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task DeletePlanAsync(MemberStoredCardPlan entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
context.MemberStoredCardPlans.Remove(entity);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<Dictionary<long, MemberStoredCardPlanAggregateSnapshot>> GetPlanAggregatesAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
IReadOnlyCollection<long> planIds,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (planIds.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var aggregates = await context.MemberStoredCardRechargeRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item =>
|
||||||
|
item.TenantId == tenantId &&
|
||||||
|
item.StoreId == storeId &&
|
||||||
|
item.PlanId.HasValue &&
|
||||||
|
planIds.Contains(item.PlanId.Value))
|
||||||
|
.GroupBy(item => item.PlanId!.Value)
|
||||||
|
.Select(group => new MemberStoredCardPlanAggregateSnapshot
|
||||||
|
{
|
||||||
|
PlanId = group.Key,
|
||||||
|
RechargeCount = group.Count(),
|
||||||
|
TotalRechargeAmount = group.Sum(item => item.RechargeAmount)
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return aggregates.ToDictionary(item => item.PlanId, item => item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<MemberStoredCardPlanStatsSnapshot> GetPlanStatsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime nowUtc,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var utcNow = NormalizeUtc(nowUtc);
|
||||||
|
var monthStart = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
var summary = await context.MemberStoredCardRechargeRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId)
|
||||||
|
.GroupBy(_ => 1)
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
TotalRechargeAmount = group.Sum(item => item.RechargeAmount),
|
||||||
|
TotalGiftAmount = group.Sum(item => item.GiftAmount),
|
||||||
|
CurrentMonthRechargeAmount = group
|
||||||
|
.Where(item => item.RechargedAt >= monthStart && item.RechargedAt <= utcNow)
|
||||||
|
.Sum(item => item.RechargeAmount),
|
||||||
|
RechargeMemberCount = group.Select(item => item.MemberId).Distinct().Count()
|
||||||
|
})
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (summary is null)
|
||||||
|
{
|
||||||
|
return new MemberStoredCardPlanStatsSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MemberStoredCardPlanStatsSnapshot
|
||||||
|
{
|
||||||
|
TotalRechargeAmount = summary.TotalRechargeAmount,
|
||||||
|
TotalGiftAmount = summary.TotalGiftAmount,
|
||||||
|
CurrentMonthRechargeAmount = summary.CurrentMonthRechargeAmount,
|
||||||
|
RechargeMemberCount = summary.RechargeMemberCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<(IReadOnlyList<MemberStoredCardRechargeRecord> Items, int TotalCount)> SearchRechargeRecordsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime? startUtc,
|
||||||
|
DateTime? endUtc,
|
||||||
|
string? keyword,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalizedPage = Math.Max(1, page);
|
||||||
|
var normalizedPageSize = Math.Clamp(pageSize, 1, 500);
|
||||||
|
|
||||||
|
var query = BuildRechargeRecordQuery(
|
||||||
|
tenantId,
|
||||||
|
storeId,
|
||||||
|
startUtc,
|
||||||
|
endUtc,
|
||||||
|
keyword);
|
||||||
|
|
||||||
|
var totalCount = await query.CountAsync(cancellationToken);
|
||||||
|
if (totalCount == 0)
|
||||||
|
{
|
||||||
|
return ([], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = await query
|
||||||
|
.OrderByDescending(item => item.RechargedAt)
|
||||||
|
.ThenByDescending(item => item.Id)
|
||||||
|
.Skip((normalizedPage - 1) * normalizedPageSize)
|
||||||
|
.Take(normalizedPageSize)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return (items, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<MemberStoredCardRechargeRecord>> ListRechargeRecordsForExportAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime? startUtc,
|
||||||
|
DateTime? endUtc,
|
||||||
|
string? keyword,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await BuildRechargeRecordQuery(
|
||||||
|
tenantId,
|
||||||
|
storeId,
|
||||||
|
startUtc,
|
||||||
|
endUtc,
|
||||||
|
keyword)
|
||||||
|
.OrderByDescending(item => item.RechargedAt)
|
||||||
|
.ThenByDescending(item => item.Id)
|
||||||
|
.Take(20_000)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task AddRechargeRecordAsync(MemberStoredCardRechargeRecord entity, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.MemberStoredCardRechargeRecords.AddAsync(entity, cancellationToken).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IQueryable<MemberStoredCardRechargeRecord> BuildRechargeRecordQuery(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime? startUtc,
|
||||||
|
DateTime? endUtc,
|
||||||
|
string? keyword)
|
||||||
|
{
|
||||||
|
var query = context.MemberStoredCardRechargeRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.TenantId == tenantId && item.StoreId == storeId);
|
||||||
|
|
||||||
|
if (startUtc.HasValue)
|
||||||
|
{
|
||||||
|
var normalizedStart = NormalizeUtc(startUtc.Value);
|
||||||
|
query = query.Where(item => item.RechargedAt >= normalizedStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endUtc.HasValue)
|
||||||
|
{
|
||||||
|
var normalizedEnd = NormalizeUtc(endUtc.Value);
|
||||||
|
query = query.Where(item => item.RechargedAt <= normalizedEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedKeyword = (keyword ?? string.Empty).Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(normalizedKeyword))
|
||||||
|
{
|
||||||
|
var like = $"%{normalizedKeyword}%";
|
||||||
|
query = query.Where(item =>
|
||||||
|
EF.Functions.ILike(item.RecordNo, like) ||
|
||||||
|
EF.Functions.ILike(item.MemberName, like) ||
|
||||||
|
EF.Functions.ILike(item.MemberMobileMasked, like));
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime NormalizeUtc(DateTime value)
|
||||||
|
{
|
||||||
|
return value.Kind switch
|
||||||
|
{
|
||||||
|
DateTimeKind.Utc => value,
|
||||||
|
DateTimeKind.Local => value.ToUniversalTime(),
|
||||||
|
_ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMemberStoredCardModule : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "member_stored_card_plans",
|
||||||
|
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: "门店标识。"),
|
||||||
|
RechargeAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "充值金额。"),
|
||||||
|
GiftAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "赠送金额。"),
|
||||||
|
SortOrder = table.Column<int>(type: "integer", nullable: false, comment: "排序值(越小越靠前)。"),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false, comment: "启用状态。"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"),
|
||||||
|
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"),
|
||||||
|
CreatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
UpdatedBy = table.Column<long>(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"),
|
||||||
|
DeletedBy = table.Column<long>(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"),
|
||||||
|
TenantId = table.Column<long>(type: "bigint", nullable: false, comment: "所属租户 ID。")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_member_stored_card_plans", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "会员储值卡充值方案。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "member_stored_card_recharge_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: "门店标识。"),
|
||||||
|
MemberId = table.Column<long>(type: "bigint", nullable: false, comment: "会员标识。"),
|
||||||
|
PlanId = table.Column<long>(type: "bigint", nullable: true, comment: "充值方案标识(可空,表示非方案充值)。"),
|
||||||
|
RecordNo = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "充值单号。"),
|
||||||
|
MemberName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false, comment: "会员名称快照。"),
|
||||||
|
MemberMobileMasked = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "会员手机号快照(脱敏)。"),
|
||||||
|
RechargeAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "充值金额。"),
|
||||||
|
GiftAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "赠送金额。"),
|
||||||
|
ArrivedAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "到账金额(充值+赠送)。"),
|
||||||
|
PaymentMethod = table.Column<int>(type: "integer", nullable: false, comment: "支付方式。"),
|
||||||
|
Remark = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注。"),
|
||||||
|
RechargedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, comment: "充值时间(UTC)。"),
|
||||||
|
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_member_stored_card_recharge_records", x => x.Id);
|
||||||
|
},
|
||||||
|
comment: "会员储值卡充值记录。");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_member_stored_card_plans_TenantId_StoreId_RechargeAmount_Gi~",
|
||||||
|
table: "member_stored_card_plans",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "RechargeAmount", "GiftAmount" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_member_stored_card_plans_TenantId_StoreId_SortOrder",
|
||||||
|
table: "member_stored_card_plans",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "SortOrder" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_member_stored_card_plans_TenantId_StoreId_Status",
|
||||||
|
table: "member_stored_card_plans",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "Status" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_member_stored_card_recharge_records_TenantId_StoreId_Member~",
|
||||||
|
table: "member_stored_card_recharge_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "MemberId", "RechargedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_member_stored_card_recharge_records_TenantId_StoreId_PlanId~",
|
||||||
|
table: "member_stored_card_recharge_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "PlanId", "RechargedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_member_stored_card_recharge_records_TenantId_StoreId_Rechar~",
|
||||||
|
table: "member_stored_card_recharge_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "RechargedAt" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_member_stored_card_recharge_records_TenantId_StoreId_Record~",
|
||||||
|
table: "member_stored_card_recharge_records",
|
||||||
|
columns: new[] { "TenantId", "StoreId", "RecordNo" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "member_stored_card_plans");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "member_stored_card_recharge_records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2976,6 +2976,67 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberDaySetting", 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>("ExtraDiscountRate")
|
||||||
|
.HasPrecision(5, 2)
|
||||||
|
.HasColumnType("numeric(5,2)")
|
||||||
|
.HasComment("会员日额外折扣(如 9 表示 9 折)。");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否启用会员日。");
|
||||||
|
|
||||||
|
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>("Weekday")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("周几(1-7,对应周一到周日)。");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("member_day_settings", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("会员日配置。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -3116,6 +3177,21 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasComment("会员状态。");
|
.HasComment("会员状态。");
|
||||||
|
|
||||||
|
b.Property<decimal>("StoredBalance")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("储值余额。");
|
||||||
|
|
||||||
|
b.Property<decimal>("StoredGiftBalance")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("储值赠金余额。");
|
||||||
|
|
||||||
|
b.Property<decimal>("StoredRechargeBalance")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("储值实充余额。");
|
||||||
|
|
||||||
b.Property<long>("TenantId")
|
b.Property<long>("TenantId")
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("所属租户 ID。");
|
.HasComment("所属租户 ID。");
|
||||||
@@ -3134,6 +3210,8 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "MemberTierId");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "Mobile")
|
b.HasIndex("TenantId", "Mobile")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
@@ -3143,6 +3221,252 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfileTag", 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<long>("MemberProfileId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("会员标识。");
|
||||||
|
|
||||||
|
b.Property<string>("TagName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("标签名。");
|
||||||
|
|
||||||
|
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", "MemberProfileId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "MemberProfileId", "TagName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("member_profile_tags", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("会员标签。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberStoredCardPlan", 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>("GiftAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("赠送金额。");
|
||||||
|
|
||||||
|
b.Property<decimal>("RechargeAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("充值金额。");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("排序值(越小越靠前)。");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("启用状态。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店标识。");
|
||||||
|
|
||||||
|
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", "SortOrder");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "Status");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "RechargeAmount", "GiftAmount")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("member_stored_card_plans", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("会员储值卡充值方案。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberStoredCardRechargeRecord", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("实体唯一标识。");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("ArrivedAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.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>("GiftAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("赠送金额。");
|
||||||
|
|
||||||
|
b.Property<long>("MemberId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("会员标识。");
|
||||||
|
|
||||||
|
b.Property<string>("MemberMobileMasked")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("会员手机号快照(脱敏)。");
|
||||||
|
|
||||||
|
b.Property<string>("MemberName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("会员名称快照。");
|
||||||
|
|
||||||
|
b.Property<int>("PaymentMethod")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("支付方式。");
|
||||||
|
|
||||||
|
b.Property<long?>("PlanId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("充值方案标识(可空,表示非方案充值)。");
|
||||||
|
|
||||||
|
b.Property<decimal>("RechargeAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("充值金额。");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RechargedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasComment("充值时间(UTC)。");
|
||||||
|
|
||||||
|
b.Property<string>("RecordNo")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("充值单号。");
|
||||||
|
|
||||||
|
b.Property<string>("Remark")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasComment("备注。");
|
||||||
|
|
||||||
|
b.Property<long>("StoreId")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasComment("门店标识。");
|
||||||
|
|
||||||
|
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", "RechargedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "RecordNo")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "MemberId", "RechargedAt");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "StoreId", "PlanId", "RechargedAt");
|
||||||
|
|
||||||
|
b.ToTable("member_stored_card_recharge_records", null, t =>
|
||||||
|
{
|
||||||
|
t.HasComment("会员储值卡充值记录。");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b =>
|
modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -3157,6 +3481,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasComment("等级权益(JSON)。");
|
.HasComment("等级权益(JSON)。");
|
||||||
|
|
||||||
|
b.Property<string>("ColorHex")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)")
|
||||||
|
.HasComment("主题色。");
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasComment("创建时间(UTC)。");
|
.HasComment("创建时间(UTC)。");
|
||||||
@@ -3173,6 +3503,20 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
.HasComment("删除人用户标识(软删除),未删除时为 null。");
|
||||||
|
|
||||||
|
b.Property<int>("DowngradeWindowDays")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("降级观察窗口天数。");
|
||||||
|
|
||||||
|
b.Property<string>("IconKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasComment("图标键。");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDefault")
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasComment("是否默认等级。");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
@@ -3199,11 +3543,28 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UpgradeAmountThreshold")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)")
|
||||||
|
.HasComment("升级累计消费门槛。");
|
||||||
|
|
||||||
|
b.Property<int?>("UpgradeOrderCountThreshold")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasComment("升级消费次数门槛。");
|
||||||
|
|
||||||
|
b.Property<string>("UpgradeRuleType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)")
|
||||||
|
.HasComment("升级规则类型(none/amount/count/both)。");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("TenantId", "Name")
|
b.HasIndex("TenantId", "Name")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("TenantId", "SortOrder");
|
||||||
|
|
||||||
b.ToTable("member_tiers", null, t =>
|
b.ToTable("member_tiers", null, t =>
|
||||||
{
|
{
|
||||||
t.HasComment("会员等级定义。");
|
t.HasComment("会员等级定义。");
|
||||||
|
|||||||
Reference in New Issue
Block a user