From b0bb87d97c3448763a70ce6c4a69258e973a4a77 Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Wed, 4 Mar 2026 15:48:37 +0800
Subject: [PATCH] feat(finance): add tenant settlement query backend
---
.../Finance/FinanceSettlementContracts.cs | 247 +
.../FinanceSettlementController.cs | 262 +
.../Settlement/Dto/FinanceSettlementDtos.cs | 173 +
.../ExportFinanceSettlementCsvQueryHandler.cs | 71 +
.../Handlers/FinanceSettlementMapping.cs | 75 +
...GetFinanceSettlementAccountQueryHandler.cs | 42 +
.../GetFinanceSettlementDetailQueryHandler.cs | 36 +
.../GetFinanceSettlementStatsQueryHandler.cs | 37 +
...SearchFinanceSettlementListQueryHandler.cs | 44 +
.../ExportFinanceSettlementCsvQuery.cs | 31 +
.../GetFinanceSettlementAccountQuery.cs | 11 +
.../GetFinanceSettlementDetailQuery.cs | 31 +
.../Queries/GetFinanceSettlementStatsQuery.cs | 15 +
.../SearchFinanceSettlementListQuery.cs | 41 +
.../SubmitTenantVerificationCommand.cs | 10 +
.../App/Tenants/Dto/TenantVerificationDto.cs | 10 +
.../SubmitTenantVerificationCommandHandler.cs | 2 +
.../App/Tenants/TenantMapping.cs | 2 +
.../Finance/Models/FinanceSettlementRecord.cs | 128 +
.../IFinanceTransactionRepository.cs | 51 +
.../Entities/TenantVerificationProfile.cs | 13 +
.../EfFinanceTransactionRepository.cs | 297 +
...VerificationSettlementChannels.Designer.cs | 9918 +++++++++++++++++
...AddTenantVerificationSettlementChannels.cs | 42 +
.../TakeoutAppDbContextModelSnapshot.cs | 10 +
25 files changed, 11599 insertions(+)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Finance/FinanceSettlementContracts.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/FinanceSettlementController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Dto/FinanceSettlementDtos.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/ExportFinanceSettlementCsvQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/FinanceSettlementMapping.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementAccountQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementDetailQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementStatsQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/SearchFinanceSettlementListQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/ExportFinanceSettlementCsvQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementAccountQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementDetailQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementStatsQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/SearchFinanceSettlementListQuery.cs
create mode 100644 src/Domain/TakeoutSaaS.Domain/Finance/Models/FinanceSettlementRecord.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304072055_AddTenantVerificationSettlementChannels.Designer.cs
create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304072055_AddTenantVerificationSettlementChannels.cs
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Finance/FinanceSettlementContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Finance/FinanceSettlementContracts.cs
new file mode 100644
index 0000000..4c7e77d
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Finance/FinanceSettlementContracts.cs
@@ -0,0 +1,247 @@
+namespace TakeoutSaaS.TenantApi.Contracts.Finance;
+
+///
+/// 到账统计请求。
+///
+public sealed class FinanceSettlementStatsRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+}
+
+///
+/// 到账筛选请求。
+///
+public class FinanceSettlementFilterRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 开始日期(yyyy-MM-dd)。
+ ///
+ public string? StartDate { get; set; }
+
+ ///
+ /// 结束日期(yyyy-MM-dd)。
+ ///
+ public string? EndDate { get; set; }
+
+ ///
+ /// 渠道(wechat/alipay)。
+ ///
+ public string? Channel { get; set; }
+}
+
+///
+/// 到账列表请求。
+///
+public sealed class FinanceSettlementListRequest : FinanceSettlementFilterRequest
+{
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; } = 1;
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; } = 20;
+}
+
+///
+/// 到账明细请求。
+///
+public sealed class FinanceSettlementDetailRequest
+{
+ ///
+ /// 门店 ID。
+ ///
+ public string StoreId { get; set; } = string.Empty;
+
+ ///
+ /// 到账日期(yyyy-MM-dd)。
+ ///
+ public string ArrivedDate { get; set; } = string.Empty;
+
+ ///
+ /// 渠道(wechat/alipay)。
+ ///
+ public string Channel { get; set; } = string.Empty;
+}
+
+///
+/// 到账统计响应。
+///
+public sealed class FinanceSettlementStatsResponse
+{
+ ///
+ /// 今日到账。
+ ///
+ public decimal TodayArrivedAmount { get; set; }
+
+ ///
+ /// 昨日到账。
+ ///
+ public decimal YesterdayArrivedAmount { get; set; }
+
+ ///
+ /// 本月到账。
+ ///
+ public decimal CurrentMonthArrivedAmount { get; set; }
+
+ ///
+ /// 本月交易笔数。
+ ///
+ public int CurrentMonthTransactionCount { get; set; }
+}
+
+///
+/// 到账账户信息响应。
+///
+public sealed class FinanceSettlementAccountResponse
+{
+ ///
+ /// 银行名称。
+ ///
+ public string BankName { get; set; } = string.Empty;
+
+ ///
+ /// 开户名。
+ ///
+ public string BankAccountName { get; set; } = string.Empty;
+
+ ///
+ /// 脱敏银行账号。
+ ///
+ public string BankAccountNoMasked { get; set; } = string.Empty;
+
+ ///
+ /// 脱敏微信商户号。
+ ///
+ public string WechatMerchantNoMasked { get; set; } = string.Empty;
+
+ ///
+ /// 脱敏支付宝 PID。
+ ///
+ public string AlipayPidMasked { get; set; } = string.Empty;
+
+ ///
+ /// 结算周期文案。
+ ///
+ public string SettlementPeriodText { get; set; } = string.Empty;
+}
+
+///
+/// 到账列表行响应。
+///
+public sealed class FinanceSettlementListItemResponse
+{
+ ///
+ /// 到账日期。
+ ///
+ public string ArrivedDate { get; set; } = string.Empty;
+
+ ///
+ /// 渠道编码。
+ ///
+ public string Channel { get; set; } = string.Empty;
+
+ ///
+ /// 渠道文案。
+ ///
+ public string ChannelText { get; set; } = string.Empty;
+
+ ///
+ /// 交易笔数。
+ ///
+ public int TransactionCount { get; set; }
+
+ ///
+ /// 到账金额。
+ ///
+ public decimal ArrivedAmount { get; set; }
+}
+
+///
+/// 到账列表响应。
+///
+public sealed class FinanceSettlementListResultResponse
+{
+ ///
+ /// 列表项。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 总数。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+}
+
+///
+/// 到账明细行响应。
+///
+public sealed class FinanceSettlementDetailItemResponse
+{
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; set; } = string.Empty;
+
+ ///
+ /// 金额。
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// 支付时间。
+ ///
+ public string PaidAt { get; set; } = string.Empty;
+}
+
+///
+/// 到账明细响应。
+///
+public sealed class FinanceSettlementDetailResultResponse
+{
+ ///
+ /// 明细列表。
+ ///
+ public List Items { get; set; } = [];
+}
+
+///
+/// 到账导出响应。
+///
+public sealed class FinanceSettlementExportResponse
+{
+ ///
+ /// 文件名。
+ ///
+ public string FileName { get; set; } = string.Empty;
+
+ ///
+ /// 文件内容(Base64)。
+ ///
+ public string FileContentBase64 { get; set; } = string.Empty;
+
+ ///
+ /// 导出总数。
+ ///
+ public int TotalCount { get; set; }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/FinanceSettlementController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/FinanceSettlementController.cs
new file mode 100644
index 0000000..dd827c4
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/FinanceSettlementController.cs
@@ -0,0 +1,262 @@
+using System.Globalization;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+using TakeoutSaaS.Application.App.Stores.Services;
+using TakeoutSaaS.Domain.Payments.Enums;
+using TakeoutSaaS.Infrastructure.App.Persistence;
+using TakeoutSaaS.Module.Authorization.Attributes;
+using TakeoutSaaS.Shared.Abstractions.Constants;
+using TakeoutSaaS.Shared.Abstractions.Exceptions;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Web.Api;
+using TakeoutSaaS.TenantApi.Contracts.Finance;
+
+namespace TakeoutSaaS.TenantApi.Controllers;
+
+///
+/// 财务中心到账查询。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/tenant/v{version:apiVersion}/finance/settlement")]
+public sealed class FinanceSettlementController(
+ IMediator mediator,
+ TakeoutAppDbContext dbContext,
+ StoreContextService storeContextService) : BaseApiController
+{
+ private const string ViewPermission = "tenant:finance:settlement:view";
+ private const string ExportPermission = "tenant:finance:settlement:export";
+
+ ///
+ /// 查询到账统计。
+ ///
+ [HttpGet("stats")]
+ [PermissionAuthorize(ViewPermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Stats(
+ [FromQuery] FinanceSettlementStatsRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var stats = await mediator.Send(new GetFinanceSettlementStatsQuery
+ {
+ StoreId = storeId
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new FinanceSettlementStatsResponse
+ {
+ TodayArrivedAmount = stats.TodayArrivedAmount,
+ YesterdayArrivedAmount = stats.YesterdayArrivedAmount,
+ CurrentMonthArrivedAmount = stats.CurrentMonthArrivedAmount,
+ CurrentMonthTransactionCount = stats.CurrentMonthTransactionCount
+ });
+ }
+
+ ///
+ /// 查询到账账户信息。
+ ///
+ [HttpGet("account")]
+ [PermissionAuthorize(ViewPermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Account(
+ CancellationToken cancellationToken)
+ {
+ var account = await mediator.Send(new GetFinanceSettlementAccountQuery(), cancellationToken);
+ if (account is null)
+ {
+ return ApiResponse.Error(ErrorCodes.NotFound, "结算账户信息不存在");
+ }
+
+ return ApiResponse.Ok(new FinanceSettlementAccountResponse
+ {
+ BankName = account.BankName,
+ BankAccountName = account.BankAccountName,
+ BankAccountNoMasked = account.BankAccountNoMasked,
+ WechatMerchantNoMasked = account.WechatMerchantNoMasked,
+ AlipayPidMasked = account.AlipayPidMasked,
+ SettlementPeriodText = account.SettlementPeriodText
+ });
+ }
+
+ ///
+ /// 查询到账汇总列表。
+ ///
+ [HttpGet("list")]
+ [PermissionAuthorize(ViewPermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> List(
+ [FromQuery] FinanceSettlementListRequest request,
+ CancellationToken cancellationToken)
+ {
+ var parsed = await ParseFilterAsync(request, cancellationToken);
+
+ var result = await mediator.Send(new SearchFinanceSettlementListQuery
+ {
+ StoreId = parsed.StoreId,
+ StartAt = parsed.StartAt,
+ EndAt = parsed.EndAt,
+ PaymentMethod = parsed.PaymentMethod,
+ Page = Math.Max(1, request.Page),
+ PageSize = Math.Clamp(request.PageSize, 1, 200)
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new FinanceSettlementListResultResponse
+ {
+ Items = result.Items.Select(MapListItem).ToList(),
+ Total = result.Total,
+ Page = result.Page,
+ PageSize = result.PageSize
+ });
+ }
+
+ ///
+ /// 查询到账明细(展开行)。
+ ///
+ [HttpGet("detail")]
+ [PermissionAuthorize(ViewPermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Detail(
+ [FromQuery] FinanceSettlementDetailRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var arrivedDate = ParseRequiredDate(request.ArrivedDate, nameof(request.ArrivedDate));
+ var paymentMethod = ParseRequiredSettlementChannel(request.Channel);
+
+ var result = await mediator.Send(new GetFinanceSettlementDetailQuery
+ {
+ StoreId = storeId,
+ ArrivedDate = arrivedDate,
+ PaymentMethod = paymentMethod,
+ Take = 50
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new FinanceSettlementDetailResultResponse
+ {
+ Items = result.Items.Select(MapDetailItem).ToList()
+ });
+ }
+
+ ///
+ /// 导出到账汇总 CSV。
+ ///
+ [HttpGet("export")]
+ [PermissionAuthorize(ExportPermission)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Export(
+ [FromQuery] FinanceSettlementFilterRequest request,
+ CancellationToken cancellationToken)
+ {
+ var parsed = await ParseFilterAsync(request, cancellationToken);
+
+ var result = await mediator.Send(new ExportFinanceSettlementCsvQuery
+ {
+ StoreId = parsed.StoreId,
+ StartAt = parsed.StartAt,
+ EndAt = parsed.EndAt,
+ PaymentMethod = parsed.PaymentMethod
+ }, cancellationToken);
+
+ return ApiResponse.Ok(new FinanceSettlementExportResponse
+ {
+ FileName = result.FileName,
+ FileContentBase64 = result.FileContentBase64,
+ TotalCount = result.TotalCount
+ });
+ }
+
+ private async Task<(long StoreId, DateTime? StartAt, DateTime? EndAt, PaymentMethod? PaymentMethod)> ParseFilterAsync(
+ FinanceSettlementFilterRequest request,
+ CancellationToken cancellationToken)
+ {
+ var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId));
+ await EnsureStoreAccessibleAsync(storeId, cancellationToken);
+
+ var startAt = ParseDateOrNull(request.StartDate);
+ var endAt = ParseDateOrNull(request.EndDate)?.AddDays(1);
+ if (startAt.HasValue && endAt.HasValue && startAt >= endAt)
+ {
+ throw new BusinessException(ErrorCodes.BadRequest, "开始日期不能晚于结束日期");
+ }
+
+ return (storeId, startAt, endAt, ParseOptionalSettlementChannel(request.Channel));
+ }
+
+ 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 ParseRequiredDate(string? value, string parameterName)
+ {
+ return ParseDateOrNull(value)
+ ?? throw new BusinessException(ErrorCodes.BadRequest, $"{parameterName} 必填,格式为 yyyy-MM-dd");
+ }
+
+ private static DateTime? ParseDateOrNull(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return null;
+ }
+
+ if (DateTime.TryParseExact(
+ value,
+ "yyyy-MM-dd",
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out var parsed))
+ {
+ return DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
+ }
+
+ throw new BusinessException(ErrorCodes.BadRequest, "日期格式必须为 yyyy-MM-dd");
+ }
+
+ private static PaymentMethod ParseRequiredSettlementChannel(string? channel)
+ {
+ return ParseOptionalSettlementChannel(channel)
+ ?? throw new BusinessException(ErrorCodes.BadRequest, "channel 必填,仅支持 wechat 或 alipay");
+ }
+
+ private static PaymentMethod? ParseOptionalSettlementChannel(string? channel)
+ {
+ return (channel ?? string.Empty).Trim().ToLowerInvariant() switch
+ {
+ "wechat" => PaymentMethod.WeChatPay,
+ "alipay" => PaymentMethod.Alipay,
+ "" => null,
+ _ => throw new BusinessException(ErrorCodes.BadRequest, "channel 仅支持 wechat 或 alipay")
+ };
+ }
+
+ private static FinanceSettlementListItemResponse MapListItem(FinanceSettlementListItemDto source)
+ {
+ return new FinanceSettlementListItemResponse
+ {
+ ArrivedDate = source.ArrivedDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
+ Channel = source.Channel,
+ ChannelText = source.ChannelText,
+ TransactionCount = source.TransactionCount,
+ ArrivedAmount = source.ArrivedAmount
+ };
+ }
+
+ private static FinanceSettlementDetailItemResponse MapDetailItem(FinanceSettlementDetailItemDto source)
+ {
+ return new FinanceSettlementDetailItemResponse
+ {
+ OrderNo = source.OrderNo,
+ Amount = source.Amount,
+ PaidAt = source.PaidAt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Dto/FinanceSettlementDtos.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Dto/FinanceSettlementDtos.cs
new file mode 100644
index 0000000..664d8cc
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Dto/FinanceSettlementDtos.cs
@@ -0,0 +1,173 @@
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+
+///
+/// 到账查询汇总行 DTO。
+///
+public sealed class FinanceSettlementListItemDto
+{
+ ///
+ /// 到账日期(UTC 日期)。
+ ///
+ public DateTime ArrivedDate { get; set; }
+
+ ///
+ /// 渠道编码(wechat/alipay)。
+ ///
+ public string Channel { get; set; } = string.Empty;
+
+ ///
+ /// 渠道文案。
+ ///
+ public string ChannelText { get; set; } = string.Empty;
+
+ ///
+ /// 交易笔数。
+ ///
+ public int TransactionCount { get; set; }
+
+ ///
+ /// 到账金额。
+ ///
+ public decimal ArrivedAmount { get; set; }
+}
+
+///
+/// 到账查询分页结果 DTO。
+///
+public sealed class FinanceSettlementListResultDto
+{
+ ///
+ /// 列表项。
+ ///
+ public List Items { get; set; } = [];
+
+ ///
+ /// 总数。
+ ///
+ public int Total { get; set; }
+
+ ///
+ /// 当前页码。
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; set; }
+}
+
+///
+/// 到账明细行 DTO。
+///
+public sealed class FinanceSettlementDetailItemDto
+{
+ ///
+ /// 订单号。
+ ///
+ public string OrderNo { get; set; } = string.Empty;
+
+ ///
+ /// 金额。
+ ///
+ public decimal Amount { get; set; }
+
+ ///
+ /// 支付时间(UTC)。
+ ///
+ public DateTime PaidAt { get; set; }
+}
+
+///
+/// 到账明细结果 DTO。
+///
+public sealed class FinanceSettlementDetailResultDto
+{
+ ///
+ /// 明细列表。
+ ///
+ public List Items { get; set; } = [];
+}
+
+///
+/// 到账统计 DTO。
+///
+public sealed class FinanceSettlementStatsDto
+{
+ ///
+ /// 今日到账金额。
+ ///
+ public decimal TodayArrivedAmount { get; set; }
+
+ ///
+ /// 昨日到账金额。
+ ///
+ public decimal YesterdayArrivedAmount { get; set; }
+
+ ///
+ /// 本月到账金额。
+ ///
+ public decimal CurrentMonthArrivedAmount { get; set; }
+
+ ///
+ /// 本月交易笔数。
+ ///
+ public int CurrentMonthTransactionCount { get; set; }
+}
+
+///
+/// 到账账户信息 DTO。
+///
+public sealed class FinanceSettlementAccountDto
+{
+ ///
+ /// 银行名称。
+ ///
+ public string BankName { get; set; } = string.Empty;
+
+ ///
+ /// 开户名。
+ ///
+ public string BankAccountName { get; set; } = string.Empty;
+
+ ///
+ /// 脱敏银行账号。
+ ///
+ public string BankAccountNoMasked { get; set; } = string.Empty;
+
+ ///
+ /// 脱敏微信商户号。
+ ///
+ public string WechatMerchantNoMasked { get; set; } = string.Empty;
+
+ ///
+ /// 脱敏支付宝 PID。
+ ///
+ public string AlipayPidMasked { get; set; } = string.Empty;
+
+ ///
+ /// 结算周期文案。
+ ///
+ public string SettlementPeriodText { get; set; } = string.Empty;
+}
+
+///
+/// 到账导出 DTO。
+///
+public sealed class FinanceSettlementExportDto
+{
+ ///
+ /// 文件名。
+ ///
+ public string FileName { get; set; } = string.Empty;
+
+ ///
+ /// 文件内容 Base64。
+ ///
+ public string FileContentBase64 { get; set; } = string.Empty;
+
+ ///
+ /// 导出总数。
+ ///
+ public int TotalCount { get; set; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/ExportFinanceSettlementCsvQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/ExportFinanceSettlementCsvQueryHandler.cs
new file mode 100644
index 0000000..1d37ad8
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/ExportFinanceSettlementCsvQueryHandler.cs
@@ -0,0 +1,71 @@
+using System.Globalization;
+using System.Text;
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+using TakeoutSaaS.Domain.Finance.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
+
+///
+/// 到账汇总导出处理器。
+///
+public sealed class ExportFinanceSettlementCsvQueryHandler(
+ IFinanceTransactionRepository financeTransactionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(
+ ExportFinanceSettlementCsvQuery request,
+ CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var rows = await financeTransactionRepository.ListSettlementForExportAsync(
+ tenantId,
+ request.StoreId,
+ request.StartAt,
+ request.EndAt,
+ request.PaymentMethod,
+ cancellationToken);
+
+ var list = rows.Select(FinanceSettlementMapping.ToListItem).ToList();
+ var csv = BuildCsv(list);
+ return new FinanceSettlementExportDto
+ {
+ FileName = $"settlement-{request.StoreId}-{DateTime.UtcNow:yyyyMMddHHmmss}.csv",
+ FileContentBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(csv)),
+ TotalCount = list.Count
+ };
+ }
+
+ private static string BuildCsv(IReadOnlyList rows)
+ {
+ var sb = new StringBuilder();
+ sb.Append('\uFEFF');
+ sb.AppendLine("到账日期,支付渠道,交易笔数,到账金额");
+
+ foreach (var row in rows)
+ {
+ sb.AppendLine(string.Join(',',
+ Escape(row.ArrivedDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
+ Escape(row.ChannelText),
+ Escape(row.TransactionCount.ToString(CultureInfo.InvariantCulture)),
+ Escape(FinanceSettlementMapping.FormatAmount(row.ArrivedAmount))));
+ }
+
+ return sb.ToString();
+ }
+
+ private static string Escape(string? value)
+ {
+ var normalized = value ?? string.Empty;
+ if (normalized.Contains(',') || normalized.Contains('"') || normalized.Contains('\n'))
+ {
+ return $"\"{normalized.Replace("\"", "\"\"", StringComparison.Ordinal)}\"";
+ }
+
+ return normalized;
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/FinanceSettlementMapping.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/FinanceSettlementMapping.cs
new file mode 100644
index 0000000..e19333f
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/FinanceSettlementMapping.cs
@@ -0,0 +1,75 @@
+using System.Globalization;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Domain.Finance.Models;
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
+
+///
+/// 到账查询映射辅助。
+///
+internal static class FinanceSettlementMapping
+{
+ ///
+ /// 支付方式转渠道编码。
+ ///
+ public static string ToChannelCode(PaymentMethod paymentMethod)
+ {
+ return paymentMethod switch
+ {
+ PaymentMethod.WeChatPay => "wechat",
+ PaymentMethod.Alipay => "alipay",
+ _ => "unknown"
+ };
+ }
+
+ ///
+ /// 支付方式转渠道文案。
+ ///
+ public static string ToChannelText(PaymentMethod paymentMethod)
+ {
+ return paymentMethod switch
+ {
+ PaymentMethod.WeChatPay => "微信支付",
+ PaymentMethod.Alipay => "支付宝",
+ _ => "未知渠道"
+ };
+ }
+
+ ///
+ /// 映射到账汇总行。
+ ///
+ public static FinanceSettlementListItemDto ToListItem(FinanceSettlementListItemSnapshot source)
+ {
+ return new FinanceSettlementListItemDto
+ {
+ ArrivedDate = source.ArrivedDate,
+ Channel = ToChannelCode(source.PaymentMethod),
+ ChannelText = ToChannelText(source.PaymentMethod),
+ TransactionCount = source.TransactionCount,
+ ArrivedAmount = decimal.Round(source.ArrivedAmount, 2, MidpointRounding.AwayFromZero)
+ };
+ }
+
+ ///
+ /// 映射到账明细行。
+ ///
+ public static FinanceSettlementDetailItemDto ToDetailItem(FinanceSettlementDetailItemSnapshot source)
+ {
+ return new FinanceSettlementDetailItemDto
+ {
+ OrderNo = source.OrderNo,
+ Amount = decimal.Round(source.Amount, 2, MidpointRounding.AwayFromZero),
+ PaidAt = source.PaidAt
+ };
+ }
+
+ ///
+ /// 格式化金额(导出场景)。
+ ///
+ public static string FormatAmount(decimal value)
+ {
+ return decimal.Round(value, 2, MidpointRounding.AwayFromZero)
+ .ToString("0.00", CultureInfo.InvariantCulture);
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementAccountQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementAccountQueryHandler.cs
new file mode 100644
index 0000000..dc3b885
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementAccountQueryHandler.cs
@@ -0,0 +1,42 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+using TakeoutSaaS.Domain.Finance.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
+
+///
+/// 到账账户信息查询处理器。
+///
+public sealed class GetFinanceSettlementAccountQueryHandler(
+ IFinanceTransactionRepository financeTransactionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(
+ GetFinanceSettlementAccountQuery request,
+ CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var snapshot = await financeTransactionRepository.GetSettlementAccountAsync(
+ tenantId,
+ cancellationToken);
+
+ if (snapshot is null)
+ {
+ return null;
+ }
+
+ return new FinanceSettlementAccountDto
+ {
+ BankName = snapshot.BankName,
+ BankAccountName = snapshot.BankAccountName,
+ BankAccountNoMasked = snapshot.BankAccountNoMasked,
+ WechatMerchantNoMasked = snapshot.WechatMerchantNoMasked,
+ AlipayPidMasked = snapshot.AlipayPidMasked,
+ SettlementPeriodText = snapshot.SettlementPeriodText
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementDetailQueryHandler.cs
new file mode 100644
index 0000000..534b9cc
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementDetailQueryHandler.cs
@@ -0,0 +1,36 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+using TakeoutSaaS.Domain.Finance.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
+
+///
+/// 到账明细查询处理器。
+///
+public sealed class GetFinanceSettlementDetailQueryHandler(
+ IFinanceTransactionRepository financeTransactionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(
+ GetFinanceSettlementDetailQuery request,
+ CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var rows = await financeTransactionRepository.GetSettlementDetailsAsync(
+ tenantId,
+ request.StoreId,
+ request.ArrivedDate,
+ request.PaymentMethod,
+ request.Take,
+ cancellationToken);
+
+ return new FinanceSettlementDetailResultDto
+ {
+ Items = rows.Select(FinanceSettlementMapping.ToDetailItem).ToList()
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementStatsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementStatsQueryHandler.cs
new file mode 100644
index 0000000..f1f6f26
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/GetFinanceSettlementStatsQueryHandler.cs
@@ -0,0 +1,37 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+using TakeoutSaaS.Domain.Finance.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
+
+///
+/// 到账统计查询处理器。
+///
+public sealed class GetFinanceSettlementStatsQueryHandler(
+ IFinanceTransactionRepository financeTransactionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(
+ GetFinanceSettlementStatsQuery request,
+ CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var snapshot = await financeTransactionRepository.GetSettlementStatsAsync(
+ tenantId,
+ request.StoreId,
+ DateTime.UtcNow,
+ cancellationToken);
+
+ return new FinanceSettlementStatsDto
+ {
+ TodayArrivedAmount = snapshot.TodayArrivedAmount,
+ YesterdayArrivedAmount = snapshot.YesterdayArrivedAmount,
+ CurrentMonthArrivedAmount = snapshot.CurrentMonthArrivedAmount,
+ CurrentMonthTransactionCount = snapshot.CurrentMonthTransactionCount
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/SearchFinanceSettlementListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/SearchFinanceSettlementListQueryHandler.cs
new file mode 100644
index 0000000..99144f9
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Handlers/SearchFinanceSettlementListQueryHandler.cs
@@ -0,0 +1,44 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+using TakeoutSaaS.Domain.Finance.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Handlers;
+
+///
+/// 到账汇总分页查询处理器。
+///
+public sealed class SearchFinanceSettlementListQueryHandler(
+ IFinanceTransactionRepository financeTransactionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ ///
+ public async Task Handle(
+ SearchFinanceSettlementListQuery request,
+ CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var normalizedPage = Math.Max(1, request.Page);
+ var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200);
+
+ var snapshot = await financeTransactionRepository.SearchSettlementPageAsync(
+ tenantId,
+ request.StoreId,
+ request.StartAt,
+ request.EndAt,
+ request.PaymentMethod,
+ normalizedPage,
+ normalizedPageSize,
+ cancellationToken);
+
+ return new FinanceSettlementListResultDto
+ {
+ Items = snapshot.Items.Select(FinanceSettlementMapping.ToListItem).ToList(),
+ Total = snapshot.TotalCount,
+ Page = normalizedPage,
+ PageSize = normalizedPageSize
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/ExportFinanceSettlementCsvQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/ExportFinanceSettlementCsvQuery.cs
new file mode 100644
index 0000000..85e738a
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/ExportFinanceSettlementCsvQuery.cs
@@ -0,0 +1,31 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+
+///
+/// 导出到账汇总 CSV。
+///
+public sealed class ExportFinanceSettlementCsvQuery : IRequest
+{
+ ///
+ /// 门店标识。
+ ///
+ public long StoreId { get; init; }
+
+ ///
+ /// 开始时间(UTC,闭区间)。
+ ///
+ public DateTime? StartAt { get; init; }
+
+ ///
+ /// 结束时间(UTC,开区间)。
+ ///
+ public DateTime? EndAt { get; init; }
+
+ ///
+ /// 支付方式筛选。
+ ///
+ public PaymentMethod? PaymentMethod { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementAccountQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementAccountQuery.cs
new file mode 100644
index 0000000..d2ff34b
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementAccountQuery.cs
@@ -0,0 +1,11 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+
+///
+/// 查询到账账户信息。
+///
+public sealed class GetFinanceSettlementAccountQuery : IRequest
+{
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementDetailQuery.cs
new file mode 100644
index 0000000..84dfaac
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementDetailQuery.cs
@@ -0,0 +1,31 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+
+///
+/// 查询到账明细。
+///
+public sealed class GetFinanceSettlementDetailQuery : IRequest
+{
+ ///
+ /// 门店标识。
+ ///
+ public long StoreId { get; init; }
+
+ ///
+ /// 到账日期(UTC 日期)。
+ ///
+ public DateTime ArrivedDate { get; init; }
+
+ ///
+ /// 渠道(微信/支付宝)。
+ ///
+ public PaymentMethod PaymentMethod { get; init; }
+
+ ///
+ /// 限制条数。
+ ///
+ public int Take { get; init; } = 20;
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementStatsQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementStatsQuery.cs
new file mode 100644
index 0000000..d023c29
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/GetFinanceSettlementStatsQuery.cs
@@ -0,0 +1,15 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+
+///
+/// 查询到账统计。
+///
+public sealed class GetFinanceSettlementStatsQuery : IRequest
+{
+ ///
+ /// 门店标识。
+ ///
+ public long StoreId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/SearchFinanceSettlementListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/SearchFinanceSettlementListQuery.cs
new file mode 100644
index 0000000..09d0bca
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/App/Finance/Settlement/Queries/SearchFinanceSettlementListQuery.cs
@@ -0,0 +1,41 @@
+using MediatR;
+using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
+
+///
+/// 查询到账汇总分页。
+///
+public sealed class SearchFinanceSettlementListQuery : IRequest
+{
+ ///
+ /// 门店标识。
+ ///
+ public long StoreId { get; init; }
+
+ ///
+ /// 开始时间(UTC,闭区间)。
+ ///
+ public DateTime? StartAt { get; init; }
+
+ ///
+ /// 结束时间(UTC,开区间)。
+ ///
+ public DateTime? EndAt { get; init; }
+
+ ///
+ /// 支付方式筛选。
+ ///
+ public PaymentMethod? PaymentMethod { get; init; }
+
+ ///
+ /// 页码。
+ ///
+ public int Page { get; init; } = 1;
+
+ ///
+ /// 每页条数。
+ ///
+ public int PageSize { get; init; } = 20;
+}
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs
index 94de46d..bda9bdd 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Commands/SubmitTenantVerificationCommand.cs
@@ -60,6 +60,16 @@ public sealed record SubmitTenantVerificationCommand : IRequest
public string? BankName { get; init; }
+ ///
+ /// 微信商户号。
+ ///
+ public string? WeChatMerchantNo { get; init; }
+
+ ///
+ /// 支付宝 PID。
+ ///
+ public string? AlipayPid { get; init; }
+
///
/// 其他补充资料 JSON。
///
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantVerificationDto.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantVerificationDto.cs
index f925008..26bd2a6 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantVerificationDto.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Dto/TenantVerificationDto.cs
@@ -71,6 +71,16 @@ public sealed class TenantVerificationDto
///
public string? BankName { get; init; }
+ ///
+ /// 微信商户号。
+ ///
+ public string? WeChatMerchantNo { get; init; }
+
+ ///
+ /// 支付宝 PID。
+ ///
+ public string? AlipayPid { get; init; }
+
///
/// 附加资料(JSON)。
///
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs
index 93802db..d4a1141 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SubmitTenantVerificationCommandHandler.cs
@@ -54,6 +54,8 @@ public sealed class SubmitTenantVerificationCommandHandler(
profile.BankAccountName = request.BankAccountName;
profile.BankAccountNumber = request.BankAccountNumber;
profile.BankName = request.BankName;
+ profile.WeChatMerchantNo = request.WeChatMerchantNo;
+ profile.AlipayPid = request.AlipayPid;
profile.AdditionalDataJson = request.AdditionalDataJson;
profile.Status = TenantVerificationStatus.Pending;
profile.SubmittedAt = DateTime.UtcNow;
diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
index 32d0d5f..12e59cc 100644
--- a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
+++ b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs
@@ -31,6 +31,8 @@ internal static class TenantMapping
BankAccountName = profile.BankAccountName,
BankAccountNumber = profile.BankAccountNumber,
BankName = profile.BankName,
+ WeChatMerchantNo = profile.WeChatMerchantNo,
+ AlipayPid = profile.AlipayPid,
AdditionalDataJson = profile.AdditionalDataJson,
SubmittedAt = profile.SubmittedAt,
ReviewRemarks = profile.ReviewRemarks,
diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Models/FinanceSettlementRecord.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Models/FinanceSettlementRecord.cs
new file mode 100644
index 0000000..f004389
--- /dev/null
+++ b/src/Domain/TakeoutSaaS.Domain/Finance/Models/FinanceSettlementRecord.cs
@@ -0,0 +1,128 @@
+using TakeoutSaaS.Domain.Payments.Enums;
+
+namespace TakeoutSaaS.Domain.Finance.Models;
+
+///
+/// 到账查询汇总行。
+///
+public sealed record FinanceSettlementListItemSnapshot
+{
+ ///
+ /// 到账日期(UTC 日期)。
+ ///
+ public required DateTime ArrivedDate { get; init; }
+
+ ///
+ /// 支付方式。
+ ///
+ public required PaymentMethod PaymentMethod { get; init; }
+
+ ///
+ /// 交易笔数。
+ ///
+ public required int TransactionCount { get; init; }
+
+ ///
+ /// 到账金额。
+ ///
+ public required decimal ArrivedAmount { get; init; }
+}
+
+///
+/// 到账查询明细行。
+///
+public sealed record FinanceSettlementDetailItemSnapshot
+{
+ ///
+ /// 订单号。
+ ///
+ public required string OrderNo { get; init; }
+
+ ///
+ /// 支付金额。
+ ///
+ public required decimal Amount { get; init; }
+
+ ///
+ /// 支付时间(UTC)。
+ ///
+ public required DateTime PaidAt { get; init; }
+}
+
+///
+/// 到账查询分页快照。
+///
+public sealed record FinanceSettlementPageSnapshot
+{
+ ///
+ /// 列表项。
+ ///
+ public required IReadOnlyList Items { get; init; }
+
+ ///
+ /// 总数。
+ ///
+ public required int TotalCount { get; init; }
+}
+
+///
+/// 到账概览统计快照。
+///
+public sealed record FinanceSettlementStatsSnapshot
+{
+ ///
+ /// 今日到账。
+ ///
+ public required decimal TodayArrivedAmount { get; init; }
+
+ ///
+ /// 昨日到账。
+ ///
+ public required decimal YesterdayArrivedAmount { get; init; }
+
+ ///
+ /// 本月到账。
+ ///
+ public required decimal CurrentMonthArrivedAmount { get; init; }
+
+ ///
+ /// 本月交易笔数。
+ ///
+ public required int CurrentMonthTransactionCount { get; init; }
+}
+
+///
+/// 到账账户信息快照。
+///
+public sealed record FinanceSettlementAccountSnapshot
+{
+ ///
+ /// 银行名称。
+ ///
+ public required string BankName { get; init; }
+
+ ///
+ /// 开户名。
+ ///
+ public required string BankAccountName { get; init; }
+
+ ///
+ /// 脱敏银行账号。
+ ///
+ public required string BankAccountNoMasked { get; init; }
+
+ ///
+ /// 微信商户号(脱敏)。
+ ///
+ public required string WechatMerchantNoMasked { get; init; }
+
+ ///
+ /// 支付宝 PID(脱敏)。
+ ///
+ public required string AlipayPidMasked { get; init; }
+
+ ///
+ /// 结算周期文案。
+ ///
+ public required string SettlementPeriodText { get; init; }
+}
diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Repositories/IFinanceTransactionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Repositories/IFinanceTransactionRepository.cs
index 275f862..db0f5d6 100644
--- a/src/Domain/TakeoutSaaS.Domain/Finance/Repositories/IFinanceTransactionRepository.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Finance/Repositories/IFinanceTransactionRepository.cs
@@ -63,4 +63,55 @@ public interface IFinanceTransactionRepository
PaymentMethod? paymentMethod,
string? keyword,
CancellationToken cancellationToken = default);
+
+ ///
+ /// 查询到账概览统计。
+ ///
+ Task GetSettlementStatsAsync(
+ long tenantId,
+ long storeId,
+ DateTime currentUtc,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 查询到账账户信息。
+ ///
+ Task GetSettlementAccountAsync(
+ long tenantId,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 查询到账汇总分页。
+ ///
+ Task SearchSettlementPageAsync(
+ long tenantId,
+ long storeId,
+ DateTime? startAt,
+ DateTime? endAt,
+ PaymentMethod? paymentMethod,
+ int page,
+ int pageSize,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 查询到账明细。
+ ///
+ Task> GetSettlementDetailsAsync(
+ long tenantId,
+ long storeId,
+ DateTime arrivedDate,
+ PaymentMethod paymentMethod,
+ int take,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 查询到账导出数据。
+ ///
+ Task> ListSettlementForExportAsync(
+ long tenantId,
+ long storeId,
+ DateTime? startAt,
+ DateTime? endAt,
+ PaymentMethod? paymentMethod,
+ CancellationToken cancellationToken = default);
}
diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantVerificationProfile.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantVerificationProfile.cs
index 4a49fe9..d55d296 100644
--- a/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantVerificationProfile.cs
+++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Entities/TenantVerificationProfile.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Shared.Abstractions.Entities;
@@ -63,6 +64,18 @@ public sealed class TenantVerificationProfile : AuditableEntityBase
///
public string? BankName { get; set; }
+ ///
+ /// 微信商户号。
+ ///
+ [MaxLength(64)]
+ public string? WeChatMerchantNo { get; set; }
+
+ ///
+ /// 支付宝 PID。
+ ///
+ [MaxLength(64)]
+ public string? AlipayPid { get; set; }
+
///
/// 附加资料(JSON)。
///
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfFinanceTransactionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfFinanceTransactionRepository.cs
index b8e2d60..16e1807 100644
--- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfFinanceTransactionRepository.cs
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfFinanceTransactionRepository.cs
@@ -171,6 +171,203 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
return rows.Select(MapToRecord).ToList();
}
+ ///
+ public async Task GetSettlementStatsAsync(
+ long tenantId,
+ long storeId,
+ DateTime currentUtc,
+ CancellationToken cancellationToken = default)
+ {
+ var utcNow = NormalizeUtc(currentUtc);
+ var todayStart = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, 0, 0, 0, DateTimeKind.Utc);
+ var tomorrowStart = todayStart.AddDays(1);
+ var yesterdayStart = todayStart.AddDays(-1);
+ var monthStart = new DateTime(utcNow.Year, utcNow.Month, 1, 0, 0, 0, DateTimeKind.Utc);
+ var monthEnd = monthStart.AddMonths(1);
+
+ var query = BuildSettlementPaymentQuery(
+ tenantId,
+ storeId,
+ startAt: null,
+ endAt: null,
+ paymentMethod: null);
+
+ var summary = await query
+ .GroupBy(_ => 1)
+ .Select(group => new
+ {
+ TodayArrivedAmount = group
+ .Where(item => item.PaidAt >= todayStart && item.PaidAt < tomorrowStart)
+ .Sum(item => item.Amount),
+ YesterdayArrivedAmount = group
+ .Where(item => item.PaidAt >= yesterdayStart && item.PaidAt < todayStart)
+ .Sum(item => item.Amount),
+ CurrentMonthArrivedAmount = group
+ .Where(item => item.PaidAt >= monthStart && item.PaidAt < monthEnd)
+ .Sum(item => item.Amount),
+ CurrentMonthTransactionCount = group
+ .Count(item => item.PaidAt >= monthStart && item.PaidAt < monthEnd)
+ })
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (summary is null)
+ {
+ return new FinanceSettlementStatsSnapshot
+ {
+ TodayArrivedAmount = 0,
+ YesterdayArrivedAmount = 0,
+ CurrentMonthArrivedAmount = 0,
+ CurrentMonthTransactionCount = 0
+ };
+ }
+
+ return new FinanceSettlementStatsSnapshot
+ {
+ TodayArrivedAmount = decimal.Round(summary.TodayArrivedAmount, 2, MidpointRounding.AwayFromZero),
+ YesterdayArrivedAmount = decimal.Round(summary.YesterdayArrivedAmount, 2, MidpointRounding.AwayFromZero),
+ CurrentMonthArrivedAmount = decimal.Round(summary.CurrentMonthArrivedAmount, 2, MidpointRounding.AwayFromZero),
+ CurrentMonthTransactionCount = summary.CurrentMonthTransactionCount
+ };
+ }
+
+ ///
+ public async Task GetSettlementAccountAsync(
+ long tenantId,
+ CancellationToken cancellationToken = default)
+ {
+ var profile = await context.TenantVerificationProfiles
+ .AsNoTracking()
+ .Where(item => item.TenantId == tenantId && item.DeletedAt == null)
+ .Select(item => new
+ {
+ item.BankName,
+ item.BankAccountName,
+ item.BankAccountNumber,
+ item.WeChatMerchantNo,
+ item.AlipayPid
+ })
+ .FirstOrDefaultAsync(cancellationToken);
+
+ if (profile is null)
+ {
+ return null;
+ }
+
+ return new FinanceSettlementAccountSnapshot
+ {
+ BankName = (profile.BankName ?? string.Empty).Trim(),
+ BankAccountName = (profile.BankAccountName ?? string.Empty).Trim(),
+ BankAccountNoMasked = MaskBankAccountNo(profile.BankAccountNumber),
+ WechatMerchantNoMasked = MaskWechatMerchantNo(profile.WeChatMerchantNo),
+ AlipayPidMasked = MaskAlipayPid(profile.AlipayPid),
+ SettlementPeriodText = "T+1 自动到账"
+ };
+ }
+
+ ///
+ public async Task SearchSettlementPageAsync(
+ long tenantId,
+ long storeId,
+ DateTime? startAt,
+ DateTime? endAt,
+ PaymentMethod? paymentMethod,
+ int page,
+ int pageSize,
+ CancellationToken cancellationToken = default)
+ {
+ var normalizedPage = Math.Max(1, page);
+ var normalizedPageSize = Math.Clamp(pageSize, 1, 200);
+
+ var groupedQuery = BuildSettlementPaymentQuery(tenantId, storeId, startAt, endAt, paymentMethod)
+ .GroupBy(item => new { ArrivedDate = item.PaidAt.Date, item.PaymentMethod })
+ .Select(group => new FinanceSettlementListItemSnapshot
+ {
+ ArrivedDate = DateTime.SpecifyKind(group.Key.ArrivedDate, DateTimeKind.Utc),
+ PaymentMethod = group.Key.PaymentMethod,
+ TransactionCount = group.Count(),
+ ArrivedAmount = decimal.Round(group.Sum(item => item.Amount), 2, MidpointRounding.AwayFromZero)
+ });
+
+ var totalCount = await groupedQuery.CountAsync(cancellationToken);
+ if (totalCount == 0)
+ {
+ return new FinanceSettlementPageSnapshot
+ {
+ Items = [],
+ TotalCount = 0
+ };
+ }
+
+ var items = await groupedQuery
+ .OrderByDescending(item => item.ArrivedDate)
+ .ThenBy(item => item.PaymentMethod)
+ .Skip((normalizedPage - 1) * normalizedPageSize)
+ .Take(normalizedPageSize)
+ .ToListAsync(cancellationToken);
+
+ return new FinanceSettlementPageSnapshot
+ {
+ Items = items,
+ TotalCount = totalCount
+ };
+ }
+
+ ///
+ public async Task> GetSettlementDetailsAsync(
+ long tenantId,
+ long storeId,
+ DateTime arrivedDate,
+ PaymentMethod paymentMethod,
+ int take,
+ CancellationToken cancellationToken = default)
+ {
+ var arrivedDay = NormalizeUtc(arrivedDate);
+ var dayStart = new DateTime(arrivedDay.Year, arrivedDay.Month, arrivedDay.Day, 0, 0, 0, DateTimeKind.Utc);
+ var dayEnd = dayStart.AddDays(1);
+ var normalizedTake = Math.Clamp(take, 1, 200);
+
+ return await BuildSettlementPaymentQuery(
+ tenantId,
+ storeId,
+ dayStart,
+ dayEnd,
+ paymentMethod)
+ .OrderByDescending(item => item.PaidAt)
+ .ThenByDescending(item => item.PaymentRecordId)
+ .Select(item => new FinanceSettlementDetailItemSnapshot
+ {
+ OrderNo = item.OrderNo,
+ Amount = decimal.Round(item.Amount, 2, MidpointRounding.AwayFromZero),
+ PaidAt = item.PaidAt
+ })
+ .Take(normalizedTake)
+ .ToListAsync(cancellationToken);
+ }
+
+ ///
+ public async Task> ListSettlementForExportAsync(
+ long tenantId,
+ long storeId,
+ DateTime? startAt,
+ DateTime? endAt,
+ PaymentMethod? paymentMethod,
+ CancellationToken cancellationToken = default)
+ {
+ return await BuildSettlementPaymentQuery(tenantId, storeId, startAt, endAt, paymentMethod)
+ .GroupBy(item => new { ArrivedDate = item.PaidAt.Date, item.PaymentMethod })
+ .Select(group => new FinanceSettlementListItemSnapshot
+ {
+ ArrivedDate = DateTime.SpecifyKind(group.Key.ArrivedDate, DateTimeKind.Utc),
+ PaymentMethod = group.Key.PaymentMethod,
+ TransactionCount = group.Count(),
+ ArrivedAmount = decimal.Round(group.Sum(item => item.Amount), 2, MidpointRounding.AwayFromZero)
+ })
+ .OrderByDescending(item => item.ArrivedDate)
+ .ThenBy(item => item.PaymentMethod)
+ .Take(20_000)
+ .ToListAsync(cancellationToken);
+ }
+
private IQueryable BuildQuery(
long tenantId,
long storeId,
@@ -385,6 +582,50 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
return query;
}
+ private IQueryable BuildSettlementPaymentQuery(
+ long tenantId,
+ long storeId,
+ DateTime? startAt,
+ DateTime? endAt,
+ PaymentMethod? paymentMethod)
+ {
+ var query =
+ from payment in context.PaymentRecords.AsNoTracking()
+ join order in context.Orders.AsNoTracking()
+ on payment.OrderId equals order.Id
+ where payment.TenantId == tenantId
+ && order.TenantId == tenantId
+ && order.StoreId == storeId
+ && payment.Status == PaymentStatus.Paid
+ && payment.PaidAt.HasValue
+ && (payment.Method == PaymentMethod.WeChatPay || payment.Method == PaymentMethod.Alipay)
+ select new SettlementPaymentProjection
+ {
+ PaymentRecordId = payment.Id,
+ OrderNo = order.OrderNo,
+ PaymentMethod = payment.Method,
+ Amount = payment.Amount,
+ PaidAt = payment.PaidAt!.Value
+ };
+
+ if (startAt.HasValue)
+ {
+ query = query.Where(item => item.PaidAt >= startAt.Value);
+ }
+
+ if (endAt.HasValue)
+ {
+ query = query.Where(item => item.PaidAt < endAt.Value);
+ }
+
+ if (paymentMethod.HasValue)
+ {
+ query = query.Where(item => item.PaymentMethod == paymentMethod.Value);
+ }
+
+ return query;
+ }
+
private static FinanceTransactionRecord MapToRecord(TransactionProjection source)
{
return new FinanceTransactionRecord
@@ -503,4 +744,60 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
public int? PointBalanceAfterChange { get; init; }
}
+
+ private sealed class SettlementPaymentProjection
+ {
+ public required long PaymentRecordId { get; init; }
+
+ public required string OrderNo { get; init; }
+
+ public required PaymentMethod PaymentMethod { get; init; }
+
+ public required decimal Amount { get; init; }
+
+ public required DateTime PaidAt { get; init; }
+ }
+
+ private static DateTime NormalizeUtc(DateTime value)
+ {
+ return value.Kind switch
+ {
+ DateTimeKind.Utc => value,
+ DateTimeKind.Local => value.ToUniversalTime(),
+ _ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
+ };
+ }
+
+ private static string MaskBankAccountNo(string? value)
+ {
+ var digits = new string((value ?? string.Empty).Where(char.IsDigit).ToArray());
+ if (digits.Length >= 4)
+ {
+ return $"****{digits[^4..]}";
+ }
+
+ return digits;
+ }
+
+ private static string MaskWechatMerchantNo(string? value)
+ {
+ var normalized = (value ?? string.Empty).Trim();
+ if (normalized.Length >= 4)
+ {
+ return $"{normalized[..2]}{new string('x', normalized.Length - 2)}";
+ }
+
+ return normalized;
+ }
+
+ private static string MaskAlipayPid(string? value)
+ {
+ var normalized = (value ?? string.Empty).Trim();
+ if (normalized.Length > 6)
+ {
+ return $"{normalized[..4]}{new string('x', normalized.Length - 4)}";
+ }
+
+ return normalized;
+ }
}
diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304072055_AddTenantVerificationSettlementChannels.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304072055_AddTenantVerificationSettlementChannels.Designer.cs
new file mode 100644
index 0000000..052a842
--- /dev/null
+++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304072055_AddTenantVerificationSettlementChannels.Designer.cs
@@ -0,0 +1,9918 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using TakeoutSaaS.Infrastructure.App.Persistence;
+
+#nullable disable
+
+namespace TakeoutSaaS.Infrastructure.Migrations
+{
+ [DbContext(typeof(TakeoutAppDbContext))]
+ [Migration("20260304072055_AddTenantVerificationSettlementChannels")]
+ partial class AddTenantVerificationSettlementChannels
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Consumed")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ConsumerId")
+ .HasColumnType("uuid");
+
+ b.Property("Delivered")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastSequenceNumber")
+ .HasColumnType("bigint");
+
+ b.Property("LockId")
+ .HasColumnType("uuid");
+
+ b.Property("MessageId")
+ .HasColumnType("uuid");
+
+ b.Property("ReceiveCount")
+ .HasColumnType("integer");
+
+ b.Property("Received")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("bytea");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Delivered");
+
+ b.ToTable("InboxState");
+ });
+
+ modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
+ {
+ b.Property("SequenceNumber")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber"));
+
+ b.Property("Body")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ConversationId")
+ .HasColumnType("uuid");
+
+ b.Property("CorrelationId")
+ .HasColumnType("uuid");
+
+ b.Property("DestinationAddress")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("EnqueueTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FaultAddress")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Headers")
+ .HasColumnType("text");
+
+ b.Property("InboxConsumerId")
+ .HasColumnType("uuid");
+
+ b.Property("InboxMessageId")
+ .HasColumnType("uuid");
+
+ b.Property("InitiatorId")
+ .HasColumnType("uuid");
+
+ b.Property("MessageId")
+ .HasColumnType("uuid");
+
+ b.Property("MessageType")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("OutboxId")
+ .HasColumnType("uuid");
+
+ b.Property("Properties")
+ .HasColumnType("text");
+
+ b.Property("RequestId")
+ .HasColumnType("uuid");
+
+ b.Property("ResponseAddress")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("SentTime")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("SourceAddress")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("SequenceNumber");
+
+ b.HasIndex("EnqueueTime");
+
+ b.HasIndex("ExpirationTime");
+
+ b.HasIndex("OutboxId", "SequenceNumber")
+ .IsUnique();
+
+ b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
+ .IsUnique();
+
+ b.ToTable("OutboxMessage");
+ });
+
+ modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
+ {
+ b.Property("OutboxId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Created")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Delivered")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastSequenceNumber")
+ .HasColumnType("bigint");
+
+ b.Property("LockId")
+ .HasColumnType("uuid");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType("bytea");
+
+ b.HasKey("OutboxId");
+
+ b.HasIndex("Created");
+
+ b.ToTable("OutboxState");
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricAlertRule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ConditionJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasComment("触发条件 JSON。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean")
+ .HasComment("是否启用。");
+
+ b.Property("MetricDefinitionId")
+ .HasColumnType("bigint")
+ .HasComment("关联指标。");
+
+ b.Property("NotificationChannels")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasComment("通知渠道。");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasComment("告警级别。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "MetricDefinitionId", "Severity");
+
+ b.ToTable("metric_alert_rules", null, t =>
+ {
+ t.HasComment("指标告警规则。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricDefinition", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("指标编码。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DefaultAggregation")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("默认聚合方式。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("Description")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("说明。");
+
+ b.Property("DimensionsJson")
+ .HasColumnType("text")
+ .HasComment("维度描述 JSON。");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComment("指标名称。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "Code")
+ .IsUnique();
+
+ b.ToTable("metric_definitions", null, t =>
+ {
+ t.HasComment("指标定义,描述可观测的数据点。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Analytics.Entities.MetricSnapshot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("DimensionKey")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasComment("维度键(JSON)。");
+
+ b.Property("MetricDefinitionId")
+ .HasColumnType("bigint")
+ .HasComment("指标定义 ID。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("Value")
+ .HasPrecision(18, 4)
+ .HasColumnType("numeric(18,4)")
+ .HasComment("数值。");
+
+ b.Property("WindowEnd")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("统计时间窗口结束。");
+
+ b.Property("WindowStart")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("统计时间窗口开始。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "MetricDefinitionId", "DimensionKey", "WindowStart", "WindowEnd")
+ .IsUnique();
+
+ b.ToTable("metric_snapshots", null, t =>
+ {
+ t.HasComment("指标快照,用于大盘展示。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.Coupon", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("券码或序列号。");
+
+ b.Property("CouponTemplateId")
+ .HasColumnType("bigint")
+ .HasComment("模板标识。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("ExpireAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("到期时间。");
+
+ b.Property("IssuedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("发放时间。");
+
+ b.Property("OrderId")
+ .HasColumnType("bigint")
+ .HasComment("订单 ID(已使用时记录)。");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasComment("状态。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("UsedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("使用时间。");
+
+ b.Property("UserId")
+ .HasColumnType("bigint")
+ .HasComment("归属用户。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "Code")
+ .IsUnique();
+
+ b.ToTable("coupons", null, t =>
+ {
+ t.HasComment("用户领取的券。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.CouponTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AllowStack")
+ .HasColumnType("boolean")
+ .HasComment("是否允许叠加其他优惠。");
+
+ b.Property("ChannelsJson")
+ .HasColumnType("text")
+ .HasComment("发放渠道(JSON)。");
+
+ b.Property("ClaimedQuantity")
+ .HasColumnType("integer")
+ .HasComment("已领取数量。");
+
+ b.Property("CouponType")
+ .HasColumnType("integer")
+ .HasComment("券类型。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("Description")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("备注。");
+
+ b.Property("DiscountCap")
+ .HasColumnType("numeric")
+ .HasComment("折扣上限(针对折扣券)。");
+
+ b.Property("MinimumSpend")
+ .HasColumnType("numeric")
+ .HasComment("最低消费门槛。");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComment("模板名称。");
+
+ b.Property("PerUserLimit")
+ .HasColumnType("integer")
+ .HasComment("每位用户可领取上限。");
+
+ b.Property("ProductScopeJson")
+ .HasColumnType("text")
+ .HasComment("适用品类或商品范围(JSON)。");
+
+ b.Property("RelativeValidDays")
+ .HasColumnType("integer")
+ .HasComment("有效天数(相对发放时间)。");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasComment("状态。");
+
+ b.Property("StoreScopeJson")
+ .HasColumnType("text")
+ .HasComment("适用门店 ID 集合(JSON)。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("TotalQuantity")
+ .HasColumnType("integer")
+ .HasComment("总发放数量上限。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("ValidFrom")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("可用开始时间。");
+
+ b.Property("ValidTo")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("可用结束时间。");
+
+ b.Property("Value")
+ .HasColumnType("numeric")
+ .HasComment("面值或折扣额度。");
+
+ b.HasKey("Id");
+
+ b.ToTable("coupon_templates", null, t =>
+ {
+ t.HasComment("优惠券模板。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerCouponRule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CouponType")
+ .HasColumnType("integer")
+ .HasComment("券类型。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("MinimumSpend")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasComment("使用门槛。");
+
+ b.Property("Scene")
+ .HasColumnType("integer")
+ .HasComment("券规则场景。");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer")
+ .HasComment("排序值(同场景内递增)。");
+
+ b.Property("StoreId")
+ .HasColumnType("bigint")
+ .HasComment("门店 ID。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("ValidDays")
+ .HasColumnType("integer")
+ .HasComment("有效期天数。");
+
+ b.Property("Value")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasComment("面值或折扣值。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "StoreId", "Scene", "SortOrder");
+
+ b.ToTable("new_customer_coupon_rules", null, t =>
+ {
+ t.HasComment("新客有礼券规则。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerGiftSetting", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("DirectMinimumSpend")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasComment("首单直减门槛金额。");
+
+ b.Property("DirectReduceAmount")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasComment("首单直减金额。");
+
+ b.Property("GiftEnabled")
+ .HasColumnType("boolean")
+ .HasComment("是否开启新客礼包。");
+
+ b.Property("GiftType")
+ .HasColumnType("integer")
+ .HasComment("礼包类型。");
+
+ b.Property("InviteEnabled")
+ .HasColumnType("boolean")
+ .HasComment("是否开启老带新分享。");
+
+ b.Property("ShareChannelsJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasComment("分享渠道(JSON)。");
+
+ b.Property("StoreId")
+ .HasColumnType("bigint")
+ .HasComment("门店 ID。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "StoreId")
+ .IsUnique();
+
+ b.ToTable("new_customer_gift_settings", null, t =>
+ {
+ t.HasComment("新客有礼门店配置。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerGrowthRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("CustomerKey")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("顾客业务唯一键。");
+
+ b.Property("CustomerName")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("顾客展示名。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("FirstOrderAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("首单时间。");
+
+ b.Property("GiftClaimedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("礼包领取时间。");
+
+ b.Property("RegisteredAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("注册时间。");
+
+ b.Property("SourceChannel")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("渠道来源。");
+
+ b.Property("StoreId")
+ .HasColumnType("bigint")
+ .HasComment("门店 ID。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "StoreId", "CustomerKey")
+ .IsUnique();
+
+ b.HasIndex("TenantId", "StoreId", "RegisteredAt");
+
+ b.ToTable("new_customer_growth_records", null, t =>
+ {
+ t.HasComment("新客成长记录。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.NewCustomerInviteRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("InviteTime")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("邀请时间。");
+
+ b.Property("InviteeName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("被邀请人展示名。");
+
+ b.Property("InviterName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasComment("邀请人展示名。");
+
+ b.Property("OrderStatus")
+ .HasColumnType("integer")
+ .HasComment("订单状态。");
+
+ b.Property("RewardIssuedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("奖励发放时间。");
+
+ b.Property("RewardStatus")
+ .HasColumnType("integer")
+ .HasComment("奖励状态。");
+
+ b.Property("SourceChannel")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasComment("邀请来源渠道。");
+
+ b.Property("StoreId")
+ .HasColumnType("bigint")
+ .HasComment("门店 ID。");
+
+ b.Property("TenantId")
+ .HasColumnType("bigint")
+ .HasComment("所属租户 ID。");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("最近一次更新时间(UTC),从未更新时为 null。");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("bigint")
+ .HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TenantId", "StoreId", "InviteTime");
+
+ b.ToTable("new_customer_invite_records", null, t =>
+ {
+ t.HasComment("新客邀请记录。");
+ });
+ });
+
+ modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PromotionCampaign", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasComment("实体唯一标识。");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AudienceDescription")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("目标人群描述。");
+
+ b.Property("BannerUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)")
+ .HasComment("营销素材(如 banner)。");
+
+ b.Property("Budget")
+ .HasColumnType("numeric")
+ .HasComment("预算金额。");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("创建时间(UTC)。");
+
+ b.Property("CreatedBy")
+ .HasColumnType("bigint")
+ .HasComment("创建人用户标识,匿名或系统操作时为 null。");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("软删除时间(UTC),未删除时为 null。");
+
+ b.Property("DeletedBy")
+ .HasColumnType("bigint")
+ .HasComment("删除人用户标识(软删除),未删除时为 null。");
+
+ b.Property("EndAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("结束时间。");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasComment("活动名称。");
+
+ b.Property("PromotionType")
+ .HasColumnType("integer")
+ .HasComment("活动类型。");
+
+ b.Property("RulesJson")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasComment("活动规则 JSON。");
+
+ b.Property("StartAt")
+ .HasColumnType("timestamp with time zone")
+ .HasComment("开始时间。");
+
+ b.Property