Compare commits
8 Commits
dev
...
fa7b006373
| Author | SHA1 | Date | |
|---|---|---|---|
| fa7b006373 | |||
| c8359c5fc3 | |||
| 59ebe70ed3 | |||
| 76366cbc30 | |||
| b0bb87d97c | |||
| 1efa392f36 | |||
| b57b3ab228 | |||
| a88ca4056c |
@@ -0,0 +1,247 @@
|
|||||||
|
namespace TakeoutSaaS.TenantApi.Contracts.Finance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账统计请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementStatsRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账筛选请求。
|
||||||
|
/// </summary>
|
||||||
|
public class FinanceSettlementFilterRequest
|
||||||
|
{
|
||||||
|
/// <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>
|
||||||
|
/// 渠道(wechat/alipay)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Channel { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账列表请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementListRequest : FinanceSettlementFilterRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账明细请求。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementDetailRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店 ID。
|
||||||
|
/// </summary>
|
||||||
|
public string StoreId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账日期(yyyy-MM-dd)。
|
||||||
|
/// </summary>
|
||||||
|
public string ArrivedDate { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道(wechat/alipay)。
|
||||||
|
/// </summary>
|
||||||
|
public string Channel { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账统计响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementStatsResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 今日到账。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TodayArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 昨日到账。
|
||||||
|
/// </summary>
|
||||||
|
public decimal YesterdayArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月到账。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CurrentMonthArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月交易笔数。
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentMonthTransactionCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账账户信息响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementAccountResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 银行名称。
|
||||||
|
/// </summary>
|
||||||
|
public string BankName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开户名。
|
||||||
|
/// </summary>
|
||||||
|
public string BankAccountName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 脱敏银行账号。
|
||||||
|
/// </summary>
|
||||||
|
public string BankAccountNoMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 脱敏微信商户号。
|
||||||
|
/// </summary>
|
||||||
|
public string WechatMerchantNoMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 脱敏支付宝 PID。
|
||||||
|
/// </summary>
|
||||||
|
public string AlipayPidMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结算周期文案。
|
||||||
|
/// </summary>
|
||||||
|
public string SettlementPeriodText { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账列表行响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementListItemResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 到账日期。
|
||||||
|
/// </summary>
|
||||||
|
public string ArrivedDate { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道编码。
|
||||||
|
/// </summary>
|
||||||
|
public string Channel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道文案。
|
||||||
|
/// </summary>
|
||||||
|
public string ChannelText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易笔数。
|
||||||
|
/// </summary>
|
||||||
|
public int TransactionCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ArrivedAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账列表响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementListResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public List<FinanceSettlementListItemResponse> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总数。
|
||||||
|
/// </summary>
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账明细行响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementDetailItemResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间。
|
||||||
|
/// </summary>
|
||||||
|
public string PaidAt { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账明细响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementDetailResultResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 明细列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<FinanceSettlementDetailItemResponse> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账导出响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementExportResponse
|
||||||
|
{
|
||||||
|
/// <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,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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 财务中心到账查询。
|
||||||
|
/// </summary>
|
||||||
|
[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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账统计。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("stats")]
|
||||||
|
[PermissionAuthorize(ViewPermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementStatsResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<FinanceSettlementStatsResponse>> 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<FinanceSettlementStatsResponse>.Ok(new FinanceSettlementStatsResponse
|
||||||
|
{
|
||||||
|
TodayArrivedAmount = stats.TodayArrivedAmount,
|
||||||
|
YesterdayArrivedAmount = stats.YesterdayArrivedAmount,
|
||||||
|
CurrentMonthArrivedAmount = stats.CurrentMonthArrivedAmount,
|
||||||
|
CurrentMonthTransactionCount = stats.CurrentMonthTransactionCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账账户信息。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("account")]
|
||||||
|
[PermissionAuthorize(ViewPermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementAccountResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<FinanceSettlementAccountResponse>> Account(
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var account = await mediator.Send(new GetFinanceSettlementAccountQuery(), cancellationToken);
|
||||||
|
if (account is null)
|
||||||
|
{
|
||||||
|
return ApiResponse<FinanceSettlementAccountResponse>.Error(ErrorCodes.NotFound, "结算账户信息不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<FinanceSettlementAccountResponse>.Ok(new FinanceSettlementAccountResponse
|
||||||
|
{
|
||||||
|
BankName = account.BankName,
|
||||||
|
BankAccountName = account.BankAccountName,
|
||||||
|
BankAccountNoMasked = account.BankAccountNoMasked,
|
||||||
|
WechatMerchantNoMasked = account.WechatMerchantNoMasked,
|
||||||
|
AlipayPidMasked = account.AlipayPidMasked,
|
||||||
|
SettlementPeriodText = account.SettlementPeriodText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账汇总列表。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
[PermissionAuthorize(ViewPermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementListResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<FinanceSettlementListResultResponse>> 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<FinanceSettlementListResultResponse>.Ok(new FinanceSettlementListResultResponse
|
||||||
|
{
|
||||||
|
Items = result.Items.Select(MapListItem).ToList(),
|
||||||
|
Total = result.Total,
|
||||||
|
Page = result.Page,
|
||||||
|
PageSize = result.PageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账明细(展开行)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("detail")]
|
||||||
|
[PermissionAuthorize(ViewPermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementDetailResultResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<FinanceSettlementDetailResultResponse>> 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<FinanceSettlementDetailResultResponse>.Ok(new FinanceSettlementDetailResultResponse
|
||||||
|
{
|
||||||
|
Items = result.Items.Select(MapDetailItem).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出到账汇总 CSV。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("export")]
|
||||||
|
[PermissionAuthorize(ExportPermission)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<FinanceSettlementExportResponse>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<FinanceSettlementExportResponse>> 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<FinanceSettlementExportResponse>.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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
namespace TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账查询汇总行 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementListItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 到账日期(UTC 日期)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ArrivedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道编码(wechat/alipay)。
|
||||||
|
/// </summary>
|
||||||
|
public string Channel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道文案。
|
||||||
|
/// </summary>
|
||||||
|
public string ChannelText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易笔数。
|
||||||
|
/// </summary>
|
||||||
|
public int TransactionCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal ArrivedAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账查询分页结果 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementListResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public List<FinanceSettlementListItemDto> Items { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总数。
|
||||||
|
/// </summary>
|
||||||
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账明细行 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementDetailItemDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public string OrderNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime PaidAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账明细结果 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementDetailResultDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 明细列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<FinanceSettlementDetailItemDto> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账统计 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementStatsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 今日到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal TodayArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 昨日到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal YesterdayArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public decimal CurrentMonthArrivedAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月交易笔数。
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentMonthTransactionCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账账户信息 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementAccountDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 银行名称。
|
||||||
|
/// </summary>
|
||||||
|
public string BankName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开户名。
|
||||||
|
/// </summary>
|
||||||
|
public string BankAccountName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 脱敏银行账号。
|
||||||
|
/// </summary>
|
||||||
|
public string BankAccountNoMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 脱敏微信商户号。
|
||||||
|
/// </summary>
|
||||||
|
public string WechatMerchantNoMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 脱敏支付宝 PID。
|
||||||
|
/// </summary>
|
||||||
|
public string AlipayPidMasked { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结算周期文案。
|
||||||
|
/// </summary>
|
||||||
|
public string SettlementPeriodText { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账导出 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class FinanceSettlementExportDto
|
||||||
|
{
|
||||||
|
/// <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,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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账汇总导出处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportFinanceSettlementCsvQueryHandler(
|
||||||
|
IFinanceTransactionRepository financeTransactionRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<ExportFinanceSettlementCsvQuery, FinanceSettlementExportDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementExportDto> 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<FinanceSettlementListItemDto> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账查询映射辅助。
|
||||||
|
/// </summary>
|
||||||
|
internal static class FinanceSettlementMapping
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式转渠道编码。
|
||||||
|
/// </summary>
|
||||||
|
public static string ToChannelCode(PaymentMethod paymentMethod)
|
||||||
|
{
|
||||||
|
return paymentMethod switch
|
||||||
|
{
|
||||||
|
PaymentMethod.WeChatPay => "wechat",
|
||||||
|
PaymentMethod.Alipay => "alipay",
|
||||||
|
_ => "unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式转渠道文案。
|
||||||
|
/// </summary>
|
||||||
|
public static string ToChannelText(PaymentMethod paymentMethod)
|
||||||
|
{
|
||||||
|
return paymentMethod switch
|
||||||
|
{
|
||||||
|
PaymentMethod.WeChatPay => "微信支付",
|
||||||
|
PaymentMethod.Alipay => "支付宝",
|
||||||
|
_ => "未知渠道"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 映射到账汇总行。
|
||||||
|
/// </summary>
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 映射到账明细行。
|
||||||
|
/// </summary>
|
||||||
|
public static FinanceSettlementDetailItemDto ToDetailItem(FinanceSettlementDetailItemSnapshot source)
|
||||||
|
{
|
||||||
|
return new FinanceSettlementDetailItemDto
|
||||||
|
{
|
||||||
|
OrderNo = source.OrderNo,
|
||||||
|
Amount = decimal.Round(source.Amount, 2, MidpointRounding.AwayFromZero),
|
||||||
|
PaidAt = source.PaidAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化金额(导出场景)。
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatAmount(decimal value)
|
||||||
|
{
|
||||||
|
return decimal.Round(value, 2, MidpointRounding.AwayFromZero)
|
||||||
|
.ToString("0.00", CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账账户信息查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetFinanceSettlementAccountQueryHandler(
|
||||||
|
IFinanceTransactionRepository financeTransactionRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetFinanceSettlementAccountQuery, FinanceSettlementAccountDto?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementAccountDto?> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账明细查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetFinanceSettlementDetailQueryHandler(
|
||||||
|
IFinanceTransactionRepository financeTransactionRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetFinanceSettlementDetailQuery, FinanceSettlementDetailResultDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementDetailResultDto> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账统计查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetFinanceSettlementStatsQueryHandler(
|
||||||
|
IFinanceTransactionRepository financeTransactionRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<GetFinanceSettlementStatsQuery, FinanceSettlementStatsDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementStatsDto> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账汇总分页查询处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchFinanceSettlementListQueryHandler(
|
||||||
|
IFinanceTransactionRepository financeTransactionRepository,
|
||||||
|
ITenantProvider tenantProvider)
|
||||||
|
: IRequestHandler<SearchFinanceSettlementListQuery, FinanceSettlementListResultDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementListResultDto> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导出到账汇总 CSV。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExportFinanceSettlementCsvQuery : IRequest<FinanceSettlementExportDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间(UTC,闭区间)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束时间(UTC,开区间)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式筛选。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod? PaymentMethod { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账账户信息。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetFinanceSettlementAccountQuery : IRequest<FinanceSettlementAccountDto?>
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账明细。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetFinanceSettlementDetailQuery : IRequest<FinanceSettlementDetailResultDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账日期(UTC 日期)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ArrivedDate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道(微信/支付宝)。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod PaymentMethod { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 限制条数。
|
||||||
|
/// </summary>
|
||||||
|
public int Take { get; init; } = 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.App.Finance.Settlement.Dto;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.App.Finance.Settlement.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账统计。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class GetFinanceSettlementStatsQuery : IRequest<FinanceSettlementStatsDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账汇总分页。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SearchFinanceSettlementListQuery : IRequest<FinanceSettlementListResultDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 门店标识。
|
||||||
|
/// </summary>
|
||||||
|
public long StoreId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始时间(UTC,闭区间)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? StartAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束时间(UTC,开区间)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EndAt { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式筛选。
|
||||||
|
/// </summary>
|
||||||
|
public PaymentMethod? PaymentMethod { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 页码。
|
||||||
|
/// </summary>
|
||||||
|
public int Page { get; init; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数。
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; init; } = 20;
|
||||||
|
}
|
||||||
@@ -60,6 +60,16 @@ public sealed record SubmitTenantVerificationCommand : IRequest<TenantVerificati
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? BankName { get; init; }
|
public string? BankName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信商户号。
|
||||||
|
/// </summary>
|
||||||
|
public string? WeChatMerchantNo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝 PID。
|
||||||
|
/// </summary>
|
||||||
|
public string? AlipayPid { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 其他补充资料 JSON。
|
/// 其他补充资料 JSON。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -71,6 +71,16 @@ public sealed class TenantVerificationDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? BankName { get; init; }
|
public string? BankName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信商户号。
|
||||||
|
/// </summary>
|
||||||
|
public string? WeChatMerchantNo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝 PID。
|
||||||
|
/// </summary>
|
||||||
|
public string? AlipayPid { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 附加资料(JSON)。
|
/// 附加资料(JSON)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ public sealed class SubmitTenantVerificationCommandHandler(
|
|||||||
profile.BankAccountName = request.BankAccountName;
|
profile.BankAccountName = request.BankAccountName;
|
||||||
profile.BankAccountNumber = request.BankAccountNumber;
|
profile.BankAccountNumber = request.BankAccountNumber;
|
||||||
profile.BankName = request.BankName;
|
profile.BankName = request.BankName;
|
||||||
|
profile.WeChatMerchantNo = request.WeChatMerchantNo;
|
||||||
|
profile.AlipayPid = request.AlipayPid;
|
||||||
profile.AdditionalDataJson = request.AdditionalDataJson;
|
profile.AdditionalDataJson = request.AdditionalDataJson;
|
||||||
profile.Status = TenantVerificationStatus.Pending;
|
profile.Status = TenantVerificationStatus.Pending;
|
||||||
profile.SubmittedAt = DateTime.UtcNow;
|
profile.SubmittedAt = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ internal static class TenantMapping
|
|||||||
BankAccountName = profile.BankAccountName,
|
BankAccountName = profile.BankAccountName,
|
||||||
BankAccountNumber = profile.BankAccountNumber,
|
BankAccountNumber = profile.BankAccountNumber,
|
||||||
BankName = profile.BankName,
|
BankName = profile.BankName,
|
||||||
|
WeChatMerchantNo = profile.WeChatMerchantNo,
|
||||||
|
AlipayPid = profile.AlipayPid,
|
||||||
AdditionalDataJson = profile.AdditionalDataJson,
|
AdditionalDataJson = profile.AdditionalDataJson,
|
||||||
SubmittedAt = profile.SubmittedAt,
|
SubmittedAt = profile.SubmittedAt,
|
||||||
ReviewRemarks = profile.ReviewRemarks,
|
ReviewRemarks = profile.ReviewRemarks,
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using TakeoutSaaS.Domain.Payments.Enums;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Finance.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账查询汇总行。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FinanceSettlementListItemSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 到账日期(UTC 日期)。
|
||||||
|
/// </summary>
|
||||||
|
public required DateTime ArrivedDate { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付方式。
|
||||||
|
/// </summary>
|
||||||
|
public required PaymentMethod PaymentMethod { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 交易笔数。
|
||||||
|
/// </summary>
|
||||||
|
public required int TransactionCount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账金额。
|
||||||
|
/// </summary>
|
||||||
|
public required decimal ArrivedAmount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账查询明细行。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FinanceSettlementDetailItemSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 订单号。
|
||||||
|
/// </summary>
|
||||||
|
public required string OrderNo { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付金额。
|
||||||
|
/// </summary>
|
||||||
|
public required decimal Amount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public required DateTime PaidAt { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账查询分页快照。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FinanceSettlementPageSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 列表项。
|
||||||
|
/// </summary>
|
||||||
|
public required IReadOnlyList<FinanceSettlementListItemSnapshot> Items { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总数。
|
||||||
|
/// </summary>
|
||||||
|
public required int TotalCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账概览统计快照。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FinanceSettlementStatsSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 今日到账。
|
||||||
|
/// </summary>
|
||||||
|
public required decimal TodayArrivedAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 昨日到账。
|
||||||
|
/// </summary>
|
||||||
|
public required decimal YesterdayArrivedAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月到账。
|
||||||
|
/// </summary>
|
||||||
|
public required decimal CurrentMonthArrivedAmount { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 本月交易笔数。
|
||||||
|
/// </summary>
|
||||||
|
public required int CurrentMonthTransactionCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 到账账户信息快照。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FinanceSettlementAccountSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 银行名称。
|
||||||
|
/// </summary>
|
||||||
|
public required string BankName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开户名。
|
||||||
|
/// </summary>
|
||||||
|
public required string BankAccountName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 脱敏银行账号。
|
||||||
|
/// </summary>
|
||||||
|
public required string BankAccountNoMasked { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信商户号(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public required string WechatMerchantNoMasked { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝 PID(脱敏)。
|
||||||
|
/// </summary>
|
||||||
|
public required string AlipayPidMasked { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结算周期文案。
|
||||||
|
/// </summary>
|
||||||
|
public required string SettlementPeriodText { get; init; }
|
||||||
|
}
|
||||||
@@ -63,4 +63,55 @@ public interface IFinanceTransactionRepository
|
|||||||
PaymentMethod? paymentMethod,
|
PaymentMethod? paymentMethod,
|
||||||
string? keyword,
|
string? keyword,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账概览统计。
|
||||||
|
/// </summary>
|
||||||
|
Task<FinanceSettlementStatsSnapshot> GetSettlementStatsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime currentUtc,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账账户信息。
|
||||||
|
/// </summary>
|
||||||
|
Task<FinanceSettlementAccountSnapshot?> GetSettlementAccountAsync(
|
||||||
|
long tenantId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账汇总分页。
|
||||||
|
/// </summary>
|
||||||
|
Task<FinanceSettlementPageSnapshot> SearchSettlementPageAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime? startAt,
|
||||||
|
DateTime? endAt,
|
||||||
|
PaymentMethod? paymentMethod,
|
||||||
|
int page,
|
||||||
|
int pageSize,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账明细。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<FinanceSettlementDetailItemSnapshot>> GetSettlementDetailsAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime arrivedDate,
|
||||||
|
PaymentMethod paymentMethod,
|
||||||
|
int take,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询到账导出数据。
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<FinanceSettlementListItemSnapshot>> ListSettlementForExportAsync(
|
||||||
|
long tenantId,
|
||||||
|
long storeId,
|
||||||
|
DateTime? startAt,
|
||||||
|
DateTime? endAt,
|
||||||
|
PaymentMethod? paymentMethod,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using TakeoutSaaS.Domain.Tenants.Enums;
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
@@ -63,6 +64,18 @@ public sealed class TenantVerificationProfile : AuditableEntityBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? BankName { get; set; }
|
public string? BankName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信商户号。
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string? WeChatMerchantNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 支付宝 PID。
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(64)]
|
||||||
|
public string? AlipayPid { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 附加资料(JSON)。
|
/// 附加资料(JSON)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -171,6 +171,203 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
|
|||||||
return rows.Select(MapToRecord).ToList();
|
return rows.Select(MapToRecord).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementStatsSnapshot> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementAccountSnapshot?> 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 自动到账"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<FinanceSettlementPageSnapshot> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<FinanceSettlementDetailItemSnapshot>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<FinanceSettlementListItemSnapshot>> 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<TransactionProjection> BuildQuery(
|
private IQueryable<TransactionProjection> BuildQuery(
|
||||||
long tenantId,
|
long tenantId,
|
||||||
long storeId,
|
long storeId,
|
||||||
@@ -385,6 +582,50 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IQueryable<SettlementPaymentProjection> 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)
|
private static FinanceTransactionRecord MapToRecord(TransactionProjection source)
|
||||||
{
|
{
|
||||||
return new FinanceTransactionRecord
|
return new FinanceTransactionRecord
|
||||||
@@ -503,4 +744,60 @@ public sealed class EfFinanceTransactionRepository(TakeoutAppDbContext context)
|
|||||||
|
|
||||||
public int? PointBalanceAfterChange { get; init; }
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddTenantVerificationSettlementChannels : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "AlipayPid",
|
||||||
|
table: "tenant_verification_profiles",
|
||||||
|
type: "character varying(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true,
|
||||||
|
comment: "支付宝 PID。");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "WeChatMerchantNo",
|
||||||
|
table: "tenant_verification_profiles",
|
||||||
|
type: "character varying(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true,
|
||||||
|
comment: "微信商户号。");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AlipayPid",
|
||||||
|
table: "tenant_verification_profiles");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WeChatMerchantNo",
|
||||||
|
table: "tenant_verification_profiles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9711,6 +9711,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasComment("附加资料(JSON)。");
|
.HasComment("附加资料(JSON)。");
|
||||||
|
|
||||||
|
b.Property<string>("AlipayPid")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("支付宝 PID。");
|
||||||
|
|
||||||
b.Property<string>("BankAccountName")
|
b.Property<string>("BankAccountName")
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("character varying(128)")
|
.HasColumnType("character varying(128)")
|
||||||
@@ -9810,6 +9815,11 @@ namespace TakeoutSaaS.Infrastructure.Migrations
|
|||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
|
||||||
|
|
||||||
|
b.Property<string>("WeChatMerchantNo")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasComment("微信商户号。");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("TenantId")
|
b.HasIndex("TenantId")
|
||||||
|
|||||||
Reference in New Issue
Block a user