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<IPunchCardRepository, EfPunchCardRepository>();
|
||||
services.AddScoped<IMemberRepository, EfMemberRepository>();
|
||||
services.AddScoped<IStoredCardRepository, EfStoredCardRepository>();
|
||||
services.AddScoped<IOrderRepository, EfOrderRepository>();
|
||||
services.AddScoped<IPaymentRepository, EfPaymentRepository>();
|
||||
services.AddScoped<IDeliveryRepository, EfDeliveryRepository>();
|
||||
|
||||
@@ -402,6 +402,14 @@ public sealed class TakeoutAppDbContext(
|
||||
/// </summary>
|
||||
public DbSet<MemberPointLedger> MemberPointLedgers => Set<MemberPointLedger>();
|
||||
/// <summary>
|
||||
/// 会员储值方案。
|
||||
/// </summary>
|
||||
public DbSet<MemberStoredCardPlan> MemberStoredCardPlans => Set<MemberStoredCardPlan>();
|
||||
/// <summary>
|
||||
/// 会员储值充值记录。
|
||||
/// </summary>
|
||||
public DbSet<MemberStoredCardRechargeRecord> MemberStoredCardRechargeRecords => Set<MemberStoredCardRechargeRecord>();
|
||||
/// <summary>
|
||||
/// 会话记录。
|
||||
/// </summary>
|
||||
public DbSet<ChatSession> ChatSessions => Set<ChatSession>();
|
||||
@@ -568,6 +576,8 @@ public sealed class TakeoutAppDbContext(
|
||||
ConfigureMemberProfileTag(modelBuilder.Entity<MemberProfileTag>());
|
||||
ConfigureMemberDaySetting(modelBuilder.Entity<MemberDaySetting>());
|
||||
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
|
||||
ConfigureMemberStoredCardPlan(modelBuilder.Entity<MemberStoredCardPlan>());
|
||||
ConfigureMemberStoredCardRechargeRecord(modelBuilder.Entity<MemberStoredCardRechargeRecord>());
|
||||
ConfigureChatSession(modelBuilder.Entity<ChatSession>());
|
||||
ConfigureChatMessage(modelBuilder.Entity<ChatMessage>());
|
||||
ConfigureSupportTicket(modelBuilder.Entity<SupportTicket>());
|
||||
@@ -1846,6 +1856,42 @@ public sealed class TakeoutAppDbContext(
|
||||
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)
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -3116,6 +3177,21 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("integer")
|
||||
.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")
|
||||
.HasColumnType("bigint")
|
||||
.HasComment("所属租户 ID。");
|
||||
@@ -3134,6 +3210,8 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId", "MemberTierId");
|
||||
|
||||
b.HasIndex("TenantId", "Mobile")
|
||||
.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 =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -3157,6 +3481,12 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("text")
|
||||
.HasComment("等级权益(JSON)。");
|
||||
|
||||
b.Property<string>("ColorHex")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)")
|
||||
.HasComment("主题色。");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasComment("创建时间(UTC)。");
|
||||
@@ -3173,6 +3503,20 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("bigint")
|
||||
.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")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
@@ -3199,11 +3543,28 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
||||
.HasColumnType("bigint")
|
||||
.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.HasIndex("TenantId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("TenantId", "SortOrder");
|
||||
|
||||
b.ToTable("member_tiers", null, t =>
|
||||
{
|
||||
t.HasComment("会员等级定义。");
|
||||
|
||||
Reference in New Issue
Block a user