diff --git a/TakeoutSaaS.Docs b/TakeoutSaaS.Docs index 2bceb20..6daa444 160000 --- a/TakeoutSaaS.Docs +++ b/TakeoutSaaS.Docs @@ -1 +1 @@ -Subproject commit 2bceb20baed29fdcc48774b6b65fb9121e806b6f +Subproject commit 6daa444c5efd2106daab7cbfd82e14b650a36ab7 diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Finance/FinanceBusinessReportContracts.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Finance/FinanceBusinessReportContracts.cs new file mode 100644 index 0000000..b0884b6 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Finance/FinanceBusinessReportContracts.cs @@ -0,0 +1,285 @@ +namespace TakeoutSaaS.TenantApi.Contracts.Finance; + +/// +/// 经营报表列表请求。 +/// +public sealed class FinanceBusinessReportListRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 周期类型(daily/weekly/monthly)。 + /// + public string? PeriodType { get; set; } = "daily"; + + /// + /// 页码。 + /// + public int Page { get; set; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } = 20; +} + +/// +/// 经营报表详情请求。 +/// +public sealed class FinanceBusinessReportDetailRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 报表 ID。 + /// + public string ReportId { get; set; } = string.Empty; +} + +/// +/// 经营报表批量导出请求。 +/// +public sealed class FinanceBusinessReportBatchExportRequest +{ + /// + /// 门店 ID。 + /// + public string StoreId { get; set; } = string.Empty; + + /// + /// 周期类型(daily/weekly/monthly)。 + /// + public string? PeriodType { get; set; } = "daily"; + + /// + /// 页码。 + /// + public int Page { get; set; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } = 20; +} + +/// +/// 经营报表列表行响应。 +/// +public sealed class FinanceBusinessReportListItemResponse +{ + /// + /// 报表 ID。 + /// + public string ReportId { get; set; } = string.Empty; + + /// + /// 日期文案。 + /// + public string DateText { get; set; } = string.Empty; + + /// + /// 营业额。 + /// + public decimal RevenueAmount { get; set; } + + /// + /// 订单数。 + /// + public int OrderCount { get; set; } + + /// + /// 客单价。 + /// + public decimal AverageOrderValue { get; set; } + + /// + /// 退款率(百分数)。 + /// + public decimal RefundRatePercent { get; set; } + + /// + /// 成本总额。 + /// + public decimal CostTotalAmount { get; set; } + + /// + /// 净利润。 + /// + public decimal NetProfitAmount { get; set; } + + /// + /// 利润率(百分数)。 + /// + public decimal ProfitRatePercent { get; set; } + + /// + /// 状态编码。 + /// + public string Status { get; set; } = string.Empty; + + /// + /// 状态文案。 + /// + public string StatusText { get; set; } = string.Empty; + + /// + /// 是否可下载。 + /// + public bool CanDownload { get; set; } +} + +/// +/// 经营报表列表响应。 +/// +public sealed class FinanceBusinessReportListResultResponse +{ + /// + /// 列表。 + /// + public List Items { get; set; } = []; + + /// + /// 总数。 + /// + public int Total { get; set; } + + /// + /// 页码。 + /// + public int Page { get; set; } + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } +} + +/// +/// KPI 响应项。 +/// +public sealed class FinanceBusinessReportKpiResponse +{ + /// + /// 指标键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 指标名称。 + /// + public string Label { get; set; } = string.Empty; + + /// + /// 指标值文案。 + /// + public string ValueText { get; set; } = string.Empty; + + /// + /// 同比变化率(百分数)。 + /// + public decimal YoyChangeRate { get; set; } + + /// + /// 环比变化率(百分数)。 + /// + public decimal MomChangeRate { get; set; } +} + +/// +/// 明细行响应项。 +/// +public sealed class FinanceBusinessReportBreakdownItemResponse +{ + /// + /// 明细键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 明细名称。 + /// + public string Label { get; set; } = string.Empty; + + /// + /// 金额。 + /// + public decimal Amount { get; set; } + + /// + /// 占比(百分数)。 + /// + public decimal RatioPercent { get; set; } +} + +/// +/// 经营报表详情响应。 +/// +public sealed class FinanceBusinessReportDetailResponse +{ + /// + /// 报表 ID。 + /// + public string ReportId { get; set; } = string.Empty; + + /// + /// 标题。 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 周期类型编码。 + /// + public string PeriodType { get; set; } = string.Empty; + + /// + /// 状态编码。 + /// + public string Status { get; set; } = string.Empty; + + /// + /// 状态文案。 + /// + public string StatusText { get; set; } = string.Empty; + + /// + /// KPI 列表。 + /// + public List Kpis { get; set; } = []; + + /// + /// 收入明细(按渠道)。 + /// + public List IncomeBreakdowns { get; set; } = []; + + /// + /// 成本明细(按类别)。 + /// + public List CostBreakdowns { get; set; } = []; +} + +/// +/// 经营报表导出响应。 +/// +public sealed class FinanceBusinessReportExportResponse +{ + /// + /// 文件名。 + /// + 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/FinanceReportController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/FinanceReportController.cs new file mode 100644 index 0000000..a2cfc81 --- /dev/null +++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/FinanceReportController.cs @@ -0,0 +1,250 @@ +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; +using TakeoutSaaS.Application.App.Stores.Services; +using TakeoutSaaS.Domain.Finance.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/report")] +public sealed class FinanceReportController( + IMediator mediator, + TakeoutAppDbContext dbContext, + StoreContextService storeContextService) : BaseApiController +{ + private const string ViewPermission = "tenant:statistics:report:view"; + private const string ExportPermission = "tenant:statistics:report:export"; + + /// + /// 查询经营报表列表。 + /// + [HttpGet("list")] + [PermissionAuthorize(ViewPermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> List( + [FromQuery] FinanceBusinessReportListRequest request, + CancellationToken cancellationToken) + { + // 1. 校验门店访问权限并解析查询参数。 + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + var periodType = ParsePeriodType(request.PeriodType); + + // 2. 发起查询并返回结果。 + var result = await mediator.Send(new SearchFinanceBusinessReportListQuery + { + StoreId = storeId, + PeriodType = periodType, + Page = Math.Max(1, request.Page), + PageSize = Math.Clamp(request.PageSize, 1, 200) + }, cancellationToken); + + return ApiResponse.Ok(new FinanceBusinessReportListResultResponse + { + 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] FinanceBusinessReportDetailRequest request, + CancellationToken cancellationToken) + { + // 1. 校验门店访问权限并解析参数。 + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + var reportId = StoreApiHelpers.ParseRequiredSnowflake(request.ReportId, nameof(request.ReportId)); + + // 2. 发起详情查询。 + var detail = await mediator.Send(new GetFinanceBusinessReportDetailQuery + { + StoreId = storeId, + ReportId = reportId + }, cancellationToken); + + if (detail is null) + { + return ApiResponse.Error(ErrorCodes.NotFound, "经营报表不存在"); + } + + return ApiResponse.Ok(MapDetail(detail)); + } + + /// + /// 导出单条报表 PDF。 + /// + [HttpGet("export/pdf")] + [PermissionAuthorize(ExportPermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> ExportPdf( + [FromQuery] FinanceBusinessReportDetailRequest request, + CancellationToken cancellationToken) + { + // 1. 校验门店访问权限并解析参数。 + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + var reportId = StoreApiHelpers.ParseRequiredSnowflake(request.ReportId, nameof(request.ReportId)); + + // 2. 执行导出。 + var export = await mediator.Send(new ExportFinanceBusinessReportPdfQuery + { + StoreId = storeId, + ReportId = reportId + }, cancellationToken); + + return ApiResponse.Ok(MapExport(export)); + } + + /// + /// 导出单条报表 Excel。 + /// + [HttpGet("export/excel")] + [PermissionAuthorize(ExportPermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> ExportExcel( + [FromQuery] FinanceBusinessReportDetailRequest request, + CancellationToken cancellationToken) + { + // 1. 校验门店访问权限并解析参数。 + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + var reportId = StoreApiHelpers.ParseRequiredSnowflake(request.ReportId, nameof(request.ReportId)); + + // 2. 执行导出。 + var export = await mediator.Send(new ExportFinanceBusinessReportExcelQuery + { + StoreId = storeId, + ReportId = reportId + }, cancellationToken); + + return ApiResponse.Ok(MapExport(export)); + } + + /// + /// 批量导出报表 ZIP(PDF + Excel)。 + /// + [HttpGet("export/batch")] + [PermissionAuthorize(ExportPermission)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> ExportBatch( + [FromQuery] FinanceBusinessReportBatchExportRequest request, + CancellationToken cancellationToken) + { + // 1. 校验门店访问权限并解析参数。 + var storeId = StoreApiHelpers.ParseRequiredSnowflake(request.StoreId, nameof(request.StoreId)); + await EnsureStoreAccessibleAsync(storeId, cancellationToken); + var periodType = ParsePeriodType(request.PeriodType); + + // 2. 执行批量导出。 + var export = await mediator.Send(new ExportFinanceBusinessReportBatchQuery + { + StoreId = storeId, + PeriodType = periodType, + Page = Math.Max(1, request.Page), + PageSize = Math.Clamp(request.PageSize, 1, 200) + }, cancellationToken); + + return ApiResponse.Ok(MapExport(export)); + } + + private async Task EnsureStoreAccessibleAsync(long storeId, CancellationToken cancellationToken) + { + var (tenantId, merchantId) = StoreApiHelpers.GetTenantMerchantContext(storeContextService); + await StoreApiHelpers.EnsureStoreAccessibleAsync(dbContext, tenantId, merchantId, storeId, cancellationToken); + } + + private static FinanceBusinessReportPeriodType ParsePeriodType(string? value) + { + return (value ?? string.Empty).Trim().ToLowerInvariant() switch + { + "" or "daily" => FinanceBusinessReportPeriodType.Daily, + "weekly" => FinanceBusinessReportPeriodType.Weekly, + "monthly" => FinanceBusinessReportPeriodType.Monthly, + _ => throw new BusinessException(ErrorCodes.BadRequest, "periodType 参数不合法") + }; + } + + private static FinanceBusinessReportListItemResponse MapListItem(FinanceBusinessReportListItemDto source) + { + return new FinanceBusinessReportListItemResponse + { + ReportId = source.ReportId, + DateText = source.DateText, + RevenueAmount = source.RevenueAmount, + OrderCount = source.OrderCount, + AverageOrderValue = source.AverageOrderValue, + RefundRatePercent = source.RefundRatePercent, + CostTotalAmount = source.CostTotalAmount, + NetProfitAmount = source.NetProfitAmount, + ProfitRatePercent = source.ProfitRatePercent, + Status = source.Status, + StatusText = source.StatusText, + CanDownload = source.CanDownload + }; + } + + private static FinanceBusinessReportDetailResponse MapDetail(FinanceBusinessReportDetailDto source) + { + return new FinanceBusinessReportDetailResponse + { + ReportId = source.ReportId, + Title = source.Title, + PeriodType = source.PeriodType, + Status = source.Status, + StatusText = source.StatusText, + Kpis = source.Kpis.Select(item => new FinanceBusinessReportKpiResponse + { + Key = item.Key, + Label = item.Label, + ValueText = item.ValueText, + YoyChangeRate = item.YoyChangeRate, + MomChangeRate = item.MomChangeRate + }).ToList(), + IncomeBreakdowns = source.IncomeBreakdowns.Select(MapBreakdown).ToList(), + CostBreakdowns = source.CostBreakdowns.Select(MapBreakdown).ToList() + }; + } + + private static FinanceBusinessReportBreakdownItemResponse MapBreakdown(FinanceBusinessReportBreakdownItemDto source) + { + return new FinanceBusinessReportBreakdownItemResponse + { + Key = source.Key, + Label = source.Label, + Amount = source.Amount, + RatioPercent = source.RatioPercent + }; + } + + private static FinanceBusinessReportExportResponse MapExport(FinanceBusinessReportExportDto source) + { + return new FinanceBusinessReportExportResponse + { + FileName = source.FileName, + FileContentBase64 = source.FileContentBase64, + TotalCount = source.TotalCount + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Dto/FinanceBusinessReportDtos.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Dto/FinanceBusinessReportDtos.cs new file mode 100644 index 0000000..bcf431c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Dto/FinanceBusinessReportDtos.cs @@ -0,0 +1,218 @@ +namespace TakeoutSaaS.Application.App.Finance.Reports.Dto; + +/// +/// 经营报表列表行 DTO。 +/// +public sealed class FinanceBusinessReportListItemDto +{ + /// + /// 报表 ID。 + /// + public string ReportId { get; set; } = string.Empty; + + /// + /// 日期文案。 + /// + public string DateText { get; set; } = string.Empty; + + /// + /// 营业额。 + /// + public decimal RevenueAmount { get; set; } + + /// + /// 订单数。 + /// + public int OrderCount { get; set; } + + /// + /// 客单价。 + /// + public decimal AverageOrderValue { get; set; } + + /// + /// 退款率(百分数)。 + /// + public decimal RefundRatePercent { get; set; } + + /// + /// 成本总额。 + /// + public decimal CostTotalAmount { get; set; } + + /// + /// 净利润。 + /// + public decimal NetProfitAmount { get; set; } + + /// + /// 利润率(百分数)。 + /// + public decimal ProfitRatePercent { get; set; } + + /// + /// 状态编码。 + /// + public string Status { get; set; } = string.Empty; + + /// + /// 状态文案。 + /// + public string StatusText { get; set; } = string.Empty; + + /// + /// 是否可下载。 + /// + public bool CanDownload { get; set; } +} + +/// +/// 经营报表列表结果 DTO。 +/// +public sealed class FinanceBusinessReportListResultDto +{ + /// + /// 列表项。 + /// + public List Items { get; set; } = []; + + /// + /// 总数。 + /// + public int Total { get; set; } + + /// + /// 页码。 + /// + public int Page { get; set; } + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } +} + +/// +/// 经营报表 KPI DTO。 +/// +public sealed class FinanceBusinessReportKpiDto +{ + /// + /// 指标键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 指标名称。 + /// + public string Label { get; set; } = string.Empty; + + /// + /// 指标值文本。 + /// + public string ValueText { get; set; } = string.Empty; + + /// + /// 同比变化率(百分数)。 + /// + public decimal YoyChangeRate { get; set; } + + /// + /// 环比变化率(百分数)。 + /// + public decimal MomChangeRate { get; set; } +} + +/// +/// 经营报表明细行 DTO。 +/// +public sealed class FinanceBusinessReportBreakdownItemDto +{ + /// + /// 明细键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 明细名称。 + /// + public string Label { get; set; } = string.Empty; + + /// + /// 金额。 + /// + public decimal Amount { get; set; } + + /// + /// 占比(百分数)。 + /// + public decimal RatioPercent { get; set; } +} + +/// +/// 经营报表详情 DTO。 +/// +public sealed class FinanceBusinessReportDetailDto +{ + /// + /// 报表 ID。 + /// + public string ReportId { get; set; } = string.Empty; + + /// + /// 标题。 + /// + public string Title { get; set; } = string.Empty; + + /// + /// 周期类型编码。 + /// + public string PeriodType { get; set; } = string.Empty; + + /// + /// 状态编码。 + /// + public string Status { get; set; } = string.Empty; + + /// + /// 状态文案。 + /// + public string StatusText { get; set; } = string.Empty; + + /// + /// 关键指标。 + /// + public List Kpis { get; set; } = []; + + /// + /// 收入明细(按渠道)。 + /// + public List IncomeBreakdowns { get; set; } = []; + + /// + /// 成本明细(按类别)。 + /// + public List CostBreakdowns { get; set; } = []; +} + +/// +/// 经营报表导出 DTO。 +/// +public sealed class FinanceBusinessReportExportDto +{ + /// + /// 文件名。 + /// + 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/Reports/Handlers/ExportFinanceBusinessReportBatchQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportBatchQueryHandler.cs new file mode 100644 index 0000000..1101089 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportBatchQueryHandler.cs @@ -0,0 +1,112 @@ +using System.Globalization; +using System.IO.Compression; +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; +using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Domain.Finance.Services; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers; + +/// +/// 经营报表批量导出处理器(ZIP:PDF + Excel)。 +/// +public sealed class ExportFinanceBusinessReportBatchQueryHandler( + IFinanceBusinessReportRepository financeBusinessReportRepository, + IFinanceBusinessReportExportService financeBusinessReportExportService, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + ExportFinanceBusinessReportBatchQuery request, + CancellationToken cancellationToken) + { + // 1. 读取租户上下文并归一化分页参数。 + var tenantId = tenantProvider.GetCurrentTenantId(); + var normalizedPage = Math.Max(1, request.Page); + var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200); + + // 2. 确保成本配置并补齐快照。 + await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync( + tenantId, + request.StoreId, + cancellationToken); + await financeBusinessReportRepository.QueueSnapshotsForPageAsync( + tenantId, + request.StoreId, + request.PeriodType, + normalizedPage, + normalizedPageSize, + cancellationToken); + + // 3. 查询导出明细集合(允许实时补算)。 + var details = await financeBusinessReportRepository.ListBatchDetailsAsync( + tenantId, + request.StoreId, + request.PeriodType, + normalizedPage, + normalizedPageSize, + allowRealtimeBuild: true, + cancellationToken); + + // 4. 生成批量 PDF/Excel 并打包 ZIP。 + var periodCode = FinanceBusinessReportMapping.ToPeriodTypeCode(request.PeriodType); + var zipBytes = await CreateZipAsync( + details, + periodCode, + financeBusinessReportExportService, + cancellationToken); + + return new FinanceBusinessReportExportDto + { + FileName = string.Create( + CultureInfo.InvariantCulture, + $"business-report-batch-{request.StoreId}-{periodCode}-{DateTime.UtcNow:yyyyMMddHHmmss}.zip"), + FileContentBase64 = Convert.ToBase64String(zipBytes), + TotalCount = details.Count + }; + } + + private static async Task CreateZipAsync( + IReadOnlyList details, + string periodCode, + IFinanceBusinessReportExportService exportService, + CancellationToken cancellationToken) + { + if (details.Count == 0) + { + using var emptyStream = new MemoryStream(); + using (var emptyArchive = new ZipArchive(emptyStream, ZipArchiveMode.Create, true)) + { + var entry = emptyArchive.CreateEntry("README.txt"); + await using var writer = new StreamWriter(entry.Open()); + await writer.WriteAsync("No business report data in current selection."); + } + + return emptyStream.ToArray(); + } + + var pdfBytes = await exportService.ExportBatchPdfAsync(details, cancellationToken); + var excelBytes = await exportService.ExportBatchExcelAsync(details, cancellationToken); + + using var stream = new MemoryStream(); + using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true)) + { + var pdfEntry = archive.CreateEntry($"business-report-{periodCode}.pdf"); + await using (var pdfEntryStream = pdfEntry.Open()) + { + await pdfEntryStream.WriteAsync(pdfBytes, cancellationToken); + } + + var excelEntry = archive.CreateEntry($"business-report-{periodCode}.xlsx"); + await using (var excelEntryStream = excelEntry.Open()) + { + await excelEntryStream.WriteAsync(excelBytes, cancellationToken); + } + } + + return stream.ToArray(); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportExcelQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportExcelQueryHandler.cs new file mode 100644 index 0000000..8f9fc64 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportExcelQueryHandler.cs @@ -0,0 +1,59 @@ +using System.Globalization; +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; +using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Domain.Finance.Services; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers; + +/// +/// 经营报表 Excel 导出处理器。 +/// +public sealed class ExportFinanceBusinessReportExcelQueryHandler( + IFinanceBusinessReportRepository financeBusinessReportRepository, + IFinanceBusinessReportExportService financeBusinessReportExportService, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + ExportFinanceBusinessReportExcelQuery request, + CancellationToken cancellationToken) + { + // 1. 读取租户上下文并确保成本配置存在。 + var tenantId = tenantProvider.GetCurrentTenantId(); + await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync( + tenantId, + request.StoreId, + cancellationToken); + + // 2. 查询报表详情(允许实时补算)。 + var detail = await financeBusinessReportRepository.GetDetailAsync( + tenantId, + request.StoreId, + request.ReportId, + allowRealtimeBuild: true, + cancellationToken); + + if (detail is null) + { + throw new BusinessException(ErrorCodes.NotFound, "经营报表不存在"); + } + + // 3. 导出 Excel 并返回 Base64。 + var fileBytes = await financeBusinessReportExportService.ExportSingleExcelAsync(detail, cancellationToken); + var periodCode = FinanceBusinessReportMapping.ToPeriodTypeCode(detail.PeriodType); + return new FinanceBusinessReportExportDto + { + FileName = string.Create( + CultureInfo.InvariantCulture, + $"business-report-{request.StoreId}-{periodCode}-{request.ReportId}.xlsx"), + FileContentBase64 = Convert.ToBase64String(fileBytes), + TotalCount = 1 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportPdfQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportPdfQueryHandler.cs new file mode 100644 index 0000000..2d5eb3c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/ExportFinanceBusinessReportPdfQueryHandler.cs @@ -0,0 +1,59 @@ +using System.Globalization; +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; +using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Domain.Finance.Services; +using TakeoutSaaS.Shared.Abstractions.Constants; +using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers; + +/// +/// 经营报表 PDF 导出处理器。 +/// +public sealed class ExportFinanceBusinessReportPdfQueryHandler( + IFinanceBusinessReportRepository financeBusinessReportRepository, + IFinanceBusinessReportExportService financeBusinessReportExportService, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + ExportFinanceBusinessReportPdfQuery request, + CancellationToken cancellationToken) + { + // 1. 读取租户上下文并确保成本配置存在。 + var tenantId = tenantProvider.GetCurrentTenantId(); + await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync( + tenantId, + request.StoreId, + cancellationToken); + + // 2. 查询报表详情(允许实时补算)。 + var detail = await financeBusinessReportRepository.GetDetailAsync( + tenantId, + request.StoreId, + request.ReportId, + allowRealtimeBuild: true, + cancellationToken); + + if (detail is null) + { + throw new BusinessException(ErrorCodes.NotFound, "经营报表不存在"); + } + + // 3. 导出 PDF 并返回 Base64。 + var fileBytes = await financeBusinessReportExportService.ExportSinglePdfAsync(detail, cancellationToken); + var periodCode = FinanceBusinessReportMapping.ToPeriodTypeCode(detail.PeriodType); + return new FinanceBusinessReportExportDto + { + FileName = string.Create( + CultureInfo.InvariantCulture, + $"business-report-{request.StoreId}-{periodCode}-{request.ReportId}.pdf"), + FileContentBase64 = Convert.ToBase64String(fileBytes), + TotalCount = 1 + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/FinanceBusinessReportMapping.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/FinanceBusinessReportMapping.cs new file mode 100644 index 0000000..a8a6a58 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/FinanceBusinessReportMapping.cs @@ -0,0 +1,178 @@ +using System.Globalization; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Domain.Finance.Enums; +using TakeoutSaaS.Domain.Finance.Models; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers; + +/// +/// 经营报表映射工具。 +/// +internal static class FinanceBusinessReportMapping +{ + /// + /// 映射列表行 DTO。 + /// + public static FinanceBusinessReportListItemDto ToListItem(FinanceBusinessReportListItemSnapshot source) + { + return new FinanceBusinessReportListItemDto + { + ReportId = source.ReportId.ToString(CultureInfo.InvariantCulture), + DateText = FormatPeriodText(source.PeriodType, source.PeriodStartAt, source.PeriodEndAt), + RevenueAmount = RoundMoney(source.RevenueAmount), + OrderCount = Math.Max(0, source.OrderCount), + AverageOrderValue = RoundMoney(source.AverageOrderValue), + RefundRatePercent = RoundPercent(source.RefundRate), + CostTotalAmount = RoundMoney(source.CostTotalAmount), + NetProfitAmount = RoundMoney(source.NetProfitAmount), + ProfitRatePercent = RoundPercent(source.ProfitRate), + Status = ToStatusCode(source.Status), + StatusText = ToStatusText(source.Status), + CanDownload = source.Status == FinanceBusinessReportStatus.Succeeded + }; + } + + /// + /// 映射详情 DTO。 + /// + public static FinanceBusinessReportDetailDto ToDetail(FinanceBusinessReportDetailSnapshot source) + { + return new FinanceBusinessReportDetailDto + { + ReportId = source.ReportId.ToString(CultureInfo.InvariantCulture), + Title = BuildTitle(source.PeriodType, source.PeriodStartAt, source.PeriodEndAt), + PeriodType = ToPeriodTypeCode(source.PeriodType), + Status = ToStatusCode(source.Status), + StatusText = ToStatusText(source.Status), + Kpis = source.Kpis.Select(ToKpi).ToList(), + IncomeBreakdowns = source.IncomeBreakdowns.Select(ToBreakdown).ToList(), + CostBreakdowns = source.CostBreakdowns.Select(ToBreakdown).ToList() + }; + } + + /// + /// 周期类型编码。 + /// + public static string ToPeriodTypeCode(FinanceBusinessReportPeriodType value) + { + return value switch + { + FinanceBusinessReportPeriodType.Daily => "daily", + FinanceBusinessReportPeriodType.Weekly => "weekly", + FinanceBusinessReportPeriodType.Monthly => "monthly", + _ => "daily" + }; + } + + private static FinanceBusinessReportKpiDto ToKpi(FinanceBusinessReportKpiSnapshot source) + { + return new FinanceBusinessReportKpiDto + { + Key = source.Key, + Label = source.Label, + ValueText = FormatKpiValue(source.Key, source.Value), + YoyChangeRate = RoundRate(source.YoyChangeRate), + MomChangeRate = RoundRate(source.MomChangeRate) + }; + } + + private static FinanceBusinessReportBreakdownItemDto ToBreakdown(FinanceBusinessReportBreakdownSnapshot source) + { + return new FinanceBusinessReportBreakdownItemDto + { + Key = source.Key, + Label = source.Label, + Amount = RoundMoney(source.Amount), + RatioPercent = RoundPercent(source.Ratio) + }; + } + + private static string FormatPeriodText( + FinanceBusinessReportPeriodType periodType, + DateTime startAt, + DateTime endAt) + { + return periodType switch + { + FinanceBusinessReportPeriodType.Daily => startAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), + FinanceBusinessReportPeriodType.Weekly => + $"{startAt:MM-dd} ~ {endAt.AddDays(-1):MM-dd}", + FinanceBusinessReportPeriodType.Monthly => startAt.ToString("yyyy年M月", CultureInfo.InvariantCulture), + _ => startAt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + }; + } + + private static string BuildTitle( + FinanceBusinessReportPeriodType periodType, + DateTime startAt, + DateTime endAt) + { + return periodType switch + { + FinanceBusinessReportPeriodType.Daily => $"{startAt:yyyy年M月d日} 经营日报", + FinanceBusinessReportPeriodType.Weekly => $"{startAt:yyyy年M月d日}~{endAt.AddDays(-1):M月d日} 经营周报", + FinanceBusinessReportPeriodType.Monthly => $"{startAt:yyyy年M月} 经营月报", + _ => "经营报表" + }; + } + + private static string ToStatusCode(FinanceBusinessReportStatus status) + { + return status switch + { + FinanceBusinessReportStatus.Queued => "queued", + FinanceBusinessReportStatus.Running => "running", + FinanceBusinessReportStatus.Succeeded => "succeeded", + FinanceBusinessReportStatus.Failed => "failed", + _ => "queued" + }; + } + + private static string ToStatusText(FinanceBusinessReportStatus status) + { + return status switch + { + FinanceBusinessReportStatus.Queued => "排队中", + FinanceBusinessReportStatus.Running => "生成中", + FinanceBusinessReportStatus.Succeeded => "已生成", + FinanceBusinessReportStatus.Failed => "生成失败", + _ => "排队中" + }; + } + + private static string FormatKpiValue(string key, decimal value) + { + if (key is "order_count") + { + return Math.Round(value, 0, MidpointRounding.AwayFromZero).ToString("0", CultureInfo.InvariantCulture); + } + + if (key is "refund_rate" or "profit_rate") + { + return $"{RoundPercent(value):0.##}%"; + } + + if (key is "average_order_value") + { + return $"¥{RoundMoney(value):0.##}"; + } + + return $"¥{RoundMoney(value):0.##}"; + } + + private static decimal RoundMoney(decimal value) + { + return decimal.Round(value, 2, MidpointRounding.AwayFromZero); + } + + private static decimal RoundPercent(decimal value) + { + return decimal.Round(value * 100m, 2, MidpointRounding.AwayFromZero); + } + + private static decimal RoundRate(decimal value) + { + return decimal.Round(value, 2, MidpointRounding.AwayFromZero); + } +} + diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/GetFinanceBusinessReportDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/GetFinanceBusinessReportDetailQueryHandler.cs new file mode 100644 index 0000000..d3f7b09 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/GetFinanceBusinessReportDetailQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; +using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers; + +/// +/// 经营报表详情查询处理器。 +/// +public sealed class GetFinanceBusinessReportDetailQueryHandler( + IFinanceBusinessReportRepository financeBusinessReportRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + GetFinanceBusinessReportDetailQuery request, + CancellationToken cancellationToken) + { + // 1. 读取租户上下文并确保成本配置存在。 + var tenantId = tenantProvider.GetCurrentTenantId(); + await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync( + tenantId, + request.StoreId, + cancellationToken); + + // 2. 查询详情(允许实时补算)并映射输出。 + var detail = await financeBusinessReportRepository.GetDetailAsync( + tenantId, + request.StoreId, + request.ReportId, + allowRealtimeBuild: true, + cancellationToken); + + return detail is null ? null : FinanceBusinessReportMapping.ToDetail(detail); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/SearchFinanceBusinessReportListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/SearchFinanceBusinessReportListQueryHandler.cs new file mode 100644 index 0000000..356d3a2 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Handlers/SearchFinanceBusinessReportListQueryHandler.cs @@ -0,0 +1,57 @@ +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; +using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Handlers; + +/// +/// 经营报表分页查询处理器。 +/// +public sealed class SearchFinanceBusinessReportListQueryHandler( + IFinanceBusinessReportRepository financeBusinessReportRepository, + ITenantProvider tenantProvider) + : IRequestHandler +{ + /// + public async Task Handle( + SearchFinanceBusinessReportListQuery request, + CancellationToken cancellationToken) + { + // 1. 读取租户上下文并归一化分页参数。 + var tenantId = tenantProvider.GetCurrentTenantId(); + var normalizedPage = Math.Max(1, request.Page); + var normalizedPageSize = Math.Clamp(request.PageSize, 1, 200); + + // 2. 确保成本配置并补齐分页周期快照。 + await financeBusinessReportRepository.EnsureDefaultCostProfilesAsync( + tenantId, + request.StoreId, + cancellationToken); + await financeBusinessReportRepository.QueueSnapshotsForPageAsync( + tenantId, + request.StoreId, + request.PeriodType, + normalizedPage, + normalizedPageSize, + cancellationToken); + + // 3. 查询分页快照并映射输出。 + var pageSnapshot = await financeBusinessReportRepository.SearchPageAsync( + tenantId, + request.StoreId, + request.PeriodType, + normalizedPage, + normalizedPageSize, + cancellationToken); + + return new FinanceBusinessReportListResultDto + { + Items = pageSnapshot.Items.Select(FinanceBusinessReportMapping.ToListItem).ToList(), + Total = pageSnapshot.TotalCount, + Page = normalizedPage, + PageSize = normalizedPageSize + }; + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportBatchQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportBatchQuery.cs new file mode 100644 index 0000000..94661bf --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportBatchQuery.cs @@ -0,0 +1,31 @@ +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Domain.Finance.Enums; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Queries; + +/// +/// 批量导出经营报表(ZIP:PDF + Excel)。 +/// +public sealed class ExportFinanceBusinessReportBatchQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 周期类型。 + /// + public FinanceBusinessReportPeriodType PeriodType { get; init; } = FinanceBusinessReportPeriodType.Daily; + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportExcelQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportExcelQuery.cs new file mode 100644 index 0000000..eae96b7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportExcelQuery.cs @@ -0,0 +1,20 @@ +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Queries; + +/// +/// 导出经营报表 Excel。 +/// +public sealed class ExportFinanceBusinessReportExcelQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 报表 ID。 + /// + public long ReportId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportPdfQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportPdfQuery.cs new file mode 100644 index 0000000..ae1a6ee --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/ExportFinanceBusinessReportPdfQuery.cs @@ -0,0 +1,20 @@ +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Queries; + +/// +/// 导出经营报表 PDF。 +/// +public sealed class ExportFinanceBusinessReportPdfQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 报表 ID。 + /// + public long ReportId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/GetFinanceBusinessReportDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/GetFinanceBusinessReportDetailQuery.cs new file mode 100644 index 0000000..b6da647 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/GetFinanceBusinessReportDetailQuery.cs @@ -0,0 +1,20 @@ +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Queries; + +/// +/// 查询经营报表详情。 +/// +public sealed class GetFinanceBusinessReportDetailQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 报表 ID。 + /// + public long ReportId { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/SearchFinanceBusinessReportListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/SearchFinanceBusinessReportListQuery.cs new file mode 100644 index 0000000..4d584d9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Queries/SearchFinanceBusinessReportListQuery.cs @@ -0,0 +1,31 @@ +using MediatR; +using TakeoutSaaS.Application.App.Finance.Reports.Dto; +using TakeoutSaaS.Domain.Finance.Enums; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Queries; + +/// +/// 查询经营报表分页列表。 +/// +public sealed class SearchFinanceBusinessReportListQuery : IRequest +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; init; } + + /// + /// 周期类型。 + /// + public FinanceBusinessReportPeriodType PeriodType { get; init; } = FinanceBusinessReportPeriodType.Daily; + + /// + /// 页码。 + /// + public int Page { get; init; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; init; } = 20; +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportBatchQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportBatchQueryValidator.cs new file mode 100644 index 0000000..d49aa5b --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportBatchQueryValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Validators; + +/// +/// 经营报表批量导出查询验证器。 +/// +public sealed class ExportFinanceBusinessReportBatchQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public ExportFinanceBusinessReportBatchQueryValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportExcelQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportExcelQueryValidator.cs new file mode 100644 index 0000000..2c6026a --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportExcelQueryValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Validators; + +/// +/// 经营报表 Excel 导出查询验证器。 +/// +public sealed class ExportFinanceBusinessReportExcelQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public ExportFinanceBusinessReportExcelQueryValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.ReportId).GreaterThan(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportPdfQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportPdfQueryValidator.cs new file mode 100644 index 0000000..c17e111 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/ExportFinanceBusinessReportPdfQueryValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Validators; + +/// +/// 经营报表 PDF 导出查询验证器。 +/// +public sealed class ExportFinanceBusinessReportPdfQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public ExportFinanceBusinessReportPdfQueryValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.ReportId).GreaterThan(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/GetFinanceBusinessReportDetailQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/GetFinanceBusinessReportDetailQueryValidator.cs new file mode 100644 index 0000000..b5a59c7 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/GetFinanceBusinessReportDetailQueryValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Validators; + +/// +/// 经营报表详情查询验证器。 +/// +public sealed class GetFinanceBusinessReportDetailQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public GetFinanceBusinessReportDetailQueryValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.ReportId).GreaterThan(0); + } +} diff --git a/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/SearchFinanceBusinessReportListQueryValidator.cs b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/SearchFinanceBusinessReportListQueryValidator.cs new file mode 100644 index 0000000..2894a73 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/App/Finance/Reports/Validators/SearchFinanceBusinessReportListQueryValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using TakeoutSaaS.Application.App.Finance.Reports.Queries; + +namespace TakeoutSaaS.Application.App.Finance.Reports.Validators; + +/// +/// 经营报表分页查询验证器。 +/// +public sealed class SearchFinanceBusinessReportListQueryValidator : AbstractValidator +{ + /// + /// 初始化验证规则。 + /// + public SearchFinanceBusinessReportListQueryValidator() + { + RuleFor(x => x.StoreId).GreaterThan(0); + RuleFor(x => x.Page).GreaterThan(0); + RuleFor(x => x.PageSize).InclusiveBetween(1, 200); + } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceBusinessReportSnapshot.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceBusinessReportSnapshot.cs new file mode 100644 index 0000000..a4c3bc4 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceBusinessReportSnapshot.cs @@ -0,0 +1,111 @@ +using TakeoutSaaS.Domain.Finance.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Finance.Entities; + +/// +/// 经营报表快照实体。 +/// +public sealed class FinanceBusinessReportSnapshot : MultiTenantEntityBase +{ + /// + /// 所属门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 周期类型。 + /// + public FinanceBusinessReportPeriodType PeriodType { get; set; } + + /// + /// 周期开始时间(UTC,含)。 + /// + public DateTime PeriodStartAt { get; set; } + + /// + /// 周期结束时间(UTC,不含)。 + /// + public DateTime PeriodEndAt { get; set; } + + /// + /// 生成状态。 + /// + public FinanceBusinessReportStatus Status { get; set; } = FinanceBusinessReportStatus.Queued; + + /// + /// 营业额。 + /// + public decimal RevenueAmount { get; set; } + + /// + /// 订单数。 + /// + public int OrderCount { get; set; } + + /// + /// 客单价。 + /// + public decimal AverageOrderValue { get; set; } + + /// + /// 退款率(0-1)。 + /// + public decimal RefundRate { get; set; } + + /// + /// 成本总额。 + /// + public decimal CostTotalAmount { get; set; } + + /// + /// 净利润。 + /// + public decimal NetProfitAmount { get; set; } + + /// + /// 利润率(0-1)。 + /// + public decimal ProfitRate { get; set; } + + /// + /// KPI 比较快照 JSON(同比/环比)。 + /// + public string KpiComparisonJson { get; set; } = "[]"; + + /// + /// 收入明细快照 JSON(按渠道)。 + /// + public string IncomeBreakdownJson { get; set; } = "[]"; + + /// + /// 成本明细快照 JSON(按类别)。 + /// + public string CostBreakdownJson { get; set; } = "[]"; + + /// + /// 生成开始时间(UTC)。 + /// + public DateTime? StartedAt { get; set; } + + /// + /// 生成完成时间(UTC)。 + /// + public DateTime? FinishedAt { get; set; } + + /// + /// 最近一次失败信息。 + /// + public string? LastError { get; set; } + + /// + /// 重试次数。 + /// + public int RetryCount { get; set; } + + /// + /// 调度任务 ID。 + /// + public string? HangfireJobId { get; set; } +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceCostDailyOverride.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceCostDailyOverride.cs new file mode 100644 index 0000000..afae73b --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceCostDailyOverride.cs @@ -0,0 +1,36 @@ +using TakeoutSaaS.Domain.Finance.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Finance.Entities; + +/// +/// 成本日覆盖实体。 +/// +public sealed class FinanceCostDailyOverride : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 业务日期(UTC 日期)。 + /// + public DateTime BusinessDate { get; set; } + + /// + /// 成本分类。 + /// + public FinanceCostCategory Category { get; set; } + + /// + /// 覆盖金额。 + /// + public decimal Amount { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceCostProfile.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceCostProfile.cs new file mode 100644 index 0000000..52419e8 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Entities/FinanceCostProfile.cs @@ -0,0 +1,56 @@ +using TakeoutSaaS.Domain.Finance.Enums; +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Finance.Entities; + +/// +/// 成本配置实体(类别级规则)。 +/// +public sealed class FinanceCostProfile : MultiTenantEntityBase +{ + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 成本分类。 + /// + public FinanceCostCategory Category { get; set; } + + /// + /// 计算模式。 + /// + public FinanceCostCalcMode CalcMode { get; set; } + + /// + /// 比例值(0-1,Ratio 模式使用)。 + /// + public decimal Ratio { get; set; } + + /// + /// 固定日金额(FixedDaily 模式使用)。 + /// + public decimal FixedDailyAmount { get; set; } + + /// + /// 生效开始日期(UTC 日期)。 + /// + public DateTime EffectiveFrom { get; set; } + + /// + /// 生效结束日期(UTC 日期,含,null 表示长期)。 + /// + public DateTime? EffectiveTo { get; set; } + + /// + /// 是否启用。 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 排序值。 + /// + public int SortOrder { get; set; } = 100; +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceBusinessReportPeriodType.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceBusinessReportPeriodType.cs new file mode 100644 index 0000000..acb62a7 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceBusinessReportPeriodType.cs @@ -0,0 +1,23 @@ +namespace TakeoutSaaS.Domain.Finance.Enums; + +/// +/// 经营报表周期类型。 +/// +public enum FinanceBusinessReportPeriodType +{ + /// + /// 日报。 + /// + Daily = 1, + + /// + /// 周报。 + /// + Weekly = 2, + + /// + /// 月报。 + /// + Monthly = 3 +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceBusinessReportStatus.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceBusinessReportStatus.cs new file mode 100644 index 0000000..71e0b09 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceBusinessReportStatus.cs @@ -0,0 +1,28 @@ +namespace TakeoutSaaS.Domain.Finance.Enums; + +/// +/// 经营报表快照状态。 +/// +public enum FinanceBusinessReportStatus +{ + /// + /// 已排队。 + /// + Queued = 1, + + /// + /// 生成中。 + /// + Running = 2, + + /// + /// 已生成。 + /// + Succeeded = 3, + + /// + /// 生成失败。 + /// + Failed = 4 +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceCostCalcMode.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceCostCalcMode.cs new file mode 100644 index 0000000..d93f2e9 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Enums/FinanceCostCalcMode.cs @@ -0,0 +1,18 @@ +namespace TakeoutSaaS.Domain.Finance.Enums; + +/// +/// 成本计算模式。 +/// +public enum FinanceCostCalcMode +{ + /// + /// 按营业额比例计算。 + /// + Ratio = 1, + + /// + /// 按固定日金额计算。 + /// + FixedDaily = 2 +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Models/FinanceBusinessReportSnapshots.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Models/FinanceBusinessReportSnapshots.cs new file mode 100644 index 0000000..f9eb8c1 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Models/FinanceBusinessReportSnapshots.cs @@ -0,0 +1,245 @@ +using TakeoutSaaS.Domain.Finance.Enums; + +namespace TakeoutSaaS.Domain.Finance.Models; + +/// +/// 经营报表 KPI 快照项。 +/// +public sealed class FinanceBusinessReportKpiSnapshot +{ + /// + /// 指标键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 指标名称。 + /// + public string Label { get; set; } = string.Empty; + + /// + /// 指标值。 + /// + public decimal Value { get; set; } + + /// + /// 同比变化率(百分数,如 3.5 表示 +3.5%)。 + /// + public decimal YoyChangeRate { get; set; } + + /// + /// 环比变化率(百分数,如 2.1 表示 +2.1%)。 + /// + public decimal MomChangeRate { get; set; } +} + +/// +/// 经营报表明细行快照。 +/// +public sealed class FinanceBusinessReportBreakdownSnapshot +{ + /// + /// 明细键。 + /// + public string Key { get; set; } = string.Empty; + + /// + /// 明细名称。 + /// + public string Label { get; set; } = string.Empty; + + /// + /// 金额。 + /// + public decimal Amount { get; set; } + + /// + /// 占比(0-1)。 + /// + public decimal Ratio { get; set; } +} + +/// +/// 经营报表列表行快照。 +/// +public sealed class FinanceBusinessReportListItemSnapshot +{ + /// + /// 报表 ID。 + /// + public long ReportId { get; set; } + + /// + /// 周期类型。 + /// + public FinanceBusinessReportPeriodType PeriodType { get; set; } + + /// + /// 周期开始时间(UTC,含)。 + /// + public DateTime PeriodStartAt { get; set; } + + /// + /// 周期结束时间(UTC,不含)。 + /// + public DateTime PeriodEndAt { get; set; } + + /// + /// 状态。 + /// + public FinanceBusinessReportStatus Status { get; set; } + + /// + /// 营业额。 + /// + public decimal RevenueAmount { get; set; } + + /// + /// 订单数。 + /// + public int OrderCount { get; set; } + + /// + /// 客单价。 + /// + public decimal AverageOrderValue { get; set; } + + /// + /// 退款率(0-1)。 + /// + public decimal RefundRate { get; set; } + + /// + /// 成本总额。 + /// + public decimal CostTotalAmount { get; set; } + + /// + /// 净利润。 + /// + public decimal NetProfitAmount { get; set; } + + /// + /// 利润率(0-1)。 + /// + public decimal ProfitRate { get; set; } +} + +/// +/// 经营报表分页快照。 +/// +public sealed class FinanceBusinessReportPageSnapshot +{ + /// + /// 列表。 + /// + public List Items { get; set; } = []; + + /// + /// 总数。 + /// + public int TotalCount { get; set; } +} + +/// +/// 经营报表详情快照。 +/// +public sealed class FinanceBusinessReportDetailSnapshot +{ + /// + /// 报表 ID。 + /// + public long ReportId { get; set; } + + /// + /// 门店 ID。 + /// + public long StoreId { get; set; } + + /// + /// 周期类型。 + /// + public FinanceBusinessReportPeriodType PeriodType { get; set; } + + /// + /// 周期开始时间(UTC,含)。 + /// + public DateTime PeriodStartAt { get; set; } + + /// + /// 周期结束时间(UTC,不含)。 + /// + public DateTime PeriodEndAt { get; set; } + + /// + /// 状态。 + /// + public FinanceBusinessReportStatus Status { get; set; } + + /// + /// 营业额。 + /// + public decimal RevenueAmount { get; set; } + + /// + /// 订单数。 + /// + public int OrderCount { get; set; } + + /// + /// 客单价。 + /// + public decimal AverageOrderValue { get; set; } + + /// + /// 退款率(0-1)。 + /// + public decimal RefundRate { get; set; } + + /// + /// 成本总额。 + /// + public decimal CostTotalAmount { get; set; } + + /// + /// 净利润。 + /// + public decimal NetProfitAmount { get; set; } + + /// + /// 利润率(0-1)。 + /// + public decimal ProfitRate { get; set; } + + /// + /// 关键指标快照列表。 + /// + public List Kpis { get; set; } = []; + + /// + /// 收入明细(按渠道)。 + /// + public List IncomeBreakdowns { get; set; } = []; + + /// + /// 成本明细(按类别)。 + /// + public List CostBreakdowns { get; set; } = []; +} + +/// +/// 待处理报表任务快照。 +/// +public sealed class FinanceBusinessReportPendingSnapshot +{ + /// + /// 快照 ID。 + /// + public long SnapshotId { get; set; } + + /// + /// 租户 ID。 + /// + public long TenantId { get; set; } +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Repositories/IFinanceBusinessReportRepository.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Repositories/IFinanceBusinessReportRepository.cs new file mode 100644 index 0000000..aed9aee --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Repositories/IFinanceBusinessReportRepository.cs @@ -0,0 +1,77 @@ +using TakeoutSaaS.Domain.Finance.Enums; +using TakeoutSaaS.Domain.Finance.Models; + +namespace TakeoutSaaS.Domain.Finance.Repositories; + +/// +/// 经营报表仓储契约。 +/// +public interface IFinanceBusinessReportRepository +{ + /// + /// 确保门店存在默认成本配置。 + /// + Task EnsureDefaultCostProfilesAsync( + long tenantId, + long storeId, + CancellationToken cancellationToken = default); + + /// + /// 为指定分页周期补齐快照并排队。 + /// + Task QueueSnapshotsForPageAsync( + long tenantId, + long storeId, + FinanceBusinessReportPeriodType periodType, + int page, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 查询经营报表分页结果。 + /// + Task SearchPageAsync( + long tenantId, + long storeId, + FinanceBusinessReportPeriodType periodType, + int page, + int pageSize, + CancellationToken cancellationToken = default); + + /// + /// 查询经营报表详情。 + /// + Task GetDetailAsync( + long tenantId, + long storeId, + long reportId, + bool allowRealtimeBuild, + CancellationToken cancellationToken = default); + + /// + /// 查询批量导出详情集合。 + /// + Task> ListBatchDetailsAsync( + long tenantId, + long storeId, + FinanceBusinessReportPeriodType periodType, + int page, + int pageSize, + bool allowRealtimeBuild, + CancellationToken cancellationToken = default); + + /// + /// 拉取待处理任务。 + /// + Task> GetPendingSnapshotsAsync( + int take, + CancellationToken cancellationToken = default); + + /// + /// 执行报表快照生成。 + /// + Task GenerateSnapshotAsync( + long snapshotId, + CancellationToken cancellationToken = default); +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Finance/Services/IFinanceBusinessReportExportService.cs b/src/Domain/TakeoutSaaS.Domain/Finance/Services/IFinanceBusinessReportExportService.cs new file mode 100644 index 0000000..9d2f212 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Finance/Services/IFinanceBusinessReportExportService.cs @@ -0,0 +1,38 @@ +using TakeoutSaaS.Domain.Finance.Models; + +namespace TakeoutSaaS.Domain.Finance.Services; + +/// +/// 经营报表导出服务契约。 +/// +public interface IFinanceBusinessReportExportService +{ + /// + /// 导出单条报表 PDF。 + /// + Task ExportSinglePdfAsync( + FinanceBusinessReportDetailSnapshot detail, + CancellationToken cancellationToken = default); + + /// + /// 导出单条报表 Excel。 + /// + Task ExportSingleExcelAsync( + FinanceBusinessReportDetailSnapshot detail, + CancellationToken cancellationToken = default); + + /// + /// 导出批量报表 PDF。 + /// + Task ExportBatchPdfAsync( + IReadOnlyList details, + CancellationToken cancellationToken = default); + + /// + /// 导出批量报表 Excel。 + /// + Task ExportBatchExcelAsync( + IReadOnlyList details, + CancellationToken cancellationToken = default); +} + diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs index cf47802..60b714b 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Extensions/AppServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TakeoutSaaS.Application.App.Stores.Services; using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Domain.Finance.Services; using TakeoutSaaS.Domain.Coupons.Repositories; using TakeoutSaaS.Domain.Deliveries.Repositories; using TakeoutSaaS.Domain.Inventory.Repositories; @@ -56,6 +57,7 @@ public static class AppServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -80,6 +82,7 @@ public static class AppServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // 2. (空行后) 门店配置服务 services.AddScoped(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 759b770..a7d50b6 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -103,6 +103,18 @@ public sealed class TakeoutAppDbContext( /// public DbSet TenantInvoiceRecords => Set(); /// + /// 经营报表快照。 + /// + public DbSet FinanceBusinessReportSnapshots => Set(); + /// + /// 成本配置。 + /// + public DbSet FinanceCostProfiles => Set(); + /// + /// 成本日覆盖。 + /// + public DbSet FinanceCostDailyOverrides => Set(); + /// /// 成本录入汇总。 /// public DbSet FinanceCostEntries => Set(); @@ -544,6 +556,9 @@ public sealed class TakeoutAppDbContext( ConfigureTenantVisibilityRoleRule(modelBuilder.Entity()); ConfigureTenantInvoiceSetting(modelBuilder.Entity()); ConfigureTenantInvoiceRecord(modelBuilder.Entity()); + ConfigureFinanceBusinessReportSnapshot(modelBuilder.Entity()); + ConfigureFinanceCostProfile(modelBuilder.Entity()); + ConfigureFinanceCostDailyOverride(modelBuilder.Entity()); ConfigureFinanceCostEntry(modelBuilder.Entity()); ConfigureFinanceCostEntryItem(modelBuilder.Entity()); ConfigureQuotaPackage(modelBuilder.Entity()); @@ -1109,6 +1124,68 @@ public sealed class TakeoutAppDbContext( builder.HasIndex(x => new { x.TenantId, x.InvoiceType, x.AppliedAt }); } + private static void ConfigureFinanceBusinessReportSnapshot(EntityTypeBuilder builder) + { + builder.ToTable("finance_business_report_snapshots"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.PeriodType).HasConversion().IsRequired(); + builder.Property(x => x.PeriodStartAt).IsRequired(); + builder.Property(x => x.PeriodEndAt).IsRequired(); + builder.Property(x => x.Status).HasConversion().IsRequired(); + builder.Property(x => x.RevenueAmount).HasPrecision(18, 2); + builder.Property(x => x.OrderCount).IsRequired(); + builder.Property(x => x.AverageOrderValue).HasPrecision(18, 2); + builder.Property(x => x.RefundRate).HasPrecision(9, 4); + builder.Property(x => x.CostTotalAmount).HasPrecision(18, 2); + builder.Property(x => x.NetProfitAmount).HasPrecision(18, 2); + builder.Property(x => x.ProfitRate).HasPrecision(9, 4); + builder.Property(x => x.KpiComparisonJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.IncomeBreakdownJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.CostBreakdownJson).HasColumnType("text").IsRequired(); + builder.Property(x => x.LastError).HasMaxLength(1024); + builder.Property(x => x.HangfireJobId).HasMaxLength(64); + builder.Property(x => x.RetryCount).HasDefaultValue(0); + + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PeriodType, x.PeriodStartAt }).IsUnique(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.PeriodType, x.Status, x.PeriodStartAt }); + builder.HasIndex(x => new { x.TenantId, x.Status, x.CreatedAt }); + } + + private static void ConfigureFinanceCostProfile(EntityTypeBuilder builder) + { + builder.ToTable("finance_cost_profiles"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.Category).HasConversion().IsRequired(); + builder.Property(x => x.CalcMode).HasConversion().IsRequired(); + builder.Property(x => x.Ratio).HasPrecision(9, 6).IsRequired(); + builder.Property(x => x.FixedDailyAmount).HasPrecision(18, 2).IsRequired(); + builder.Property(x => x.EffectiveFrom).IsRequired(); + builder.Property(x => x.EffectiveTo); + builder.Property(x => x.IsEnabled).IsRequired(); + builder.Property(x => x.SortOrder).HasDefaultValue(100); + + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Category, x.EffectiveFrom, x.EffectiveTo }); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.IsEnabled, x.SortOrder }); + } + + private static void ConfigureFinanceCostDailyOverride(EntityTypeBuilder builder) + { + builder.ToTable("finance_cost_daily_overrides"); + builder.HasKey(x => x.Id); + builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.StoreId).IsRequired(); + builder.Property(x => x.BusinessDate).IsRequired(); + builder.Property(x => x.Category).HasConversion().IsRequired(); + builder.Property(x => x.Amount).HasPrecision(18, 2).IsRequired(); + builder.Property(x => x.Remark).HasMaxLength(256); + + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.BusinessDate, x.Category }).IsUnique(); + builder.HasIndex(x => new { x.TenantId, x.StoreId, x.BusinessDate }); + } private static void ConfigureFinanceCostEntry(EntityTypeBuilder builder) { builder.ToTable("finance_cost_entries"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfFinanceBusinessReportRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfFinanceBusinessReportRepository.cs new file mode 100644 index 0000000..5e8c00b --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfFinanceBusinessReportRepository.cs @@ -0,0 +1,761 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using TakeoutSaaS.Domain.Finance.Entities; +using TakeoutSaaS.Domain.Finance.Enums; +using TakeoutSaaS.Domain.Finance.Models; +using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Domain.Orders.Enums; +using TakeoutSaaS.Domain.Payments.Enums; +using TakeoutSaaS.Infrastructure.App.Persistence; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Infrastructure.App.Repositories; + +/// +/// 经营报表 EF Core 仓储实现。 +/// +public sealed class EfFinanceBusinessReportRepository( + TakeoutAppDbContext context, + ITenantContextAccessor tenantContextAccessor) : IFinanceBusinessReportRepository +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + private static readonly FinanceCostCategory[] CostCategoryOrder = + [ + FinanceCostCategory.FoodMaterial, + FinanceCostCategory.Labor, + FinanceCostCategory.FixedExpense, + FinanceCostCategory.PackagingConsumable + ]; + private static readonly DeliveryType[] IncomeChannelOrder = + [ + DeliveryType.Delivery, + DeliveryType.Pickup, + DeliveryType.DineIn + ]; + private static readonly IReadOnlyDictionary DefaultCostProfileMap = + new Dictionary + { + [FinanceCostCategory.FoodMaterial] = (FinanceCostCalcMode.Ratio, 0.36m, 0m), + [FinanceCostCategory.Labor] = (FinanceCostCalcMode.Ratio, 0.19m, 0m), + [FinanceCostCategory.FixedExpense] = (FinanceCostCalcMode.FixedDaily, 0m, 190m), + [FinanceCostCategory.PackagingConsumable] = (FinanceCostCalcMode.Ratio, 0.04m, 0m) + }; + + /// + public async Task EnsureDefaultCostProfilesAsync(long tenantId, long storeId, CancellationToken cancellationToken = default) + { + if (tenantId <= 0 || storeId <= 0) + { + return; + } + + var existing = await context.FinanceCostProfiles + .AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId && item.DeletedAt == null) + .Select(item => item.Category) + .Distinct() + .ToListAsync(cancellationToken); + var missing = CostCategoryOrder.Where(item => !existing.Contains(item)).ToList(); + if (missing.Count == 0) + { + return; + } + + var effectiveFrom = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var entities = missing.Select((category, index) => + { + var profile = DefaultCostProfileMap[category]; + return new FinanceCostProfile + { + TenantId = tenantId, + StoreId = storeId, + Category = category, + CalcMode = profile.Mode, + Ratio = profile.Ratio, + FixedDailyAmount = profile.Fixed, + EffectiveFrom = effectiveFrom, + IsEnabled = true, + SortOrder = (index + 1) * 10 + }; + }).ToList(); + + await context.FinanceCostProfiles.AddRangeAsync(entities, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task QueueSnapshotsForPageAsync(long tenantId, long storeId, FinanceBusinessReportPeriodType periodType, int page, int pageSize, CancellationToken cancellationToken = default) + { + if (tenantId <= 0 || storeId <= 0) + { + return; + } + + var now = DateTime.UtcNow; + var periods = BuildPagedPeriods(periodType, page, pageSize, now); + var starts = periods.Select(item => item.StartAt).ToHashSet(); + var existing = await context.FinanceBusinessReportSnapshots + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.PeriodType == periodType && + item.DeletedAt == null && + starts.Contains(item.PeriodStartAt)) + .OrderByDescending(item => item.Id) + .ToListAsync(cancellationToken); + var map = existing.GroupBy(item => item.PeriodStartAt).ToDictionary(group => group.Key, group => group.First()); + + var changed = false; + foreach (var period in periods) + { + if (!map.TryGetValue(period.StartAt, out var snapshot)) + { + await context.FinanceBusinessReportSnapshots.AddAsync(new FinanceBusinessReportSnapshot + { + TenantId = tenantId, + StoreId = storeId, + PeriodType = periodType, + PeriodStartAt = period.StartAt, + PeriodEndAt = period.EndAt, + Status = FinanceBusinessReportStatus.Queued + }, cancellationToken); + changed = true; + continue; + } + + if (snapshot.PeriodEndAt != period.EndAt) + { + snapshot.PeriodEndAt = period.EndAt; + changed = true; + } + + if (snapshot.Status == FinanceBusinessReportStatus.Failed && snapshot.RetryCount < 5) + { + snapshot.Status = FinanceBusinessReportStatus.Queued; + snapshot.LastError = null; + changed = true; + } + + if (now >= period.StartAt + && now < period.EndAt + && snapshot.Status == FinanceBusinessReportStatus.Succeeded + && (!snapshot.FinishedAt.HasValue || snapshot.FinishedAt.Value.AddMinutes(30) <= now)) + { + snapshot.Status = FinanceBusinessReportStatus.Queued; + snapshot.StartedAt = null; + snapshot.FinishedAt = null; + snapshot.LastError = null; + changed = true; + } + } + + if (changed) + { + await context.SaveChangesAsync(cancellationToken); + } + } + + /// + public async Task SearchPageAsync(long tenantId, long storeId, FinanceBusinessReportPeriodType periodType, int page, int pageSize, CancellationToken cancellationToken = default) + { + var normalizedPage = Math.Max(1, page); + var normalizedPageSize = Math.Clamp(pageSize, 1, 200); + var query = context.FinanceBusinessReportSnapshots + .AsNoTracking() + .Where(item => + item.TenantId == tenantId && + item.StoreId == storeId && + item.PeriodType == periodType && + item.DeletedAt == null); + + var totalCount = await query.CountAsync(cancellationToken); + if (totalCount == 0) + { + return new FinanceBusinessReportPageSnapshot(); + } + + var items = await query + .OrderByDescending(item => item.PeriodStartAt) + .ThenByDescending(item => item.Id) + .Skip((normalizedPage - 1) * normalizedPageSize) + .Take(normalizedPageSize) + .Select(item => new FinanceBusinessReportListItemSnapshot + { + ReportId = item.Id, + PeriodType = item.PeriodType, + PeriodStartAt = item.PeriodStartAt, + PeriodEndAt = item.PeriodEndAt, + Status = item.Status, + RevenueAmount = item.RevenueAmount, + OrderCount = item.OrderCount, + AverageOrderValue = item.AverageOrderValue, + RefundRate = item.RefundRate, + CostTotalAmount = item.CostTotalAmount, + NetProfitAmount = item.NetProfitAmount, + ProfitRate = item.ProfitRate + }) + .ToListAsync(cancellationToken); + + return new FinanceBusinessReportPageSnapshot + { + Items = items, + TotalCount = totalCount + }; + } + + /// + public async Task GetDetailAsync(long tenantId, long storeId, long reportId, bool allowRealtimeBuild, CancellationToken cancellationToken = default) + { + var snapshot = await context.FinanceBusinessReportSnapshots + .AsNoTracking() + .FirstOrDefaultAsync(item => item.TenantId == tenantId && item.StoreId == storeId && item.Id == reportId && item.DeletedAt == null, cancellationToken); + if (snapshot is null) + { + return null; + } + + if (allowRealtimeBuild && snapshot.Status != FinanceBusinessReportStatus.Succeeded) + { + await GenerateSnapshotAsync(reportId, cancellationToken); + snapshot = await context.FinanceBusinessReportSnapshots + .AsNoTracking() + .FirstOrDefaultAsync(item => item.TenantId == tenantId && item.StoreId == storeId && item.Id == reportId && item.DeletedAt == null, cancellationToken); + if (snapshot is null) + { + return null; + } + } + + return new FinanceBusinessReportDetailSnapshot + { + ReportId = snapshot.Id, + StoreId = snapshot.StoreId, + PeriodType = snapshot.PeriodType, + PeriodStartAt = snapshot.PeriodStartAt, + PeriodEndAt = snapshot.PeriodEndAt, + Status = snapshot.Status, + RevenueAmount = snapshot.RevenueAmount, + OrderCount = snapshot.OrderCount, + AverageOrderValue = snapshot.AverageOrderValue, + RefundRate = snapshot.RefundRate, + CostTotalAmount = snapshot.CostTotalAmount, + NetProfitAmount = snapshot.NetProfitAmount, + ProfitRate = snapshot.ProfitRate, + Kpis = Deserialize(snapshot.KpiComparisonJson), + IncomeBreakdowns = Deserialize(snapshot.IncomeBreakdownJson), + CostBreakdowns = Deserialize(snapshot.CostBreakdownJson) + }; + } + + /// + public async Task> ListBatchDetailsAsync(long tenantId, long storeId, FinanceBusinessReportPeriodType periodType, int page, int pageSize, bool allowRealtimeBuild, CancellationToken cancellationToken = default) + { + var normalizedPage = Math.Max(1, page); + var normalizedPageSize = Math.Clamp(pageSize, 1, 200); + var reportIds = await context.FinanceBusinessReportSnapshots + .AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId && item.PeriodType == periodType && item.DeletedAt == null) + .OrderByDescending(item => item.PeriodStartAt) + .ThenByDescending(item => item.Id) + .Skip((normalizedPage - 1) * normalizedPageSize) + .Take(normalizedPageSize) + .Select(item => item.Id) + .ToListAsync(cancellationToken); + + var list = new List(reportIds.Count); + foreach (var reportId in reportIds) + { + var detail = await GetDetailAsync(tenantId, storeId, reportId, allowRealtimeBuild, cancellationToken); + if (detail is not null) + { + list.Add(detail); + } + } + + return list; + } + + /// + public async Task> GetPendingSnapshotsAsync(int take, CancellationToken cancellationToken = default) + { + var normalizedTake = Math.Clamp(take, 1, 200); + if ((tenantContextAccessor.Current?.TenantId ?? 0) != 0) + { + return await QueryPendingAsync(normalizedTake, cancellationToken); + } + + var tenantIds = await context.Tenants.AsNoTracking().Where(item => item.DeletedAt == null && item.Id > 0).Select(item => item.Id).ToListAsync(cancellationToken); + var pending = new List<(long SnapshotId, long TenantId, DateTime CreatedAt)>(); + foreach (var tenantId in tenantIds) + { + using (tenantContextAccessor.EnterTenantScope(tenantId, "finance-report")) + { + var rows = await context.FinanceBusinessReportSnapshots + .AsNoTracking() + .Where(item => + item.DeletedAt == null && + (item.Status == FinanceBusinessReportStatus.Queued || (item.Status == FinanceBusinessReportStatus.Failed && item.RetryCount < 3))) + .OrderBy(item => item.CreatedAt) + .ThenBy(item => item.Id) + .Take(normalizedTake) + .Select(item => new { item.Id, item.TenantId, item.CreatedAt }) + .ToListAsync(cancellationToken); + pending.AddRange(rows.Select(item => (item.Id, item.TenantId, item.CreatedAt))); + } + } + + return pending.OrderBy(item => item.CreatedAt).ThenBy(item => item.SnapshotId).Take(normalizedTake).Select(item => new FinanceBusinessReportPendingSnapshot { SnapshotId = item.SnapshotId, TenantId = item.TenantId }).ToList(); + } + + /// + public async Task GenerateSnapshotAsync(long snapshotId, CancellationToken cancellationToken = default) + { + var snapshot = await context.FinanceBusinessReportSnapshots.FirstOrDefaultAsync(item => item.Id == snapshotId && item.DeletedAt == null, cancellationToken); + if (snapshot is null) + { + return; + } + + if (snapshot.Status == FinanceBusinessReportStatus.Running + && snapshot.StartedAt.HasValue + && snapshot.StartedAt.Value.AddMinutes(10) > DateTime.UtcNow) + { + return; + } + + snapshot.Status = FinanceBusinessReportStatus.Running; + snapshot.StartedAt = DateTime.UtcNow; + snapshot.LastError = null; + await context.SaveChangesAsync(cancellationToken); + + try + { + await EnsureDefaultCostProfilesAsync(snapshot.TenantId, snapshot.StoreId, cancellationToken); + var report = await BuildComputedSnapshotAsync(snapshot.TenantId, snapshot.StoreId, snapshot.PeriodType, snapshot.PeriodStartAt, snapshot.PeriodEndAt, cancellationToken); + snapshot.RevenueAmount = report.RevenueAmount; + snapshot.OrderCount = report.OrderCount; + snapshot.AverageOrderValue = report.AverageOrderValue; + snapshot.RefundRate = report.RefundRate; + snapshot.CostTotalAmount = report.CostTotalAmount; + snapshot.NetProfitAmount = report.NetProfitAmount; + snapshot.ProfitRate = report.ProfitRate; + snapshot.KpiComparisonJson = JsonSerializer.Serialize(report.Kpis, JsonOptions); + snapshot.IncomeBreakdownJson = JsonSerializer.Serialize(report.IncomeBreakdowns, JsonOptions); + snapshot.CostBreakdownJson = JsonSerializer.Serialize(report.CostBreakdowns, JsonOptions); + snapshot.Status = FinanceBusinessReportStatus.Succeeded; + snapshot.FinishedAt = DateTime.UtcNow; + snapshot.LastError = null; + } + catch (Exception ex) + { + snapshot.Status = FinanceBusinessReportStatus.Failed; + snapshot.FinishedAt = DateTime.UtcNow; + snapshot.RetryCount += 1; + snapshot.LastError = ex.Message[..Math.Min(1024, ex.Message.Length)]; + } + + await context.SaveChangesAsync(cancellationToken); + } + + private async Task> QueryPendingAsync(int take, CancellationToken cancellationToken) + { + return await context.FinanceBusinessReportSnapshots + .AsNoTracking() + .Where(item => + item.DeletedAt == null && + (item.Status == FinanceBusinessReportStatus.Queued + || (item.Status == FinanceBusinessReportStatus.Failed && item.RetryCount < 3))) + .OrderBy(item => item.CreatedAt) + .ThenBy(item => item.Id) + .Take(take) + .Select(item => new FinanceBusinessReportPendingSnapshot + { + SnapshotId = item.Id, + TenantId = item.TenantId + }) + .ToListAsync(cancellationToken); + } + + private async Task BuildComputedSnapshotAsync( + long tenantId, + long storeId, + FinanceBusinessReportPeriodType periodType, + DateTime startAt, + DateTime endAt, + CancellationToken cancellationToken) + { + var current = await BuildRawMetricsAsync(tenantId, storeId, startAt, endAt, cancellationToken); + var previous = ResolvePreviousPeriod(periodType, startAt, endAt); + var yearAgo = (startAt.AddYears(-1), endAt.AddYears(-1)); + var mom = await BuildRawMetricsAsync(tenantId, storeId, previous.StartAt, previous.EndAt, cancellationToken); + var yoy = await BuildRawMetricsAsync(tenantId, storeId, yearAgo.Item1, yearAgo.Item2, cancellationToken); + + return current with + { + Kpis = BuildKpis(current, mom, yoy) + }; + } + + private async Task BuildRawMetricsAsync( + long tenantId, + long storeId, + DateTime startAt, + DateTime endAt, + CancellationToken cancellationToken) + { + var summary = await QueryRevenueSummaryAsync(tenantId, storeId, startAt, endAt, cancellationToken); + var averageOrderValue = summary.OrderCount <= 0 ? 0m : RoundMoney(summary.RevenueAmount / summary.OrderCount); + var refundRate = summary.OrderCount <= 0 ? 0m : RoundRatio((decimal)summary.RefundOrderCount / summary.OrderCount); + var incomeBreakdowns = await QueryIncomeBreakdownsAsync(tenantId, storeId, startAt, endAt, summary.RevenueAmount, cancellationToken); + var dailyRevenueMap = await QueryDailyRevenueMapAsync(tenantId, storeId, startAt, endAt, cancellationToken); + var costBreakdowns = await BuildCostBreakdownsAsync(tenantId, storeId, startAt, endAt, dailyRevenueMap, cancellationToken); + var costTotalAmount = RoundMoney(costBreakdowns.Sum(item => item.Amount)); + var netProfitAmount = RoundMoney(summary.RevenueAmount - costTotalAmount); + var profitRate = summary.RevenueAmount <= 0 ? 0m : RoundRatio(netProfitAmount / summary.RevenueAmount); + + return new ComputedReportSnapshot + { + RevenueAmount = summary.RevenueAmount, + OrderCount = summary.OrderCount, + AverageOrderValue = averageOrderValue, + RefundRate = refundRate, + CostTotalAmount = costTotalAmount, + NetProfitAmount = netProfitAmount, + ProfitRate = profitRate, + Kpis = [], + IncomeBreakdowns = incomeBreakdowns, + CostBreakdowns = costBreakdowns + }; + } + + private async Task<(decimal RevenueAmount, int OrderCount, int RefundOrderCount)> QueryRevenueSummaryAsync( + long tenantId, + long storeId, + DateTime startAt, + DateTime endAt, + CancellationToken cancellationToken) + { + var paidBaseQuery = + 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 ?? payment.CreatedAt) >= startAt + && (payment.PaidAt ?? payment.CreatedAt) < endAt + select new { payment.Amount, payment.OrderId }; + + var paidAmount = await paidBaseQuery.Select(item => item.Amount).DefaultIfEmpty(0m).SumAsync(cancellationToken); + var orderCount = await paidBaseQuery.Select(item => item.OrderId).Distinct().CountAsync(cancellationToken); + + var refundBaseQuery = + from refund in context.PaymentRefundRecords.AsNoTracking() + join order in context.Orders.AsNoTracking() + on refund.OrderId equals order.Id + where refund.TenantId == tenantId + && order.TenantId == tenantId + && order.StoreId == storeId + && refund.Status == PaymentRefundStatus.Succeeded + && (refund.CompletedAt ?? refund.RequestedAt) >= startAt + && (refund.CompletedAt ?? refund.RequestedAt) < endAt + select new { refund.Amount, refund.OrderId }; + + var refundAmount = await refundBaseQuery.Select(item => item.Amount).DefaultIfEmpty(0m).SumAsync(cancellationToken); + var refundOrderCount = await refundBaseQuery.Select(item => item.OrderId).Distinct().CountAsync(cancellationToken); + return (RoundMoney(paidAmount - refundAmount), orderCount, refundOrderCount); + } + + private async Task> QueryDailyRevenueMapAsync( + long tenantId, + long storeId, + DateTime startAt, + DateTime endAt, + CancellationToken cancellationToken) + { + var paidRows = await ( + 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 ?? payment.CreatedAt) >= startAt + && (payment.PaidAt ?? payment.CreatedAt) < endAt + group payment by (payment.PaidAt ?? payment.CreatedAt).Date into grouped + select new { BusinessDate = grouped.Key, Amount = grouped.Sum(item => item.Amount) }) + .ToListAsync(cancellationToken); + + var refundRows = await ( + from refund in context.PaymentRefundRecords.AsNoTracking() + join order in context.Orders.AsNoTracking() + on refund.OrderId equals order.Id + where refund.TenantId == tenantId + && order.TenantId == tenantId + && order.StoreId == storeId + && refund.Status == PaymentRefundStatus.Succeeded + && (refund.CompletedAt ?? refund.RequestedAt) >= startAt + && (refund.CompletedAt ?? refund.RequestedAt) < endAt + group refund by (refund.CompletedAt ?? refund.RequestedAt).Date into grouped + select new { BusinessDate = grouped.Key, Amount = grouped.Sum(item => item.Amount) }) + .ToListAsync(cancellationToken); + + var map = new Dictionary(); + foreach (var row in paidRows) + { + var date = ToUtcDate(row.BusinessDate); + map[date] = map.GetValueOrDefault(date, 0m) + row.Amount; + } + + foreach (var row in refundRows) + { + var date = ToUtcDate(row.BusinessDate); + map[date] = map.GetValueOrDefault(date, 0m) - row.Amount; + } + + return map.ToDictionary(item => item.Key, item => RoundMoney(item.Value)); + } + + private async Task> QueryIncomeBreakdownsAsync( + long tenantId, + long storeId, + DateTime startAt, + DateTime endAt, + decimal totalRevenue, + CancellationToken cancellationToken) + { + var paidRows = await ( + 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 ?? payment.CreatedAt) >= startAt + && (payment.PaidAt ?? payment.CreatedAt) < endAt + group payment by order.DeliveryType into grouped + select new { DeliveryType = grouped.Key, Amount = grouped.Sum(item => item.Amount) }) + .ToListAsync(cancellationToken); + var refundRows = await ( + from refund in context.PaymentRefundRecords.AsNoTracking() + join order in context.Orders.AsNoTracking() + on refund.OrderId equals order.Id + where refund.TenantId == tenantId + && order.TenantId == tenantId + && order.StoreId == storeId + && refund.Status == PaymentRefundStatus.Succeeded + && (refund.CompletedAt ?? refund.RequestedAt) >= startAt + && (refund.CompletedAt ?? refund.RequestedAt) < endAt + group refund by order.DeliveryType into grouped + select new { DeliveryType = grouped.Key, Amount = grouped.Sum(item => item.Amount) }) + .ToListAsync(cancellationToken); + var paidMap = paidRows.ToDictionary(item => item.DeliveryType, item => item.Amount); + var refundMap = refundRows.ToDictionary(item => item.DeliveryType, item => item.Amount); + + return IncomeChannelOrder.Select(channel => + { + var amount = paidMap.GetValueOrDefault(channel, 0m) - refundMap.GetValueOrDefault(channel, 0m); + return new FinanceBusinessReportBreakdownSnapshot + { + Key = channel switch + { + DeliveryType.Delivery => "delivery", + DeliveryType.Pickup => "pickup", + DeliveryType.DineIn => "dine_in", + _ => "delivery" + }, + Label = channel switch + { + DeliveryType.Delivery => "外卖", + DeliveryType.Pickup => "自提", + DeliveryType.DineIn => "堂食", + _ => "外卖" + }, + Amount = RoundMoney(amount), + Ratio = totalRevenue <= 0 ? 0m : RoundRatio(amount / totalRevenue) + }; + }).ToList(); + } + + private async Task> BuildCostBreakdownsAsync( + long tenantId, + long storeId, + DateTime startAt, + DateTime endAt, + IReadOnlyDictionary dailyRevenueMap, + CancellationToken cancellationToken) + { + var profiles = await context.FinanceCostProfiles.AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId && item.DeletedAt == null && item.IsEnabled) + .OrderBy(item => item.SortOrder).ThenByDescending(item => item.EffectiveFrom).ToListAsync(cancellationToken); + var overrides = await context.FinanceCostDailyOverrides.AsNoTracking() + .Where(item => item.TenantId == tenantId && item.StoreId == storeId && item.DeletedAt == null && item.BusinessDate >= startAt.Date && item.BusinessDate < endAt.Date) + .ToListAsync(cancellationToken); + var overrideMap = overrides.ToDictionary(item => $"{item.BusinessDate:yyyyMMdd}:{(int)item.Category}", item => item.Amount); + var categoryAmountMap = CostCategoryOrder.ToDictionary(item => item, _ => 0m); + + for (var businessDay = startAt.Date; businessDay < endAt.Date; businessDay = businessDay.AddDays(1)) + { + var dayRevenue = dailyRevenueMap.GetValueOrDefault(ToUtcDate(businessDay), 0m); + foreach (var category in CostCategoryOrder) + { + var key = $"{businessDay:yyyyMMdd}:{(int)category}"; + decimal amount; + if (overrideMap.TryGetValue(key, out var overrideAmount)) + { + amount = overrideAmount; + } + else + { + var profile = profiles.FirstOrDefault(item => + item.Category == category && + item.EffectiveFrom.Date <= businessDay && + (!item.EffectiveTo.HasValue || item.EffectiveTo.Value.Date >= businessDay)); + var defaults = DefaultCostProfileMap[category]; + var mode = profile?.CalcMode ?? defaults.Mode; + var ratio = profile?.Ratio ?? defaults.Ratio; + var fixedDaily = profile?.FixedDailyAmount ?? defaults.Fixed; + amount = mode == FinanceCostCalcMode.FixedDaily ? fixedDaily : dayRevenue * Math.Max(0m, ratio); + } + + categoryAmountMap[category] += RoundMoney(amount); + } + } + + var totalCostAmount = categoryAmountMap.Sum(item => item.Value); + return CostCategoryOrder.Select(category => new FinanceBusinessReportBreakdownSnapshot + { + Key = category switch + { + FinanceCostCategory.FoodMaterial => "food_material", + FinanceCostCategory.Labor => "labor", + FinanceCostCategory.FixedExpense => "fixed_expense", + FinanceCostCategory.PackagingConsumable => "packaging_consumable", + _ => "food_material" + }, + Label = category switch + { + FinanceCostCategory.FoodMaterial => "食材成本", + FinanceCostCategory.Labor => "人工成本", + FinanceCostCategory.FixedExpense => "固定成本", + FinanceCostCategory.PackagingConsumable => "包装成本", + _ => "食材成本" + }, + Amount = RoundMoney(categoryAmountMap[category]), + Ratio = totalCostAmount <= 0m ? 0m : RoundRatio(categoryAmountMap[category] / totalCostAmount) + }).ToList(); + } + + private static List BuildKpis(ComputedReportSnapshot current, ComputedReportSnapshot mom, ComputedReportSnapshot yoy) + { + var definitions = new List<(string Key, string Label, decimal Current, decimal PrevMom, decimal PrevYoy)> + { + ("revenue", "营业额", current.RevenueAmount, mom.RevenueAmount, yoy.RevenueAmount), + ("order_count", "订单数", current.OrderCount, mom.OrderCount, yoy.OrderCount), + ("average_order_value", "客单价", current.AverageOrderValue, mom.AverageOrderValue, yoy.AverageOrderValue), + ("refund_rate", "退款率", current.RefundRate, mom.RefundRate, yoy.RefundRate), + ("net_profit", "净利润", current.NetProfitAmount, mom.NetProfitAmount, yoy.NetProfitAmount), + ("profit_rate", "利润率", current.ProfitRate, mom.ProfitRate, yoy.ProfitRate) + }; + + return definitions.Select(item => new FinanceBusinessReportKpiSnapshot + { + Key = item.Key, + Label = item.Label, + Value = item.Current, + MomChangeRate = CalculateChangeRate(item.Current, item.PrevMom), + YoyChangeRate = CalculateChangeRate(item.Current, item.PrevYoy) + }).ToList(); + } + + private static (DateTime StartAt, DateTime EndAt) ResolvePreviousPeriod(FinanceBusinessReportPeriodType periodType, DateTime startAt, DateTime endAt) + { + return periodType switch + { + FinanceBusinessReportPeriodType.Daily => (startAt.AddDays(-1), endAt.AddDays(-1)), + FinanceBusinessReportPeriodType.Weekly => (startAt.AddDays(-7), endAt.AddDays(-7)), + FinanceBusinessReportPeriodType.Monthly => (startAt.AddMonths(-1), endAt.AddMonths(-1)), + _ => (startAt.AddDays(-1), endAt.AddDays(-1)) + }; + } + + private static List<(DateTime StartAt, DateTime EndAt)> BuildPagedPeriods(FinanceBusinessReportPeriodType periodType, int page, int pageSize, DateTime now) + { + var normalizedPage = Math.Max(1, page); + var normalizedPageSize = Math.Clamp(pageSize, 1, 200); + var offsetStart = (normalizedPage - 1) * normalizedPageSize; + var today = ToUtcDate(now); + var list = new List<(DateTime StartAt, DateTime EndAt)>(normalizedPageSize); + for (var index = 0; index < normalizedPageSize; index++) + { + var offset = offsetStart + index; + if (periodType == FinanceBusinessReportPeriodType.Weekly) + { + var weekStart = GetWeekStart(today).AddDays(-7 * offset); + list.Add((weekStart, weekStart.AddDays(7))); + } + else if (periodType == FinanceBusinessReportPeriodType.Monthly) + { + var monthStart = new DateTime(today.Year, today.Month, 1, 0, 0, 0, DateTimeKind.Utc).AddMonths(-offset); + list.Add((monthStart, monthStart.AddMonths(1))); + } + else + { + var dayStart = today.AddDays(-offset); + list.Add((dayStart, dayStart.AddDays(1))); + } + } + + return list; + } + + private static List Deserialize(string? json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return []; + } + + try + { + return JsonSerializer.Deserialize>(json, JsonOptions) ?? []; + } + catch + { + return []; + } + } + + private sealed record ComputedReportSnapshot + { + public decimal RevenueAmount { get; init; } + + public int OrderCount { get; init; } + + public decimal AverageOrderValue { get; init; } + + public decimal RefundRate { get; init; } + + public decimal CostTotalAmount { get; init; } + + public decimal NetProfitAmount { get; init; } + + public decimal ProfitRate { get; init; } + + public List Kpis { get; init; } = []; + + public List IncomeBreakdowns { get; init; } = []; + + public List CostBreakdowns { get; init; } = []; + } + + private static decimal CalculateChangeRate(decimal currentValue, decimal previousValue) => previousValue <= 0m ? (currentValue <= 0m ? 0m : 100m) : RoundRate((currentValue - previousValue) / previousValue * 100m); + private static decimal RoundMoney(decimal value) => decimal.Round(value, 2, MidpointRounding.AwayFromZero); + private static decimal RoundRate(decimal value) => decimal.Round(value, 2, MidpointRounding.AwayFromZero); + private static decimal RoundRatio(decimal value) => decimal.Round(value, 4, MidpointRounding.AwayFromZero); + private static DateTime ToUtcDate(DateTime value) => new(value.Year, value.Month, value.Day, 0, 0, 0, DateTimeKind.Utc); + private static DateTime GetWeekStart(DateTime date) => date.AddDays(0 - (((int)date.DayOfWeek + 6) % 7)); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/FinanceBusinessReportExportService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/FinanceBusinessReportExportService.cs new file mode 100644 index 0000000..8e42e81 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Services/FinanceBusinessReportExportService.cs @@ -0,0 +1,303 @@ +using ClosedXML.Excel; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using System.Globalization; +using TakeoutSaaS.Domain.Finance.Enums; +using TakeoutSaaS.Domain.Finance.Models; +using TakeoutSaaS.Domain.Finance.Services; + +namespace TakeoutSaaS.Infrastructure.App.Services; + +/// +/// 经营报表导出服务实现(PDF / Excel)。 +/// +public sealed class FinanceBusinessReportExportService : IFinanceBusinessReportExportService +{ + public FinanceBusinessReportExportService() + { + QuestPDF.Settings.License = LicenseType.Community; + } + + /// + public Task ExportSinglePdfAsync( + FinanceBusinessReportDetailSnapshot detail, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(detail); + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(BuildPdf([detail])); + } + + /// + public Task ExportSingleExcelAsync( + FinanceBusinessReportDetailSnapshot detail, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(detail); + cancellationToken.ThrowIfCancellationRequested(); + + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("经营报表"); + WriteDetailWorksheet(worksheet, detail); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + return Task.FromResult(stream.ToArray()); + } + + /// + public Task ExportBatchPdfAsync( + IReadOnlyList details, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(details); + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(BuildPdf(details)); + } + + /// + public Task ExportBatchExcelAsync( + IReadOnlyList details, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(details); + cancellationToken.ThrowIfCancellationRequested(); + + using var workbook = new XLWorkbook(); + var summary = workbook.Worksheets.Add("汇总"); + WriteSummaryWorksheet(summary, details); + + for (var index = 0; index < details.Count; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + var detail = details[index]; + var sheetName = $"报表{index + 1:D2}"; + var worksheet = workbook.Worksheets.Add(sheetName); + WriteDetailWorksheet(worksheet, detail); + } + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + return Task.FromResult(stream.ToArray()); + } + + private static byte[] BuildPdf(IReadOnlyList details) + { + var source = details.Count == 0 + ? [new FinanceBusinessReportDetailSnapshot()] + : details; + + var document = Document.Create(container => + { + foreach (var detail in source) + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(24); + page.DefaultTextStyle(x => x.FontSize(10)); + + page.Content().Column(column => + { + column.Spacing(8); + column.Item().Text(BuildTitle(detail)).FontSize(16).SemiBold(); + column.Item().Text($"状态:{ResolveStatusText(detail.Status)}"); + + column.Item().Element(section => BuildKpiSection(section, detail.Kpis)); + column.Item().Element(section => BuildBreakdownSection(section, "收入明细(按渠道)", detail.IncomeBreakdowns)); + column.Item().Element(section => BuildBreakdownSection(section, "成本明细(按类别)", detail.CostBreakdowns)); + }); + }); + } + }); + + return document.GeneratePdf(); + } + + private static void BuildKpiSection(IContainer container, IReadOnlyList kpis) + { + container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column => + { + column.Spacing(4); + column.Item().Text("关键指标").SemiBold(); + + if (kpis.Count == 0) + { + column.Item().Text("暂无数据"); + return; + } + + foreach (var item in kpis) + { + column.Item().Row(row => + { + row.RelativeItem().Text(item.Label); + row.RelativeItem().AlignRight().Text(FormatKpiValue(item.Key, item.Value)); + row.RelativeItem().AlignRight().Text( + $"同比 {FormatSignedRate(item.YoyChangeRate)} | 环比 {FormatSignedRate(item.MomChangeRate)}"); + }); + } + }); + } + + private static void BuildBreakdownSection( + IContainer container, + string title, + IReadOnlyList rows) + { + container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column => + { + column.Spacing(4); + column.Item().Text(title).SemiBold(); + + if (rows.Count == 0) + { + column.Item().Text("暂无数据"); + return; + } + + foreach (var item in rows) + { + column.Item().Row(row => + { + row.RelativeItem().Text(item.Label); + row.ConstantItem(80).AlignRight().Text(FormatPercent(item.Ratio)); + row.ConstantItem(120).AlignRight().Text(FormatCurrency(item.Amount)); + }); + } + }); + } + + private static void WriteSummaryWorksheet( + IXLWorksheet worksheet, + IReadOnlyList details) + { + worksheet.Cell(1, 1).Value = "报表标题"; + worksheet.Cell(1, 2).Value = "状态"; + worksheet.Cell(1, 3).Value = "营业额"; + worksheet.Cell(1, 4).Value = "订单数"; + worksheet.Cell(1, 5).Value = "客单价"; + worksheet.Cell(1, 6).Value = "退款率"; + worksheet.Cell(1, 7).Value = "成本总额"; + worksheet.Cell(1, 8).Value = "净利润"; + worksheet.Cell(1, 9).Value = "利润率"; + + for (var index = 0; index < details.Count; index++) + { + var row = index + 2; + var detail = details[index]; + worksheet.Cell(row, 1).Value = BuildTitle(detail); + worksheet.Cell(row, 2).Value = ResolveStatusText(detail.Status); + worksheet.Cell(row, 3).Value = detail.RevenueAmount; + worksheet.Cell(row, 4).Value = detail.OrderCount; + worksheet.Cell(row, 5).Value = detail.AverageOrderValue; + worksheet.Cell(row, 6).Value = detail.RefundRate; + worksheet.Cell(row, 7).Value = detail.CostTotalAmount; + worksheet.Cell(row, 8).Value = detail.NetProfitAmount; + worksheet.Cell(row, 9).Value = detail.ProfitRate; + } + + worksheet.Columns().AdjustToContents(); + } + + private static void WriteDetailWorksheet( + IXLWorksheet worksheet, + FinanceBusinessReportDetailSnapshot detail) + { + var row = 1; + + worksheet.Cell(row, 1).Value = BuildTitle(detail); + worksheet.Range(row, 1, row, 4).Merge().Style.Font.SetBold(); + row += 2; + + worksheet.Cell(row, 1).Value = "关键指标"; + worksheet.Cell(row, 1).Style.Font.SetBold(); + row += 1; + + worksheet.Cell(row, 1).Value = "指标"; + worksheet.Cell(row, 2).Value = "值"; + worksheet.Cell(row, 3).Value = "同比"; + worksheet.Cell(row, 4).Value = "环比"; + row += 1; + + foreach (var item in detail.Kpis) + { + worksheet.Cell(row, 1).Value = item.Label; + worksheet.Cell(row, 2).Value = FormatKpiValue(item.Key, item.Value); + worksheet.Cell(row, 3).Value = FormatSignedRate(item.YoyChangeRate); + worksheet.Cell(row, 4).Value = FormatSignedRate(item.MomChangeRate); + row += 1; + } + + row += 1; + row = WriteBreakdownTable(worksheet, row, "收入明细(按渠道)", detail.IncomeBreakdowns); + row += 1; + _ = WriteBreakdownTable(worksheet, row, "成本明细(按类别)", detail.CostBreakdowns); + + worksheet.Columns().AdjustToContents(); + } + + private static int WriteBreakdownTable( + IXLWorksheet worksheet, + int startRow, + string title, + IReadOnlyList rows) + { + var row = startRow; + worksheet.Cell(row, 1).Value = title; + worksheet.Cell(row, 1).Style.Font.SetBold(); + row += 1; + + worksheet.Cell(row, 1).Value = "名称"; + worksheet.Cell(row, 2).Value = "占比"; + worksheet.Cell(row, 3).Value = "金额"; + row += 1; + + foreach (var item in rows) + { + worksheet.Cell(row, 1).Value = item.Label; + worksheet.Cell(row, 2).Value = FormatPercent(item.Ratio); + worksheet.Cell(row, 3).Value = item.Amount; + row += 1; + } + + return row; + } + + private static string FormatCurrency(decimal value) => $"¥{value:0.##}"; + private static string FormatPercent(decimal ratioValue) => $"{ratioValue * 100m:0.##}%"; + private static string FormatSignedRate(decimal rate) => $"{(rate >= 0m ? "+" : string.Empty)}{rate:0.##}%"; + private static string BuildTitle(FinanceBusinessReportDetailSnapshot detail) => detail.PeriodType switch + { + FinanceBusinessReportPeriodType.Daily => $"{detail.PeriodStartAt:yyyy年M月d日} 经营日报", + FinanceBusinessReportPeriodType.Weekly => $"{detail.PeriodStartAt:yyyy年M月d日}~{detail.PeriodEndAt.AddDays(-1):M月d日} 经营周报", + FinanceBusinessReportPeriodType.Monthly => $"{detail.PeriodStartAt:yyyy年M月} 经营月报", + _ => detail.PeriodStartAt == default + ? "经营报表" + : detail.PeriodStartAt.ToString("yyyy-MM-dd 经营报表", CultureInfo.InvariantCulture) + }; + private static string ResolveStatusText(FinanceBusinessReportStatus status) => status switch + { + FinanceBusinessReportStatus.Queued => "排队中", + FinanceBusinessReportStatus.Running => "生成中", + FinanceBusinessReportStatus.Succeeded => "已生成", + FinanceBusinessReportStatus.Failed => "生成失败", + _ => "未知" + }; + + private static string FormatKpiValue(string key, decimal value) + { + if (key == "order_count") + { + return $"{decimal.Round(value, 0, MidpointRounding.AwayFromZero):0}"; + } + + if (key is "refund_rate" or "profit_rate") + { + return $"{value * 100m:0.##}%"; + } + + return FormatCurrency(value); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304071623_AddFinanceInvoiceModule.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304071623_AddFinanceInvoiceModule.Designer.cs new file mode 100644 index 0000000..e64a535 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304071623_AddFinanceInvoiceModule.Designer.cs @@ -0,0 +1,10972 @@ +// +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("20260304071623_AddFinanceInvoiceModule")] + partial class AddFinanceInvoiceModule + { + /// + 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("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.HasKey("Id"); + + b.ToTable("promotion_campaigns", null, t => + { + t.HasComment("营销活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardInstance", 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("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC,可空)。"); + + b.Property("InstanceNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("实例编号(业务唯一)。"); + + b.Property("MemberName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会员名称。"); + + b.Property("MemberPhoneMasked") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("会员手机号(脱敏)。"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("PunchCardTemplateId") + .HasColumnType("bigint") + .HasComment("次卡模板 ID。"); + + b.Property("PurchasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("购买时间(UTC)。"); + + b.Property("RemainingTimes") + .HasColumnType("integer") + .HasComment("剩余次数。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("实例状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TotalTimes") + .HasColumnType("integer") + .HasComment("总次数。"); + + 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", "InstanceNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId"); + + b.HasIndex("TenantId", "StoreId", "Status", "ExpiresAt"); + + b.ToTable("punch_card_instances", null, t => + { + t.HasComment("次卡实例(顾客购买后生成)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowTransfer") + .HasColumnType("boolean") + .HasComment("是否允许转赠。"); + + b.Property("CoverImageUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("封面图片地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DailyLimit") + .HasColumnType("integer") + .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("ExpireStrategy") + .HasColumnType("integer") + .HasComment("过期策略。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("次卡名称。"); + + b.Property("NotifyChannelsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("购买通知渠道 JSON。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("PerOrderLimit") + .HasColumnType("integer") + .HasComment("每单限用次数。"); + + b.Property("PerUserPurchaseLimit") + .HasColumnType("integer") + .HasComment("每人限购张数。"); + + b.Property("SalePrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("售价。"); + + b.Property("ScopeCategoryIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定分类 ID JSON。"); + + b.Property("ScopeProductIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定商品 ID JSON。"); + + b.Property("ScopeTagIdsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("指定标签 ID JSON。"); + + b.Property("ScopeType") + .HasColumnType("integer") + .HasComment("适用范围类型。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("次卡状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TotalTimes") + .HasColumnType("integer") + .HasComment("总次数。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UsageCapAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("金额上限(UsageMode=Cap 时有效)。"); + + b.Property("UsageMode") + .HasColumnType("integer") + .HasComment("使用模式。"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasComment("固定开始日期(UTC,ValidityType=DateRange 时有效)。"); + + b.Property("ValidTo") + .HasColumnType("timestamp with time zone") + .HasComment("固定结束日期(UTC,ValidityType=DateRange 时有效)。"); + + b.Property("ValidityDays") + .HasColumnType("integer") + .HasComment("固定天数(ValidityType=Days 时有效)。"); + + b.Property("ValidityType") + .HasColumnType("integer") + .HasComment("有效期类型。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("punch_card_templates", null, t => + { + t.HasComment("次卡模板配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Coupons.Entities.PunchCardUsageRecord", 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("ExtraPayAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("超额补差金额。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("兑换商品名称。"); + + b.Property("PunchCardInstanceId") + .HasColumnType("bigint") + .HasComment("次卡实例 ID。"); + + b.Property("PunchCardTemplateId") + .HasColumnType("bigint") + .HasComment("次卡模板 ID。"); + + b.Property("RecordNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("使用单号。"); + + b.Property("RemainingTimesAfterUse") + .HasColumnType("integer") + .HasComment("使用后剩余次数。"); + + b.Property("StatusAfterUse") + .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("UsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("使用时间(UTC)。"); + + b.Property("UsedTimes") + .HasColumnType("integer") + .HasComment("本次使用次数。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "RecordNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "PunchCardInstanceId", "UsedAt"); + + b.HasIndex("TenantId", "StoreId", "PunchCardTemplateId", "UsedAt"); + + b.ToTable("punch_card_usage_records", null, t => + { + t.HasComment("次卡使用记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSessionId") + .HasColumnType("bigint") + .HasComment("会话标识。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("消息内容。"); + + b.Property("ContentType") + .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("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsRead") + .HasColumnType("boolean") + .HasComment("是否已读。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("读取时间。"); + + b.Property("SenderType") + .HasColumnType("integer") + .HasComment("发送方类型。"); + + b.Property("SenderUserId") + .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", "ChatSessionId", "CreatedAt"); + + b.ToTable("chat_messages", null, t => + { + t.HasComment("会话消息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.ChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AgentUserId") + .HasColumnType("bigint") + .HasComment("当前客服员工 ID。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("bigint") + .HasComment("顾客用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone") + .HasComment("结束时间。"); + + b.Property("IsBotActive") + .HasColumnType("boolean") + .HasComment("是否机器人接待中。"); + + b.Property("SessionCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话编号。"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "SessionCode") + .IsUnique(); + + b.ToTable("chat_sessions", null, t => + { + t.HasComment("客服会话。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.SupportTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAgentId") + .HasColumnType("bigint") + .HasComment("指派的客服。"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone") + .HasComment("关闭时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerUserId") + .HasColumnType("bigint") + .HasComment("客户用户 ID。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("工单详情。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单(如有)。"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("优先级。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("工单主题。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TicketNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("工单编号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TicketNo") + .IsUnique(); + + b.ToTable("support_tickets", null, t => + { + t.HasComment("客服工单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.CustomerService.Entities.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttachmentsJson") + .HasColumnType("text") + .HasComment("附件 JSON。"); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .HasComment("评论人 ID。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .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("IsInternal") + .HasColumnType("boolean") + .HasComment("是否内部备注。"); + + b.Property("SupportTicketId") + .HasColumnType("bigint") + .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", "SupportTicketId"); + + b.ToTable("ticket_comments", null, t => + { + t.HasComment("工单评论/流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryEvent", 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("DeliveryOrderId") + .HasColumnType("bigint") + .HasComment("配送单标识。"); + + b.Property("EventType") + .HasColumnType("integer") + .HasComment("事件类型。"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("事件描述。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("原始数据 JSON。"); + + 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", "DeliveryOrderId", "EventType"); + + b.ToTable("delivery_events", null, t => + { + t.HasComment("配送状态事件流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Deliveries.Entities.DeliveryOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CourierName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("骑手姓名。"); + + b.Property("CourierPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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("DeliveredAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); + + b.Property("DispatchedAt") + .HasColumnType("timestamp with time zone") + .HasComment("下发时间。"); + + b.Property("FailureReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("异常原因。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("获取或设置关联订单 ID。"); + + b.Property("PickedUpAt") + .HasColumnType("timestamp with time zone") + .HasComment("取餐时间。"); + + b.Property("Provider") + .HasColumnType("integer") + .HasComment("配送服务商。"); + + b.Property("ProviderOrderId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("第三方配送单号。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId") + .IsUnique(); + + b.ToTable("delivery_orders", null, t => + { + t.HasComment("配送单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliateOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") + .HasComment("推广人标识。"); + + b.Property("BuyerUserId") + .HasColumnType("bigint") + .HasComment("用户 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("EstimatedCommission") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("预计佣金。"); + + b.Property("OrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("订单金额。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单。"); + + b.Property("SettledAt") + .HasColumnType("timestamp with time zone") + .HasComment("结算完成时间。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "OrderId") + .IsUnique(); + + b.ToTable("affiliate_orders", null, t => + { + t.HasComment("分销订单记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePartner", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelType") + .HasColumnType("integer") + .HasComment("渠道类型。"); + + b.Property("CommissionRate") + .HasColumnType("numeric") + .HasComment("分成比例(0-1)。"); + + 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("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称或渠道名称。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + 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("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID(如绑定登录账号)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "DisplayName"); + + b.ToTable("affiliate_partners", null, t => + { + t.HasComment("分销/推广合作伙伴。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Distribution.Entities.AffiliatePayout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffiliatePartnerId") + .HasColumnType("bigint") + .HasComment("合作伙伴标识。"); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("打款时间。"); + + b.Property("Period") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("结算周期描述。"); + + b.Property("Remarks") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "AffiliatePartnerId", "Period") + .IsUnique(); + + b.ToTable("affiliate_payouts", null, t => + { + t.HasComment("佣金结算记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInCampaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowMakeupCount") + .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("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("结束日期。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("活动名称。"); + + b.Property("RewardsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("连签奖励 JSON。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("开始日期。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "Name"); + + b.ToTable("checkin_campaigns", null, t => + { + t.HasComment("签到活动配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CheckInRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CheckInCampaignId") + .HasColumnType("bigint") + .HasComment("活动标识。"); + + b.Property("CheckInDate") + .HasColumnType("timestamp with time zone") + .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("IsMakeup") + .HasColumnType("boolean") + .HasComment("是否补签。"); + + b.Property("RewardJson") + .IsRequired() + .HasColumnType("text") + .HasComment("获得奖励 JSON。"); + + 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("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CheckInCampaignId", "UserId", "CheckInDate") + .IsUnique(); + + b.ToTable("checkin_records", null, t => + { + t.HasComment("用户签到记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .HasComment("评论人。"); + + b.Property("Content") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .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("IsDeleted") + .HasColumnType("boolean") + .HasComment("状态。"); + + b.Property("ParentId") + .HasColumnType("bigint") + .HasComment("父级评论 ID。"); + + b.Property("PostId") + .HasColumnType("bigint") + .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", "PostId", "CreatedAt"); + + b.ToTable("community_comments", null, t => + { + t.HasComment("社区评论。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorUserId") + .HasColumnType("bigint") + .HasComment("作者用户 ID。"); + + b.Property("CommentCount") + .HasColumnType("integer") + .HasComment("评论数。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .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("LikeCount") + .HasColumnType("integer") + .HasComment("点赞数。"); + + b.Property("MediaJson") + .HasColumnType("text") + .HasComment("媒体资源 JSON。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AuthorUserId", "CreatedAt"); + + b.ToTable("community_posts", null, t => + { + t.HasComment("社区动态。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Engagement.Entities.CommunityReaction", 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("PostId") + .HasColumnType("bigint") + .HasComment("动态 ID。"); + + b.Property("ReactedAt") + .HasColumnType("timestamp with time zone") + .HasComment("时间戳。"); + + b.Property("ReactionType") + .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("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "PostId", "UserId") + .IsUnique(); + + b.ToTable("community_reactions", null, t => + { + t.HasComment("社区互动反馈。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Finance.Entities.FinanceCostEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("integer") + .HasComment("成本分类。"); + + b.Property("CostMonth") + .HasColumnType("timestamp with time zone") + .HasComment("成本月份(统一存储为 UTC 每月第一天 00:00:00)。"); + + 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("Dimension") + .HasColumnType("integer") + .HasComment("统计维度。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识(租户汇总维度为空)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TotalAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("分类总金额。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Dimension", "StoreId", "CostMonth"); + + b.HasIndex("TenantId", "Dimension", "StoreId", "CostMonth", "Category") + .IsUnique(); + + b.ToTable("finance_cost_entries", null, t => + { + t.HasComment("成本录入月度汇总实体(按维度 + 分类)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Finance.Entities.FinanceCostEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("明细金额。"); + + b.Property("Category") + .HasColumnType("integer") + .HasComment("成本分类。"); + + b.Property("CostMonth") + .HasColumnType("timestamp with time zone") + .HasComment("成本月份(统一存储为 UTC 每月第一天 00:00:00)。"); + + 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("Dimension") + .HasColumnType("integer") + .HasComment("统计维度。"); + + b.Property("EntryId") + .HasColumnType("bigint") + .HasComment("关联汇总行标识。"); + + b.Property("ItemName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("明细名称。"); + + b.Property("Quantity") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("数量(人工类可用)。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识(租户汇总维度为空)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价(人工类可用)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("EntryId"); + + b.HasIndex("TenantId", "Dimension", "StoreId", "CostMonth", "Category", "SortOrder"); + + b.ToTable("finance_cost_entry_items", null, t => + { + t.HasComment("成本录入明细项实体。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentCount") + .HasColumnType("integer") + .HasComment("当前已参与人数。"); + + 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("GroupOrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("拼单编号。"); + + b.Property("GroupPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("拼团价格。"); + + b.Property("LeaderUserId") + .HasColumnType("bigint") + .HasComment("团长用户 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("关联商品或套餐。"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("拼团状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("SucceededAt") + .HasColumnType("timestamp with time zone") + .HasComment("成团时间。"); + + b.Property("TargetCount") + .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", "GroupOrderNo") + .IsUnique(); + + b.ToTable("group_orders", null, t => + { + t.HasComment("拼单活动。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.GroupBuying.Entities.GroupParticipant", 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("GroupOrderId") + .HasColumnType("bigint") + .HasComment("拼单活动标识。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("参与时间。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("对应订单标识。"); + + 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("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "GroupOrderId", "UserId") + .IsUnique(); + + b.ToTable("group_participants", null, t => + { + t.HasComment("拼单参与者。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryAdjustment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdjustmentType") + .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("InventoryItemId") + .HasColumnType("bigint") + .HasComment("对应的库存记录标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("调整数量,正数增加,负数减少。"); + + b.Property("Reason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .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", "InventoryItemId", "OccurredAt"); + + b.ToTable("inventory_adjustments", null, t => + { + t.HasComment("库存调整记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryBatch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BatchNumber") + .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("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("ProductionDate") + .HasColumnType("timestamp with time zone") + .HasComment("生产日期。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("入库数量。"); + + b.Property("RemainingQuantity") + .HasColumnType("integer") + .HasComment("剩余数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "ProductSkuId", "BatchNumber") + .IsUnique(); + + b.ToTable("inventory_batches", null, t => + { + t.HasComment("SKU 批次信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BatchConsumeStrategy") + .HasColumnType("integer") + .HasComment("批次扣减策略。"); + + b.Property("BatchNumber") + .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("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("ExpireDate") + .HasColumnType("timestamp with time zone") + .HasComment("过期日期。"); + + b.Property("IsPresale") + .HasColumnType("boolean") + .HasComment("是否预售商品。"); + + b.Property("IsSoldOut") + .HasColumnType("boolean") + .HasComment("是否标记售罄。"); + + b.Property("Location") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("储位或仓位信息。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("单品限购(覆盖商品级 MaxQuantityPerOrder)。"); + + b.Property("PresaleCapacity") + .HasColumnType("integer") + .HasComment("预售名额(上限)。"); + + b.Property("PresaleEndTime") + .HasColumnType("timestamp with time zone") + .HasComment("预售结束时间(UTC)。"); + + b.Property("PresaleLocked") + .HasColumnType("integer") + .HasComment("当前预售已锁定数量。"); + + b.Property("PresaleStartTime") + .HasColumnType("timestamp with time zone") + .HasComment("预售开始时间(UTC)。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("QuantityOnHand") + .HasColumnType("integer") + .HasComment("可用库存。"); + + b.Property("QuantityReserved") + .HasColumnType("integer") + .HasComment("已锁定库存(订单占用)。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("SafetyStock") + .HasColumnType("integer") + .HasComment("安全库存阈值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "ProductSkuId", "BatchNumber"); + + b.ToTable("inventory_items", null, t => + { + t.HasComment("SKU 在门店的库存信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Inventory.Entities.InventoryLockRecord", 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("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC)。"); + + b.Property("IdempotencyKey") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("幂等键。"); + + b.Property("IsPresale") + .HasColumnType("boolean") + .HasComment("是否预售锁定。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU ID。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("锁定数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("Status") + .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.HasKey("Id"); + + b.HasIndex("TenantId", "IdempotencyKey") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "ProductSkuId", "Status"); + + b.ToTable("inventory_lock_records", null, t => + { + t.HasComment("库存锁定记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberDaySetting", 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("ExtraDiscountRate") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)") + .HasComment("会员日额外折扣(如 9 表示 9 折)。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .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("Weekday") + .HasColumnType("integer") + .HasComment("周几(1-7,对应周一到周日)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId") + .IsUnique(); + + b.ToTable("member_day_settings", null, t => + { + t.HasComment("会员日配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberMessageTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Category") + .HasColumnType("integer") + .HasComment("模板分类。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .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("LastUsedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近使用时间(UTC)。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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("UsageCount") + .HasColumnType("integer") + .HasComment("使用次数。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name") + .IsUnique(); + + b.HasIndex("TenantId", "Category", "UsageCount"); + + b.ToTable("member_message_templates", null, t => + { + t.HasComment("会员消息模板。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointLedger", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BalanceAfterChange") + .HasColumnType("integer") + .HasComment("变动后余额。"); + + b.Property("ChangeAmount") + .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("ExpireAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(如适用)。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("Reason") + .HasColumnType("integer") + .HasComment("变动原因。"); + + b.Property("SourceId") + .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", "MemberId", "OccurredAt"); + + b.ToTable("member_point_ledgers", null, t => + { + t.HasComment("积分变动流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointMallProduct", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CashAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("现金部分(积分+现金时使用)。"); + + b.Property("CouponTemplateId") + .HasColumnType("bigint") + .HasComment("关联优惠券模板 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("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("商品描述。"); + + b.Property("ExchangeType") + .HasColumnType("integer") + .HasComment("兑换方式(纯积分/积分+现金)。"); + + b.Property("ImageUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("展示图片地址。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("展示名称。"); + + b.Property("NotifyChannelsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("到账通知渠道(JSON 数组)。"); + + b.Property("PerMemberLimit") + .HasColumnType("integer") + .HasComment("每人限兑次数(null 表示不限)。"); + + b.Property("PhysicalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("实物名称(兑换实物时必填)。"); + + b.Property("PickupMethod") + .HasColumnType("integer") + .HasComment("实物领取方式。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("关联商品 ID(兑换商品时必填)。"); + + b.Property("RedeemType") + .HasColumnType("integer") + .HasComment("兑换类型。"); + + b.Property("RequiredPoints") + .HasColumnType("integer") + .HasComment("所需积分。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("上下架状态。"); + + b.Property("StockAvailable") + .HasColumnType("integer") + .HasComment("剩余库存数量。"); + + b.Property("StockTotal") + .HasColumnType("integer") + .HasComment("初始库存数量。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "CouponTemplateId"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.HasIndex("TenantId", "StoreId", "ProductId"); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("member_point_mall_products", null, t => + { + t.HasComment("会员积分商城兑换商品。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointMallRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CashAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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("ExchangeType") + .HasColumnType("integer") + .HasComment("兑换方式快照。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("发放时间(UTC)。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("MemberMobileMasked") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("会员手机号快照(脱敏)。"); + + b.Property("MemberName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会员名称快照。"); + + b.Property("PointMallProductId") + .HasColumnType("bigint") + .HasComment("关联积分商品 ID。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称快照。"); + + b.Property("RecordNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("兑换记录单号。"); + + b.Property("RedeemType") + .HasColumnType("integer") + .HasComment("兑换类型快照。"); + + b.Property("RedeemedAt") + .HasColumnType("timestamp with time zone") + .HasComment("兑换时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("记录状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("UsedPoints") + .HasColumnType("integer") + .HasComment("消耗积分。"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("核销时间(UTC)。"); + + b.Property("VerifiedBy") + .HasColumnType("bigint") + .HasComment("核销人用户标识。"); + + b.Property("VerifyMethod") + .HasColumnType("integer") + .HasComment("核销方式。"); + + b.Property("VerifyRemark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("核销备注。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "RecordNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "RedeemedAt"); + + b.HasIndex("TenantId", "StoreId", "MemberId", "RedeemedAt"); + + b.HasIndex("TenantId", "StoreId", "PointMallProductId", "RedeemedAt"); + + b.HasIndex("TenantId", "StoreId", "Status", "RedeemedAt"); + + b.ToTable("member_point_mall_records", null, t => + { + t.HasComment("会员积分商城兑换记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberPointMallRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConsumeAmountPerStep") + .HasColumnType("integer") + .HasComment("每消费多少元触发一次积分计算。"); + + b.Property("ConsumeRewardPointsPerStep") + .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("ExpiryMode") + .HasColumnType("integer") + .HasComment("积分有效期模式。"); + + b.Property("IsConsumeRewardEnabled") + .HasColumnType("boolean") + .HasComment("是否启用消费获取。"); + + b.Property("IsRegisterRewardEnabled") + .HasColumnType("boolean") + .HasComment("是否启用注册奖励。"); + + b.Property("IsReviewRewardEnabled") + .HasColumnType("boolean") + .HasComment("是否启用评价奖励。"); + + b.Property("IsSigninRewardEnabled") + .HasColumnType("boolean") + .HasComment("是否启用签到奖励。"); + + b.Property("RegisterRewardPoints") + .HasColumnType("integer") + .HasComment("注册奖励积分。"); + + b.Property("ReviewRewardPoints") + .HasColumnType("integer") + .HasComment("评价奖励积分。"); + + b.Property("SigninRewardPoints") + .HasColumnType("integer") + .HasComment("签到奖励积分。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId") + .IsUnique(); + + b.ToTable("member_point_mall_rules", null, t => + { + t.HasComment("会员积分商城规则配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarUrl") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像。"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone") + .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("GrowthValue") + .HasColumnType("integer") + .HasComment("成长值/经验值。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("注册时间。"); + + b.Property("MemberTierId") + .HasColumnType("bigint") + .HasComment("当前会员等级 ID。"); + + b.Property("Mobile") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("Nickname") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称。"); + + b.Property("PointsBalance") + .HasColumnType("integer") + .HasComment("会员积分余额。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会员状态。"); + + b.Property("StoredBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("储值余额。"); + + b.Property("StoredGiftBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("储值赠金余额。"); + + b.Property("StoredRechargeBalance") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MemberTierId"); + + b.HasIndex("TenantId", "Mobile") + .IsUnique(); + + b.ToTable("member_profiles", null, t => + { + t.HasComment("会员档案。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberProfileTag", 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("MemberProfileId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("TagName") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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", "MemberProfileId"); + + b.HasIndex("TenantId", "MemberProfileId", "TagName") + .IsUnique(); + + b.ToTable("member_profile_tags", null, t => + { + t.HasComment("会员标签。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberReachMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AudienceTagsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("目标标签 JSON(字符串数组)。"); + + b.Property("AudienceType") + .HasColumnType("integer") + .HasComment("目标人群类型。"); + + b.Property("ChannelsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("渠道数组 JSON(字符串枚举)。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("消息正文。"); + + b.Property("ConvertedCount") + .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("EstimatedReachCount") + .HasColumnType("integer") + .HasComment("预计触达人数。"); + + b.Property("HangfireJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("Hangfire 任务 ID。"); + + b.Property("LastError") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("最近失败摘要。"); + + b.Property("ReadCount") + .HasColumnType("integer") + .HasComment("已读数量。"); + + b.Property("ScheduleType") + .HasColumnType("integer") + .HasComment("发送时间类型。"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone") + .HasComment("定时发送时间(UTC)。"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasComment("实际发送时间(UTC)。"); + + b.Property("SentCount") + .HasColumnType("integer") + .HasComment("实际发送成功数量。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识(可空,空表示当前商户全部可见门店)。"); + + b.Property("TemplateId") + .HasColumnType("bigint") + .HasComment("模板标识(可空)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("消息标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "CreatedAt"); + + b.HasIndex("TenantId", "Status", "ScheduledAt"); + + b.ToTable("member_reach_messages", null, t => + { + t.HasComment("会员消息触达主记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberReachRecipient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("渠道。"); + + b.Property("ConvertedAt") + .HasColumnType("timestamp with time zone") + .HasComment("转化时间(UTC)。"); + + 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("ErrorMessage") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("失败摘要。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("MessageId") + .HasColumnType("bigint") + .HasComment("消息标识。"); + + b.Property("Mobile") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号快照。"); + + b.Property("OpenId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("微信 OpenId 快照。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("已读时间(UTC)。"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasComment("发送时间(UTC)。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "MessageId", "Status"); + + b.HasIndex("TenantId", "MessageId", "MemberId", "Channel") + .IsUnique(); + + b.ToTable("member_reach_recipients", null, t => + { + t.HasComment("会员消息触达收件明细。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberStoredCardPlan", 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("GiftAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("赠送金额。"); + + b.Property("RechargeAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("充值金额。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值(越小越靠前)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("启用状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "SortOrder"); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.HasIndex("TenantId", "StoreId", "RechargeAmount", "GiftAmount") + .IsUnique(); + + b.ToTable("member_stored_card_plans", null, t => + { + t.HasComment("会员储值卡充值方案。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberStoredCardRechargeRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ArrivedAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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("GiftAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("赠送金额。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("MemberMobileMasked") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("会员手机号快照(脱敏)。"); + + b.Property("MemberName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会员名称快照。"); + + b.Property("PaymentMethod") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("PlanId") + .HasColumnType("bigint") + .HasComment("充值方案标识(可空,表示非方案充值)。"); + + b.Property("RechargeAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("充值金额。"); + + b.Property("RechargedAt") + .HasColumnType("timestamp with time zone") + .HasComment("充值时间(UTC)。"); + + b.Property("RecordNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("充值单号。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "RechargedAt"); + + b.HasIndex("TenantId", "StoreId", "RecordNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "MemberId", "RechargedAt"); + + b.HasIndex("TenantId", "StoreId", "PlanId", "RechargedAt"); + + b.ToTable("member_stored_card_recharge_records", null, t => + { + t.HasComment("会员储值卡充值记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Membership.Entities.MemberTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BenefitsJson") + .IsRequired() + .HasColumnType("text") + .HasComment("等级权益(JSON)。"); + + b.Property("ColorHex") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .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("DowngradeWindowDays") + .HasColumnType("integer") + .HasComment("降级观察窗口天数。"); + + b.Property("IconKey") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("图标键。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认等级。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("等级名称。"); + + b.Property("RequiredGrowth") + .HasColumnType("integer") + .HasComment("所需成长值。"); + + b.Property("SortOrder") + .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("UpgradeAmountThreshold") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("升级累计消费门槛。"); + + b.Property("UpgradeOrderCountThreshold") + .HasColumnType("integer") + .HasComment("升级消费次数门槛。"); + + b.Property("UpgradeRuleType") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("升级规则类型(none/amount/count/both)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Name") + .IsUnique(); + + b.HasIndex("TenantId", "SortOrder"); + + b.ToTable("member_tiers", null, t => + { + t.HasComment("会员等级定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.Merchant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); + + b.Property("ApprovedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核通过时间。"); + + b.Property("ApprovedBy") + .HasColumnType("bigint") + .HasComment("审核通过人。"); + + b.Property("BrandAlias") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("品牌简称或别名。"); + + b.Property("BrandName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("品牌名称(对外展示)。"); + + b.Property("BusinessLicenseImageUrl") + .HasColumnType("text") + .HasComment("营业执照扫描件地址。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("营业执照号。"); + + b.Property("Category") + .HasColumnType("text") + .HasComment("品牌所属品类,如火锅、咖啡等。"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); + + b.Property("ClaimExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("领取过期时间。"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone") + .HasComment("领取时间。"); + + b.Property("ClaimedBy") + .HasColumnType("bigint") + .HasComment("当前领取人。"); + + b.Property("ClaimedByName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("当前领取人姓名。"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("联系邮箱。"); + + b.Property("ContactPhone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在区县。"); + + b.Property("FrozenAt") + .HasColumnType("timestamp with time zone") + .HasComment("冻结时间。"); + + b.Property("FrozenReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("冻结原因。"); + + b.Property("GeoFailReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("地理定位失败原因。"); + + b.Property("GeoNextRetryAt") + .HasColumnType("timestamp with time zone") + .HasComment("下次地理定位重试时间(UTC)。"); + + b.Property("GeoRetryCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位重试次数。"); + + b.Property("GeoStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位状态。"); + + b.Property("GeoUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("地理定位最近成功时间(UTC)。"); + + b.Property("IsFrozen") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否冻结业务。"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone") + .HasComment("入驻时间。"); + + b.Property("LastReviewedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次审核时间。"); + + b.Property("LastReviewedBy") + .HasColumnType("bigint") + .HasComment("最近一次审核人。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度信息。"); + + b.Property("LegalPerson") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("法人或负责人姓名。"); + + b.Property("LogoUrl") + .HasColumnType("text") + .HasComment("品牌 Logo。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度信息。"); + + b.Property("OperatingMode") + .HasColumnType("integer") + .HasComment("经营模式(同一主体/不同主体)。"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("审核备注或驳回原因。"); + + b.Property("ServicePhone") + .HasColumnType("text") + .HasComment("客服电话。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("入驻状态。"); + + b.Property("SupportEmail") + .HasColumnType("text") + .HasComment("客服邮箱。"); + + b.Property("TaxNumber") + .HasColumnType("text") + .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("xmin") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClaimedBy"); + + b.HasIndex("TenantId"); + + b.HasIndex("Longitude", "Latitude") + .HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL"); + + b.HasIndex("TenantId", "Status"); + + b.HasIndex("TenantId", "GeoStatus", "GeoNextRetryAt"); + + b.ToTable("merchants", null, t => + { + t.HasComment("商户主体信息,承载入驻和资质审核结果。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantCategory", 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("DisplayOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("显示顺序,越小越靠前。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否可用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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", "Name") + .IsUnique(); + + b.ToTable("merchant_categories", null, t => + { + t.HasComment("商户可选类目。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ContractNumber") + .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("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同结束时间。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("合同文件存储地址。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("SignedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签署时间。"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasComment("合同开始时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("合同状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TerminatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("终止时间。"); + + b.Property("TerminationReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("终止原因。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "ContractNumber") + .IsUnique(); + + b.ToTable("merchant_contracts", null, t => + { + t.HasComment("商户合同记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantDocument", 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("DocumentNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("证照编号。"); + + b.Property("DocumentType") + .HasColumnType("integer") + .HasComment("证照类型。"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("到期日期。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("证照文件链接。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("签发日期。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("Remarks") + .HasColumnType("text") + .HasComment("审核备注或驳回原因。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "DocumentType"); + + b.ToTable("merchant_documents", null, t => + { + t.HasComment("商户提交的资质或证照材料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantStaff", 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("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("邮箱地址。"); + + b.Property("IdentityUserId") + .HasColumnType("bigint") + .HasComment("登录账号 ID(指向统一身份体系)。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("员工姓名。"); + + b.Property("PermissionsJson") + .HasColumnType("text") + .HasComment("自定义权限(JSON)。"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("员工角色类型。"); + + b.Property("Status") + .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.HasKey("Id"); + + b.HasIndex("TenantId", "MerchantId", "Phone"); + + b.ToTable("merchant_staff", null, t => + { + t.HasComment("商户员工账号,支持门店维度分配。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.MapLocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .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("Landmark") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("打车/导航落点描述。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("名称。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("关联门店 ID,可空表示独立 POI。"); + + 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"); + + b.ToTable("map_locations", null, t => + { + t.HasComment("地图 POI 信息,用于门店定位和推荐。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Navigation.Entities.NavigationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("来源通道(小程序、H5 等)。"); + + 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("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("请求时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店 ID。"); + + b.Property("TargetApp") + .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("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId", "RequestedAt"); + + b.ToTable("navigation_requests", null, t => + { + t.HasComment("用户发起的导航请求日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .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("ProductId") + .HasColumnType("bigint") + .HasComment("商品或 SKU 标识。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称快照。"); + + b.Property("ProductSkuId") + .HasColumnType("bigint") + .HasComment("SKU 标识。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("自定义备注(口味要求)。"); + + b.Property("ShoppingCartId") + .HasColumnType("bigint") + .HasComment("所属购物车标识。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价快照。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ShoppingCartId"); + + b.ToTable("cart_items", null, t => + { + t.HasComment("购物车条目。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CartItemAddon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CartItemId") + .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("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("OptionId") + .HasColumnType("bigint") + .HasComment("选项 ID(可对应 ProductAddonOption)。"); + + 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.ToTable("cart_item_addons", null, t => + { + t.HasComment("购物车条目的加料/附加项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.CheckoutSession", 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("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(UTC)。"); + + b.Property("SessionToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("会话 Token。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("会话状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.Property("ValidationResultJson") + .IsRequired() + .HasColumnType("text") + .HasComment("校验结果明细 JSON。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SessionToken") + .IsUnique(); + + b.ToTable("checkout_sessions", null, t => + { + t.HasComment("结账会话,记录校验上下文。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Ordering.Entities.ShoppingCart", 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("DeliveryPreference") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("履约方式(堂食/自提/配送)缓存。"); + + b.Property("LastModifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次修改时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("购物车状态,包含正常/锁定。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TableContext") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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("UserId") + .HasColumnType("bigint") + .HasComment("用户标识。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "UserId", "StoreId") + .IsUnique(); + + b.ToTable("shopping_carts", null, t => + { + t.HasComment("用户购物车,按租户/门店隔离。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("取消原因。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("Channel") + .HasColumnType("integer") + .HasComment("下单渠道。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("顾客姓名。"); + + b.Property("CustomerPhone") + .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("DeliveryType") + .HasColumnType("integer") + .HasComment("履约类型。"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("优惠金额。"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间。"); + + b.Property("ItemsAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("商品总额。"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("订单号。"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); + + b.Property("PayableAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("应付金额。"); + + b.Property("PaymentStatus") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("QueueNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("排队号(如有)。"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("ReservationId") + .HasColumnType("bigint") + .HasComment("预约 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店。"); + + b.Property("TableNo") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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", "OrderNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "Status"); + + b.ToTable("orders", null, t => + { + t.HasComment("交易订单。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .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("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("折扣金额。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("ProductName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("SkuName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("SKU/规格描述。"); + + b.Property("SubTotal") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("小计。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("单位。"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("单价。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("order_items", null, t => + { + t.HasComment("订单明细。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderStatusHistory", 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("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注信息。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .HasComment("发生时间。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人标识(可为空表示系统)。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("订单标识。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId", "OccurredAt"); + + b.ToTable("order_status_histories", null, t => + { + t.HasComment("订单状态流转记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.RefundRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单标识。"); + + b.Property("ProcessedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核完成时间。"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("申请原因。"); + + b.Property("RefundNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("退款单号。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("用户提交时间。"); + + b.Property("ReviewNotes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("审核备注。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "RefundNo") + .IsUnique(); + + b.ToTable("refund_requests", null, t => + { + t.HasComment("售后/退款申请。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); + + b.Property("ChannelTransactionId") + .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("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付完成时间。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("原始回调内容。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("错误/备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TradeNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("系统交易号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "OrderId"); + + b.ToTable("payment_records", null, t => + { + t.HasComment("支付流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Payments.Entities.PaymentRefundRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("退款金额。"); + + b.Property("ChannelRefundId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("渠道退款流水号。"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone") + .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("OrderId") + .HasColumnType("bigint") + .HasComment("关联订单标识。"); + + b.Property("Payload") + .HasColumnType("text") + .HasComment("渠道返回的原始数据 JSON。"); + + b.Property("PaymentRecordId") + .HasColumnType("bigint") + .HasComment("原支付记录标识。"); + + b.Property("RequestedAt") + .HasColumnType("timestamp with time zone") + .HasComment("退款请求时间。"); + + 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.HasKey("Id"); + + b.HasIndex("TenantId", "PaymentRecordId"); + + b.ToTable("payment_refund_records", null, t => + { + t.HasComment("支付渠道退款流水。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategoryId") + .HasColumnType("bigint") + .HasComment("所属分类。"); + + b.Property("CoverImage") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .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") + .HasColumnType("text") + .HasComment("商品描述。"); + + b.Property("EnableDelivery") + .HasColumnType("boolean") + .HasComment("支持配送。"); + + b.Property("EnableDineIn") + .HasColumnType("boolean") + .HasComment("支持堂食。"); + + b.Property("EnablePickup") + .HasColumnType("boolean") + .HasComment("支持自提。"); + + b.Property("GalleryImages") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("Gallery 图片逗号分隔。"); + + b.Property("IsFeatured") + .HasColumnType("boolean") + .HasComment("是否热门推荐。"); + + b.Property("Kind") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("商品类型。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("最大每单限购。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("商品名称。"); + + b.Property("NotifyManager") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否通知店长。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("PackingFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("打包费(元/份)。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("现价。"); + + b.Property("RecoverAt") + .HasColumnType("timestamp with time zone") + .HasComment("沽清恢复时间。"); + + b.Property("RemainStock") + .HasColumnType("integer") + .HasComment("沽清后剩余可售。"); + + b.Property("SalesMonthly") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("月销量。"); + + b.Property("SoldoutMode") + .HasColumnType("integer") + .HasComment("沽清模式。"); + + b.Property("SoldoutReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("沽清原因。"); + + b.Property("SortWeight") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("排序权重,越大越靠前。"); + + b.Property("SpuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("商品编码。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("商品状态。"); + + b.Property("StockQuantity") + .HasColumnType("integer") + .HasComment("库存数量(可选)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店。"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("副标题/卖点。"); + + b.Property("SyncToPlatform") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否同步通知外卖平台。"); + + b.Property("TagsJson") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[]") + .HasComment("标签 JSON(字符串数组)。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TimedOnShelfAt") + .HasColumnType("timestamp with time zone") + .HasComment("定时上架时间。"); + + b.Property("Unit") + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("售卖单位(份/杯等)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("WarningStock") + .HasColumnType("integer") + .HasComment("库存预警值。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "SpuCode") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("products", null, t => + { + t.HasComment("商品(SPU)信息。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonGroup", 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("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("MaxSelect") + .HasColumnType("integer") + .HasComment("最大选择数量。"); + + b.Property("MinSelect") + .HasColumnType("integer") + .HasComment("最小选择数量。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型。"); + + b.Property("SortOrder") + .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", "ProductId", "Name"); + + b.ToTable("product_addon_groups", null, t => + { + t.HasComment("加料/做法分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAddonOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddonGroupId") + .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("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选项。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .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.ToTable("product_addon_options", null, t => + { + t.HasComment("加料选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeGroup", 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("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称,例如“辣度”“份量”。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品标识。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择类型(单选/多选)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("显示排序。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "Name"); + + b.ToTable("product_attribute_groups", null, t => + { + t.HasComment("商品规格/属性分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductAttributeOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributeGroupId") + .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("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsDefault") + .HasColumnType("boolean") + .HasComment("是否默认选中。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .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", "AttributeGroupId", "Name") + .IsUnique(); + + b.ToTable("product_attribute_options", null, t => + { + t.HasComment("商品规格选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelsJson") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("[\"wm\"]") + .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("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("分类描述。"); + + b.Property("Icon") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("分类图标。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分类名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId"); + + b.ToTable("product_categories", null, t => + { + t.HasComment("商品分类。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductComboGroup", 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("MaxSelect") + .HasColumnType("integer") + .HasComment("最大选择数。"); + + b.Property("MinSelect") + .HasColumnType("integer") + .HasComment("最小选择数。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("分组名称。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("套餐商品 ID。"); + + b.Property("SortOrder") + .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", "ProductId", "Name"); + + b.HasIndex("TenantId", "ProductId", "SortOrder"); + + b.ToTable("product_combo_groups", null, t => + { + t.HasComment("套餐分组。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductComboGroupItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ComboGroupId") + .HasColumnType("bigint") + .HasComment("所属套餐分组 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("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("Quantity") + .HasColumnType("integer") + .HasComment("数量。"); + + b.Property("SortOrder") + .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", "ComboGroupId", "ProductId") + .IsUnique(); + + b.HasIndex("TenantId", "ComboGroupId", "SortOrder"); + + b.ToTable("product_combo_group_items", null, t => + { + t.HasComment("套餐分组内商品。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductLabel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Color") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("标签颜色(HEX)。"); + + 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("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("标签名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "Name") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId", "IsEnabled", "SortOrder"); + + b.ToTable("product_labels", null, t => + { + t.HasComment("商品标签模板(门店维度)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductLabelProduct", 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("LabelId") + .HasColumnType("bigint") + .HasComment("标签 ID。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "ProductId"); + + b.HasIndex("TenantId", "StoreId", "LabelId", "ProductId") + .IsUnique(); + + b.ToTable("product_label_products", null, t => + { + t.HasComment("标签与商品关联关系。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductMediaAsset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Caption") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .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("MediaType") + .HasColumnType("integer") + .HasComment("媒体类型。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("商品标识。"); + + b.Property("SortOrder") + .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("Url") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("媒资链接。"); + + b.HasKey("Id"); + + b.ToTable("product_media_assets", null, t => + { + t.HasComment("商品媒资素材。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductPricingRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionsJson") + .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("EndTime") + .HasColumnType("timestamp with time zone") + .HasComment("生效结束时间。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("特殊价格。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品。"); + + b.Property("RuleType") + .HasColumnType("integer") + .HasComment("策略类型。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .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("WeekdaysJson") + .HasColumnType("text") + .HasComment("生效星期(JSON 数组)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "RuleType"); + + b.ToTable("product_pricing_rules", null, t => + { + t.HasComment("商品价格策略,支持会员价/时段价等。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSchedule", 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("EndTime") + .HasColumnType("interval") + .HasComment("结束时间。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("规则名称。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("WeekDaysMask") + .HasColumnType("integer") + .HasComment("星期位掩码(周一到周日)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "IsEnabled"); + + b.HasIndex("TenantId", "StoreId", "Name") + .IsUnique(); + + b.ToTable("product_schedules", null, t => + { + t.HasComment("商品时段规则(门店维度)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductScheduleProduct", 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("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("ScheduleId") + .HasColumnType("bigint") + .HasComment("时段规则 ID。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "ProductId"); + + b.HasIndex("TenantId", "StoreId", "ScheduleId", "ProductId") + .IsUnique(); + + b.ToTable("product_schedule_products", null, t => + { + t.HasComment("时段规则与商品关联关系。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSku", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AttributesJson") + .IsRequired() + .HasColumnType("text") + .HasComment("规格属性 JSON(记录选项 ID)。"); + + b.Property("Barcode") + .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("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用。"); + + b.Property("OriginalPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("原价。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("售价。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品标识。"); + + b.Property("SkuCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("SKU 编码。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StockQuantity") + .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("Weight") + .HasPrecision(10, 3) + .HasColumnType("numeric(10,3)") + .HasComment("重量(千克)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId"); + + b.HasIndex("TenantId", "SkuCode") + .IsUnique(); + + b.ToTable("product_skus", null, t => + { + t.HasComment("商品 SKU,记录具体规格组合价格。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSkuSaveJob", 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("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasComment("失败摘要。"); + + b.Property("FailedCount") + .HasColumnType("integer") + .HasComment("失败条数。"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone") + .HasComment("完成时间(UTC)。"); + + b.Property("HangfireJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("Hangfire 任务 ID。"); + + b.Property("Mode") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("任务模式(当前固定 replace)。"); + + b.Property("PayloadJson") + .IsRequired() + .HasColumnType("text") + .HasComment("任务请求负载 JSON 快照。"); + + b.Property("ProductId") + .HasColumnType("bigint") + .HasComment("所属商品 ID。"); + + b.Property("ProgressProcessed") + .HasColumnType("integer") + .HasComment("已处理数。"); + + b.Property("ProgressTotal") + .HasColumnType("integer") + .HasComment("总处理数。"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone") + .HasComment("开始执行时间(UTC)。"); + + b.Property("Status") + .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.HasKey("Id"); + + b.HasIndex("TenantId", "ProductId", "CreatedAt"); + + b.HasIndex("TenantId", "Status", "CreatedAt"); + + b.ToTable("product_sku_save_jobs", null, t => + { + t.HasComment("商品 SKU 异步保存任务。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSpecTemplate", 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("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("模板描述。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasComment("是否必选。"); + + b.Property("MaxSelect") + .HasColumnType("integer") + .HasComment("最大可选数。"); + + b.Property("MinSelect") + .HasColumnType("integer") + .HasComment("最小可选数。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("模板名称。"); + + b.Property("SelectionType") + .HasColumnType("integer") + .HasComment("选择方式。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店。"); + + b.Property("TemplateType") + .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", "StoreId", "TemplateType", "IsEnabled"); + + b.HasIndex("TenantId", "StoreId", "TemplateType", "Name") + .IsUnique(); + + b.ToTable("product_spec_templates", null, t => + { + t.HasComment("门店规格做法模板。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSpecTemplateOption", 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("ExtraPrice") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("附加价格。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("选项名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("Stock") + .HasColumnType("integer") + .HasComment("库存数量。"); + + b.Property("TemplateId") + .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", "TemplateId", "Name") + .IsUnique(); + + b.ToTable("product_spec_template_options", null, t => + { + t.HasComment("规格做法模板选项。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Products.Entities.ProductSpecTemplateProduct", 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("ProductId") + .HasColumnType("bigint") + .HasComment("商品 ID。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("所属门店。"); + + b.Property("TemplateId") + .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", "ProductId"); + + b.HasIndex("TenantId", "StoreId", "TemplateId", "ProductId") + .IsUnique(); + + b.ToTable("product_spec_template_products", null, t => + { + t.HasComment("规格做法模板与商品关联。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Queues.Entities.QueueTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CalledAt") + .HasColumnType("timestamp with time zone") + .HasComment("叫号时间。"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .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("EstimatedWaitMinutes") + .HasColumnType("integer") + .HasComment("预计等待分钟。"); + + b.Property("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasComment("过号时间。"); + + b.Property("PartySize") + .HasColumnType("integer") + .HasComment("就餐人数。"); + + b.Property("Remark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("获取或设置所属门店 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TicketNumber") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("排队编号。"); + + 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"); + + b.HasIndex("TenantId", "StoreId", "TicketNumber") + .IsUnique(); + + b.ToTable("queue_tickets", null, t => + { + t.HasComment("排队叫号。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Reservations.Entities.Reservation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone") + .HasComment("取消时间。"); + + b.Property("CheckInCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("核销码/到店码。"); + + b.Property("CheckedInAt") + .HasColumnType("timestamp with time zone") + .HasComment("实际签到时间。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CustomerName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("客户姓名。"); + + b.Property("CustomerPhone") + .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("PeopleCount") + .HasColumnType("integer") + .HasComment("用餐人数。"); + + b.Property("Remark") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("ReservationNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("预约号。"); + + b.Property("ReservationTime") + .HasColumnType("timestamp with time zone") + .HasComment("预约时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店。"); + + b.Property("TablePreference") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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", "ReservationNo") + .IsUnique(); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("reservations", null, t => + { + t.HasComment("预约/预订记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.Store", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActivatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核通过时间。"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("详细地址。"); + + b.Property("Announcement") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("门店公告。"); + + b.Property("AuditStatus") + .HasColumnType("integer") + .HasComment("审核状态。"); + + b.Property("BusinessHours") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("门店营业时段描述(备用字符串)。"); + + b.Property("BusinessLicenseImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("门店营业执照图片地址(主体不一致模式使用)。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("门店营业执照号(主体不一致模式使用)。"); + + b.Property("BusinessStatus") + .HasColumnType("integer") + .HasComment("经营状态。"); + + b.Property("CategoryId") + .HasColumnType("bigint") + .HasComment("行业类目 ID。"); + + b.Property("City") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在城市。"); + + b.Property("ClosureReason") + .HasColumnType("integer") + .HasComment("歇业原因。"); + + b.Property("ClosureReasonText") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("歇业原因补充说明。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("门店编码,便于扫码及外部对接。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家或地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .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("DeliveryRadiusKm") + .HasPrecision(6, 2) + .HasColumnType("numeric(6,2)") + .HasComment("默认配送半径(公里)。"); + + b.Property("Description") + .HasColumnType("text") + .HasComment("门店描述或公告。"); + + b.Property("District") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区县信息。"); + + b.Property("ForceCloseReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("强制关闭原因。"); + + b.Property("ForceClosedAt") + .HasColumnType("timestamp with time zone") + .HasComment("强制关闭时间。"); + + b.Property("GeoFailReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("地理定位失败原因。"); + + b.Property("GeoNextRetryAt") + .HasColumnType("timestamp with time zone") + .HasComment("下次地理定位重试时间(UTC)。"); + + b.Property("GeoRetryCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位重试次数。"); + + b.Property("GeoStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位状态。"); + + b.Property("GeoUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("地理定位最近成功时间(UTC)。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度。"); + + b.Property("LegalRepresentative") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("门店法人(主体不一致模式使用)。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("高德/腾讯地图经度。"); + + b.Property("ManagerName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("门店负责人姓名。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户标识。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("门店名称。"); + + b.Property("OwnershipType") + .HasColumnType("integer") + .HasComment("主体类型。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("联系电话。"); + + b.Property("Province") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所在省份。"); + + b.Property("RegisteredAddress") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("门店注册地址(主体不一致模式使用)。"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("审核驳回原因。"); + + b.Property("SignboardImageUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("门头招牌图 URL。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("门店当前运营状态。"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasComment("提交审核时间。"); + + b.Property("SupportsDelivery") + .HasColumnType("boolean") + .HasComment("是否支持配送。"); + + b.Property("SupportsDineIn") + .HasColumnType("boolean") + .HasComment("是否支持堂食。"); + + b.Property("SupportsPickup") + .HasColumnType("boolean") + .HasComment("是否支持自提。"); + + b.Property("SupportsQueueing") + .HasColumnType("boolean") + .HasComment("支持排队叫号。"); + + b.Property("SupportsReservation") + .HasColumnType("boolean") + .HasComment("支持预约。"); + + b.Property("Tags") + .HasColumnType("text") + .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("Longitude", "Latitude") + .HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL"); + + b.HasIndex("MerchantId", "BusinessLicenseNumber") + .IsUnique() + .HasFilter("\"BusinessLicenseNumber\" IS NOT NULL AND \"Status\" <> 3"); + + b.HasIndex("TenantId", "AuditStatus"); + + b.HasIndex("TenantId", "BusinessStatus"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "MerchantId"); + + b.HasIndex("TenantId", "OwnershipType"); + + b.HasIndex("TenantId", "MerchantId", "GeoStatus", "GeoNextRetryAt"); + + b.ToTable("stores", null, t => + { + t.HasComment("门店信息,承载营业配置与能力。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreAuditRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .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("NewStatus") + .HasColumnType("integer") + .HasComment("操作后状态。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人 ID。"); + + b.Property("OperatorName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("操作人名称。"); + + b.Property("PreviousStatus") + .HasColumnType("integer") + .HasComment("操作前状态。"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("驳回理由文本。"); + + b.Property("RejectionReasonId") + .HasColumnType("bigint") + .HasComment("驳回理由 ID。"); + + b.Property("Remarks") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasComment("备注。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("CreatedAt"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("store_audit_records", null, t => + { + t.HasComment("门店审核记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreBusinessHour", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapacityLimit") + .HasColumnType("integer") + .HasComment("最大接待容量或单量限制。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期几,0 表示周日。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(本地时间)。"); + + b.Property("HourType") + .HasColumnType("integer") + .HasComment("时段类型(正常营业、休息、预约等)。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(本地时间)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "DayOfWeek"); + + b.ToTable("store_business_hours", null, t => + { + t.HasComment("门店营业时段配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliverySetting", 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("EtaAdjustmentMinutes") + .HasColumnType("integer") + .HasComment("配送时效加成(分钟)。"); + + b.Property("FreeDeliveryThreshold") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("免配送费门槛。"); + + b.Property("HourlyCapacityLimit") + .HasColumnType("integer") + .HasComment("每小时配送上限。"); + + b.Property("MaxDeliveryDistance") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("最大配送距离(公里)。"); + + b.Property("Mode") + .HasColumnType("integer") + .HasComment("配送模式。"); + + b.Property("RadiusCenterLatitude") + .HasPrecision(10, 7) + .HasColumnType("numeric(10,7)") + .HasComment("半径配送中心点纬度。"); + + b.Property("RadiusCenterLongitude") + .HasPrecision(10, 7) + .HasColumnType("numeric(10,7)") + .HasComment("半径配送中心点经度。"); + + b.Property("RadiusTiersJson") + .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("store_delivery_settings", null, t => + { + t.HasComment("门店配送设置聚合。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDeliveryZone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Color") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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("DeliveryFee") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配送费。"); + + b.Property("EstimatedMinutes") + .HasColumnType("integer") + .HasComment("预计送达分钟。"); + + b.Property("MinimumOrderAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("起送价。"); + + b.Property("PolygonGeoJson") + .IsRequired() + .HasColumnType("text") + .HasComment("GeoJSON 表示的多边形范围。"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("优先级(数值越小越优先)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("ZoneName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "ZoneName"); + + b.ToTable("store_delivery_zones", null, t => + { + t.HasComment("门店配送范围配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreDineInSetting", 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("DefaultDiningMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(90) + .HasComment("默认用餐时长(分钟)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用堂食。"); + + b.Property("OvertimeReminderMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(10) + .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") + .IsUnique(); + + b.ToTable("store_dinein_settings", null, t => + { + t.HasComment("门店堂食基础设置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreEmployeeShift", 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("EndTime") + .HasColumnType("interval") + .HasComment("结束时间。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("RoleType") + .HasColumnType("integer") + .HasComment("排班角色。"); + + b.Property("ShiftDate") + .HasColumnType("timestamp with time zone") + .HasComment("班次日期。"); + + b.Property("StaffId") + .HasColumnType("bigint") + .HasComment("员工标识。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "ShiftDate", "StaffId") + .IsUnique(); + + b.ToTable("store_employee_shifts", null, t => + { + t.HasComment("门店员工排班记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreFee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BaseDeliveryFee") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("基础配送费(元)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CutleryFeeAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("餐具费金额。"); + + b.Property("CutleryFeeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否启用餐具费。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("FixedPackagingFee") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("固定打包费(总计模式有效)。"); + + b.Property("FreeDeliveryThreshold") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("免配送费门槛。"); + + b.Property("MinimumOrderAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("起送费(元)。"); + + b.Property("OrderPackagingFeeMode") + .HasColumnType("integer") + .HasComment("订单打包费规则(按订单收费时生效)。"); + + b.Property("PackagingFeeMode") + .HasColumnType("integer") + .HasComment("打包费模式。"); + + b.Property("PackagingFeeTiersJson") + .HasColumnType("text") + .HasComment("阶梯打包费配置(JSON)。"); + + b.Property("RushFeeAmount") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)") + .HasComment("加急费金额。"); + + b.Property("RushFeeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否启用加急费。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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"); + + b.HasIndex("TenantId", "StoreId") + .IsUnique(); + + b.ToTable("store_fees", null, t => + { + t.HasComment("门店费用配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreHoliday", 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("Date") + .HasColumnType("timestamp with time zone") + .HasComment("开始日期(原 Date 字段)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasComment("结束日期(可选,用于日期范围,如春节 1.28~2.4)。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(IsAllDay=false 时使用)。"); + + b.Property("IsAllDay") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否全天生效。true=全天;false=仅 StartTime~EndTime 时段。"); + + b.Property("IsClosed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否闭店(兼容旧数据,新逻辑请用 OverrideType)。"); + + b.Property("OverrideType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("覆盖类型(闭店/临时营业/调整时间)。"); + + b.Property("Reason") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasComment("说明内容。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("开始时间(IsAllDay=false 时使用)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "Date"); + + b.ToTable("store_holidays", null, t => + { + t.HasComment("门店临时时段配置(节假日/歇业/调整营业时间)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StorePickupSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowDaysAhead") + .HasColumnType("integer") + .HasComment("可预约天数(含当天)。"); + + b.Property("AllowToday") + .HasColumnType("boolean") + .HasComment("是否允许当天自提。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DefaultCutoffMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasComment("默认截单分钟(开始前多少分钟截止)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("FineRuleJson") + .HasColumnType("text") + .HasComment("精细规则 JSON。"); + + b.Property("MaxQuantityPerOrder") + .HasColumnType("integer") + .HasComment("单笔自提最大份数。"); + + b.Property("Mode") + .HasColumnType("integer") + .HasComment("自提配置模式。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId") + .IsUnique(); + + b.ToTable("store_pickup_settings", null, t => + { + t.HasComment("门店自提配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StorePickupSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Capacity") + .HasColumnType("integer") + .HasComment("容量(份数)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CutoffMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasComment("截单分钟(开始前多少分钟截止)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("当天结束时间(UTC)。"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("档期名称。"); + + b.Property("ReservedCount") + .HasColumnType("integer") + .HasComment("已占用数量。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("StartTime") + .HasColumnType("interval") + .HasComment("当天开始时间(UTC)。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("Weekdays") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("适用星期(逗号分隔 1-7)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "StoreId", "Name"); + + b.ToTable("store_pickup_slots", null, t => + { + t.HasComment("门店自提档期。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreQualification", 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("DocumentNumber") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("证照编号。"); + + b.Property("ExpiresAt") + .HasColumnType("date") + .HasComment("到期日期。"); + + b.Property("FileUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("证照文件 URL。"); + + b.Property("IssuedAt") + .HasColumnType("date") + .HasComment("签发日期。"); + + b.Property("QualificationType") + .HasColumnType("integer") + .HasComment("资质类型。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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("ExpiresAt") + .HasFilter("\"ExpiresAt\" IS NOT NULL"); + + b.HasIndex("TenantId", "StoreId"); + + b.ToTable("store_qualifications", null, t => + { + t.HasComment("门店资质证照。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreStaffTemplate", 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("EveningEndTime") + .HasColumnType("interval") + .HasComment("晚班结束时间。"); + + b.Property("EveningStartTime") + .HasColumnType("interval") + .HasComment("晚班开始时间。"); + + b.Property("FullEndTime") + .HasColumnType("interval") + .HasComment("全天班结束时间。"); + + b.Property("FullStartTime") + .HasColumnType("interval") + .HasComment("全天班开始时间。"); + + b.Property("MorningEndTime") + .HasColumnType("interval") + .HasComment("早班结束时间。"); + + b.Property("MorningStartTime") + .HasColumnType("interval") + .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") + .IsUnique(); + + b.ToTable("store_staff_templates", null, t => + { + t.HasComment("门店员工班次模板。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreStaffWeeklySchedule", 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("DayOfWeek") + .HasColumnType("integer") + .HasComment("星期(0=周一,6=周日)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EndTime") + .HasColumnType("interval") + .HasComment("结束时间(休息时为空)。"); + + b.Property("ShiftType") + .HasColumnType("integer") + .HasComment("班次类型。"); + + b.Property("StaffId") + .HasColumnType("bigint") + .HasComment("员工 ID。"); + + b.Property("StartTime") + .HasColumnType("interval") + .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", "DayOfWeek"); + + b.HasIndex("TenantId", "StoreId", "StaffId", "DayOfWeek") + .IsUnique(); + + b.ToTable("store_staff_weekly_schedules", null, t => + { + t.HasComment("门店员工每周排班。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AreaId") + .HasColumnType("bigint") + .HasComment("所在区域 ID。"); + + b.Property("Capacity") + .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("QrCodeUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("桌码二维码地址。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前桌台状态。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .HasComment("门店标识。"); + + b.Property("TableCode") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("桌码。"); + + b.Property("Tags") + .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", "StoreId", "TableCode") + .IsUnique(); + + b.ToTable("store_tables", null, t => + { + t.HasComment("桌台信息与二维码绑定。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Stores.Entities.StoreTableArea", 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("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("区域描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("区域名称。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值。"); + + b.Property("StoreId") + .HasColumnType("bigint") + .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", "StoreId", "Name") + .IsUnique(); + + b.ToTable("store_table_areas", null, t => + { + t.HasComment("门店桌台区域配置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.QuotaPackage", 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("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("描述。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否上架。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("配额包名称。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("价格。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("配额数值。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("排序。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("QuotaType", "IsActive", "SortOrder"); + + b.ToTable("quota_packages", null, t => + { + t.HasComment("配额包定义(系统提供的可购买配额包)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("text") + .HasComment("详细地址信息。"); + + b.Property("City") + .HasColumnType("text") + .HasComment("所在城市。"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("租户短编码,作为跨系统引用的唯一标识。"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("主联系人邮箱。"); + + b.Property("ContactName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("主联系人姓名。"); + + b.Property("ContactPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("主联系人电话。"); + + b.Property("Country") + .HasColumnType("text") + .HasComment("所在国家/地区。"); + + b.Property("CoverImageUrl") + .HasColumnType("text") + .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("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("服务生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("服务到期时间(UTC)。"); + + b.Property("GeoFailReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("地理定位失败原因。"); + + b.Property("GeoNextRetryAt") + .HasColumnType("timestamp with time zone") + .HasComment("下次地理定位重试时间(UTC)。"); + + b.Property("GeoRetryCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位重试次数。"); + + b.Property("GeoStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("地理定位状态。"); + + b.Property("GeoUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("地理定位最近成功时间(UTC)。"); + + b.Property("Industry") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("所属行业,如餐饮、零售等。"); + + b.Property("Latitude") + .HasColumnType("double precision") + .HasComment("纬度信息。"); + + b.Property("LegalEntityName") + .HasColumnType("text") + .HasComment("法人或公司主体名称。"); + + b.Property("LogoUrl") + .HasColumnType("text") + .HasComment("LOGO 图片地址。"); + + b.Property("Longitude") + .HasColumnType("double precision") + .HasComment("经度信息。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("租户全称或品牌名称。"); + + b.Property("OperatingMode") + .HasColumnType("integer") + .HasComment("经营模式(同一主体/不同主体)。"); + + b.Property("PrimaryOwnerUserId") + .HasColumnType("bigint") + .HasComment("系统内对应的租户所有者账号 ID。"); + + b.Property("Province") + .HasColumnType("text") + .HasComment("所在省份或州。"); + + b.Property("Remarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息,用于运营记录特殊说明。"); + + b.Property("ShortName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("对外展示的简称。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("租户当前状态,涵盖审核、启用、停用等场景。"); + + b.Property("SuspendedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次暂停服务时间。"); + + b.Property("SuspensionReason") + .HasColumnType("text") + .HasComment("暂停或终止的原因说明。"); + + b.Property("Tags") + .HasColumnType("text") + .HasComment("业务标签集合(逗号分隔)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("Website") + .HasColumnType("text") + .HasComment("官网或主要宣传链接。"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ContactPhone") + .IsUnique(); + + b.HasIndex("GeoStatus", "GeoNextRetryAt"); + + b.HasIndex("Longitude", "Latitude") + .HasFilter("\"Longitude\" IS NOT NULL AND \"Latitude\" IS NOT NULL"); + + b.ToTable("tenants", null, t => + { + t.HasComment("租户信息,描述租户的生命周期与基础资料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnnouncementType") + .HasColumnType("integer") + .HasComment("公告类型。"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("公告正文(可为 Markdown/HTML,前端自行渲染)。"); + + 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("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("失效时间(UTC),为空表示长期有效。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否启用(已弃用,迁移期保留)。"); + + b.Property("Priority") + .HasColumnType("integer") + .HasComment("展示优先级,数值越大越靠前。"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone") + .HasComment("实际发布时间(UTC)。"); + + b.Property("PublisherScope") + .HasColumnType("integer") + .HasComment("发布者范围。"); + + b.Property("PublisherUserId") + .HasColumnType("bigint") + .HasComment("发布者用户 ID(系统或租户后台账号)。"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone") + .HasComment("撤销时间(UTC)。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("ScheduledPublishAt") + .HasColumnType("timestamp with time zone") + .HasComment("预定发布时间(UTC)。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("公告状态。"); + + b.Property("TargetParameters") + .HasColumnType("text") + .HasComment("目标受众参数(JSON)。"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("目标受众类型。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("公告标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("Status", "EffectiveFrom") + .HasFilter("\"TenantId\" = 0"); + + b.HasIndex("TenantId", "AnnouncementType", "IsActive"); + + b.HasIndex("TenantId", "EffectiveFrom", "EffectiveTo"); + + b.HasIndex("TenantId", "Status", "EffectiveFrom"); + + b.ToTable("tenant_announcements", null, t => + { + t.HasComment("租户公告。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAnnouncementRead", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AnnouncementId") + .HasColumnType("bigint") + .HasComment("公告 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("ReadAt") + .HasColumnType("timestamp with time zone") + .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("UserId") + .HasColumnType("bigint") + .HasComment("已读用户 ID(后台账号),为空表示租户级已读。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "AnnouncementId", "UserId") + .IsUnique(); + + b.ToTable("tenant_announcement_reads", null, t => + { + t.HasComment("租户公告已读记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantBillingStatement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountDue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("应付金额(原始金额)。"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("实付金额。"); + + b.Property("BillingType") + .HasColumnType("integer") + .HasComment("账单类型(订阅账单/配额包账单/手动账单/续费账单)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasDefaultValue("CNY") + .HasComment("货币类型(默认 CNY)。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DiscountAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("折扣金额。"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone") + .HasComment("到期日。"); + + b.Property("LineItemsJson") + .HasColumnType("text") + .HasComment("账单明细 JSON,记录各项费用。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息(如:人工备注、取消原因等)。"); + + b.Property("OverdueNotifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("逾期通知时间。"); + + b.Property("PeriodEnd") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期结束时间。"); + + b.Property("PeriodStart") + .HasColumnType("timestamp with time zone") + .HasComment("账单周期开始时间。"); + + b.Property("ReminderSentAt") + .HasColumnType("timestamp with time zone") + .HasComment("提醒发送时间(续费提醒、逾期提醒等)。"); + + b.Property("StatementNo") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("账单编号,供对账查询。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("当前付款状态。"); + + b.Property("SubscriptionId") + .HasColumnType("bigint") + .HasComment("关联的订阅 ID(仅当 BillingType 为 Subscription 或 Renewal 时有值)。"); + + b.Property("TaxAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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("CreatedAt") + .HasDatabaseName("idx_billing_created_at"); + + b.HasIndex("Status", "DueDate") + .HasDatabaseName("idx_billing_status_duedate") + .HasFilter("\"Status\" IN (0, 2)"); + + b.HasIndex("TenantId", "StatementNo") + .IsUnique(); + + b.HasIndex("TenantId", "Status", "DueDate") + .HasDatabaseName("idx_billing_tenant_status_duedate"); + + b.ToTable("tenant_billing_statements", null, t => + { + t.HasComment("租户账单,用于呈现周期性收费。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantInvoiceRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("开票金额。"); + + b.Property("ApplicantName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("申请人。"); + + b.Property("AppliedAt") + .HasColumnType("timestamp with time zone") + .HasComment("申请时间(UTC)。"); + + b.Property("ApplyRemark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("申请备注。"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("开票抬头(公司名)。"); + + b.Property("ContactEmail") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("接收邮箱。"); + + b.Property("ContactPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .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("InvoiceNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("发票号码。"); + + b.Property("InvoiceType") + .HasColumnType("integer") + .HasComment("发票类型。"); + + b.Property("IssueRemark") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("开票备注。"); + + b.Property("IssuedAt") + .HasColumnType("timestamp with time zone") + .HasComment("开票时间(UTC)。"); + + b.Property("IssuedByUserId") + .HasColumnType("bigint") + .HasComment("开票人 ID。"); + + b.Property("OrderNo") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("关联订单号。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("发票状态。"); + + b.Property("TaxpayerNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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("VoidReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("作废原因。"); + + b.Property("VoidedAt") + .HasColumnType("timestamp with time zone") + .HasComment("作废时间(UTC)。"); + + b.Property("VoidedByUserId") + .HasColumnType("bigint") + .HasComment("作废人 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "InvoiceNo") + .IsUnique(); + + b.HasIndex("TenantId", "OrderNo"); + + b.HasIndex("TenantId", "InvoiceType", "AppliedAt"); + + b.HasIndex("TenantId", "Status", "AppliedAt"); + + b.HasIndex("TenantId", "Status", "IssuedAt"); + + b.ToTable("finance_invoice_records", null, t => + { + t.HasComment("租户发票记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantInvoiceSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoIssueMaxAmount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("自动开票单张最大金额。"); + + b.Property("BankAccount") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("银行账号。"); + + b.Property("BankName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("开户银行。"); + + b.Property("CompanyName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .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("EnableAutoIssue") + .HasColumnType("boolean") + .HasComment("是否启用自动开票。"); + + b.Property("EnableElectronicNormalInvoice") + .HasColumnType("boolean") + .HasComment("是否启用电子普通发票。"); + + b.Property("EnableElectronicSpecialInvoice") + .HasColumnType("boolean") + .HasComment("是否启用电子专用发票。"); + + b.Property("RegisteredAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("注册地址。"); + + b.Property("RegisteredPhone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("注册电话。"); + + b.Property("TaxpayerNumber") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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") + .IsUnique(); + + b.ToTable("finance_invoice_settings", null, t => + { + t.HasComment("租户发票开票基础设置。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Channel") + .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("Message") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("通知正文。"); + + b.Property("MetadataJson") + .HasColumnType("text") + .HasComment("附加元数据 JSON。"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone") + .HasComment("租户是否已阅读。"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasComment("推送时间。"); + + b.Property("Severity") + .HasColumnType("integer") + .HasComment("通知重要级别。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("通知标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "Channel", "SentAt"); + + b.ToTable("tenant_notifications", null, t => + { + t.HasComment("面向租户的站内通知或消息推送。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPackage", 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("Description") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("套餐描述,包含适用场景、权益等。"); + + b.Property("FeaturePoliciesJson") + .HasColumnType("text") + .HasComment("权益明细 JSON,记录自定义特性开关。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否仍启用(系统控制)。"); + + b.Property("IsAllowNewTenantPurchase") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否允许新租户购买/选择(仅影响新购)。"); + + b.Property("IsPublicVisible") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否对外可见(展示页/套餐列表可见性)。"); + + b.Property("IsRecommended") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否推荐展示(运营推荐标识)。"); + + b.Property("MaxAccountCount") + .HasColumnType("integer") + .HasComment("允许创建的最大账号数。"); + + b.Property("MaxDeliveryOrders") + .HasColumnType("integer") + .HasComment("每月可调用的配送单数量上限。"); + + b.Property("MaxSmsCredits") + .HasColumnType("integer") + .HasComment("每月短信额度上限。"); + + b.Property("MaxStorageGb") + .HasColumnType("integer") + .HasComment("存储容量上限(GB)。"); + + b.Property("MaxStoreCount") + .HasColumnType("integer") + .HasComment("允许的最大门店数。"); + + b.Property("MonthlyPrice") + .HasColumnType("numeric") + .HasComment("月付价格,单位:人民币元。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("套餐名称,展示给租户的简称。"); + + b.Property("PackageType") + .HasColumnType("integer") + .HasComment("套餐分类(试用、标准、旗舰等)。"); + + b.Property("PublishStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("发布状态:0=草稿,1=已发布。"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("展示排序,数值越小越靠前。"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasColumnType("text[]") + .HasComment("套餐标签(用于展示与对比页)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("YearlyPrice") + .HasColumnType("numeric") + .HasComment("年付价格,单位:人民币元。"); + + b.HasKey("Id"); + + b.HasIndex("IsActive", "SortOrder"); + + b.HasIndex("PublishStatus", "IsActive", "IsPublicVisible", "IsAllowNewTenantPurchase", "SortOrder"); + + b.ToTable("tenant_packages", null, t => + { + t.HasComment("系统提供的租户套餐定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantPayment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("支付金额。"); + + b.Property("BillingStatementId") + .HasColumnType("bigint") + .HasComment("关联的账单 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("Method") + .HasColumnType("integer") + .HasComment("支付方式。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注信息。"); + + b.Property("PaidAt") + .HasColumnType("timestamp with time zone") + .HasComment("支付时间。"); + + b.Property("ProofUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("支付凭证 URL。"); + + b.Property("RefundReason") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("退款原因。"); + + b.Property("RefundedAt") + .HasColumnType("timestamp with time zone") + .HasComment("退款时间。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("支付状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TransactionNo") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("交易号。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("VerifiedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核时间。"); + + b.Property("VerifiedBy") + .HasColumnType("bigint") + .HasComment("审核人 ID(管理员)。"); + + b.HasKey("Id"); + + b.HasIndex("TransactionNo") + .HasDatabaseName("idx_payment_transaction_no") + .HasFilter("\"TransactionNo\" IS NOT NULL"); + + b.HasIndex("BillingStatementId", "PaidAt") + .HasDatabaseName("idx_payment_billing_paidat"); + + b.HasIndex("TenantId", "BillingStatementId"); + + b.ToTable("tenant_payments", null, t => + { + t.HasComment("租户支付记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaPackagePurchase", 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("ExpiredAt") + .HasColumnType("timestamp with time zone") + .HasComment("过期时间(可选)。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasComment("购买价格。"); + + b.Property("PurchasedAt") + .HasColumnType("timestamp with time zone") + .HasComment("购买时间。"); + + b.Property("QuotaPackageId") + .HasColumnType("bigint") + .HasComment("配额包 ID。"); + + b.Property("QuotaValue") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .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", "QuotaPackageId", "PurchasedAt"); + + b.ToTable("tenant_quota_package_purchases", null, t => + { + t.HasComment("租户配额包购买记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsage", 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("LastResetAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次重置时间。"); + + b.Property("LimitValue") + .HasColumnType("numeric") + .HasComment("当前配额上限。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型,例如门店数、短信条数等。"); + + b.Property("ResetCycle") + .HasColumnType("text") + .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("UsedValue") + .HasColumnType("numeric") + .HasComment("已消耗的数量。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "QuotaType") + .IsUnique(); + + b.ToTable("tenant_quota_usages", null, t => + { + t.HasComment("租户配额使用情况快照。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantQuotaUsageHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChangeAmount") + .HasColumnType("numeric") + .HasComment("变更量(可选)。"); + + b.Property("ChangeReason") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("变更原因(可选)。"); + + b.Property("ChangeType") + .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("LimitValue") + .HasColumnType("numeric") + .HasComment("限额值(记录时刻的快照)。"); + + b.Property("QuotaType") + .HasColumnType("integer") + .HasComment("配额类型。"); + + b.Property("RecordedAt") + .HasColumnType("timestamp with time zone") + .HasComment("记录时间(UTC)。"); + + 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("UsedValue") + .HasColumnType("numeric") + .HasComment("已使用值(记录时刻的快照)。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "RecordedAt"); + + b.HasIndex("TenantId", "QuotaType", "RecordedAt"); + + b.ToTable("tenant_quota_usage_histories", null, t => + { + t.HasComment("租户配额使用历史记录(用于追踪配额上下限与使用量的时间序列变化)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoRenew") + .HasColumnType("boolean") + .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("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("订阅生效时间(UTC)。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("订阅到期时间(UTC)。"); + + b.Property("NextBillingDate") + .HasColumnType("timestamp with time zone") + .HasComment("下一个计费时间,配合自动续费使用。"); + + b.Property("Notes") + .HasColumnType("text") + .HasComment("运营备注信息。"); + + b.Property("ScheduledPackageId") + .HasColumnType("bigint") + .HasComment("若已排期升降配,对应的新套餐 ID。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("订阅当前状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("TenantPackageId") + .HasColumnType("bigint") + .HasComment("当前订阅关联的套餐标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "TenantPackageId"); + + b.ToTable("tenant_subscriptions", null, t => + { + t.HasComment("租户套餐订阅记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantSubscriptionHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("numeric") + .HasComment("相关费用。"); + + b.Property("ChangeType") + .HasColumnType("integer") + .HasComment("变更类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("Currency") + .HasMaxLength(8) + .HasColumnType("character varying(8)") + .HasComment("币种。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone") + .HasComment("生效时间。"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone") + .HasComment("到期时间。"); + + b.Property("FromPackageId") + .HasColumnType("bigint") + .HasComment("原套餐 ID。"); + + b.Property("Notes") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("备注。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("租户标识。"); + + b.Property("TenantSubscriptionId") + .HasColumnType("bigint") + .HasComment("对应的订阅 ID。"); + + b.Property("ToPackageId") + .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", "TenantSubscriptionId"); + + b.ToTable("tenant_subscription_histories", null, t => + { + t.HasComment("租户套餐订阅变更记录。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantVerificationProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalDataJson") + .HasColumnType("text") + .HasComment("附加资料(JSON)。"); + + b.Property("BankAccountName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("开户名。"); + + b.Property("BankAccountNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("银行账号。"); + + b.Property("BankName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("银行名称。"); + + b.Property("BusinessLicenseNumber") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("营业执照编号。"); + + b.Property("BusinessLicenseUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .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("LegalPersonIdBackUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("法人身份证反面。"); + + b.Property("LegalPersonIdFrontUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("法人身份证正面。"); + + b.Property("LegalPersonIdNumber") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("法人身份证号。"); + + b.Property("LegalPersonName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("法人姓名。"); + + b.Property("ReviewRemarks") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("审核备注。"); + + b.Property("ReviewedAt") + .HasColumnType("timestamp with time zone") + .HasComment("审核时间。"); + + b.Property("ReviewedBy") + .HasColumnType("bigint") + .HasComment("审核人 ID。"); + + b.Property("ReviewedByName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("审核人姓名。"); + + b.Property("Status") + .HasColumnType("integer") + .HasComment("实名状态。"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasComment("提交时间。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("对应的租户标识。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId") + .IsUnique(); + + b.ToTable("tenant_verification_profiles", null, t => + { + t.HasComment("租户实名认证资料。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantVisibilityRoleRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.PrimitiveCollection("BillingVisibleRoleCodes") + .IsRequired() + .HasColumnType("text[]") + .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.PrimitiveCollection("QuotaVisibleRoleCodes") + .IsRequired() + .HasColumnType("text[]") + .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") + .IsUnique(); + + b.ToTable("tenant_visibility_role_rules", null, t => + { + t.HasComment("租户账单/配额可见角色规则。"); + }); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Finance.Entities.FinanceCostEntryItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Finance.Entities.FinanceCostEntry", null) + .WithMany() + .HasForeignKey("EntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Orders.Entities.OrderItem", b => + { + b.HasOne("TakeoutSaaS.Domain.Orders.Entities.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304071623_AddFinanceInvoiceModule.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304071623_AddFinanceInvoiceModule.cs new file mode 100644 index 0000000..5bbb528 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260304071623_AddFinanceInvoiceModule.cs @@ -0,0 +1,214 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations +{ + /// + public partial class AddFinanceInvoiceModule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "finance_cost_entries", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Dimension = table.Column(type: "integer", nullable: false, comment: "统计维度。"), + StoreId = table.Column(type: "bigint", nullable: true, comment: "门店标识(租户汇总维度为空)。"), + CostMonth = table.Column(type: "timestamp with time zone", nullable: false, comment: "成本月份(统一存储为 UTC 每月第一天 00:00:00)。"), + Category = table.Column(type: "integer", nullable: false, comment: "成本分类。"), + TotalAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "分类总金额。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_finance_cost_entries", x => x.Id); + }, + comment: "成本录入月度汇总实体(按维度 + 分类)。"); + + migrationBuilder.CreateTable( + name: "finance_invoice_records", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + InvoiceNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "发票号码。"), + ApplicantName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "申请人。"), + CompanyName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "开票抬头(公司名)。"), + TaxpayerNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "纳税人识别号快照。"), + InvoiceType = table.Column(type: "integer", nullable: false, comment: "发票类型。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "开票金额。"), + OrderNo = table.Column(type: "character varying(32)", maxLength: 32, nullable: false, comment: "关联订单号。"), + ContactEmail = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "接收邮箱。"), + ContactPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "联系电话。"), + ApplyRemark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "申请备注。"), + Status = table.Column(type: "integer", nullable: false, comment: "发票状态。"), + AppliedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "申请时间(UTC)。"), + IssuedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "开票时间(UTC)。"), + IssuedByUserId = table.Column(type: "bigint", nullable: true, comment: "开票人 ID。"), + IssueRemark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "开票备注。"), + VoidedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "作废时间(UTC)。"), + VoidedByUserId = table.Column(type: "bigint", nullable: true, comment: "作废人 ID。"), + VoidReason = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "作废原因。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_finance_invoice_records", x => x.Id); + }, + comment: "租户发票记录。"); + + migrationBuilder.CreateTable( + name: "finance_invoice_settings", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + CompanyName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false, comment: "企业名称。"), + TaxpayerNumber = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "纳税人识别号。"), + RegisteredAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "注册地址。"), + RegisteredPhone = table.Column(type: "character varying(32)", maxLength: 32, nullable: true, comment: "注册电话。"), + BankName = table.Column(type: "character varying(128)", maxLength: 128, nullable: true, comment: "开户银行。"), + BankAccount = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "银行账号。"), + EnableElectronicNormalInvoice = table.Column(type: "boolean", nullable: false, comment: "是否启用电子普通发票。"), + EnableElectronicSpecialInvoice = table.Column(type: "boolean", nullable: false, comment: "是否启用电子专用发票。"), + EnableAutoIssue = table.Column(type: "boolean", nullable: false, comment: "是否启用自动开票。"), + AutoIssueMaxAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "自动开票单张最大金额。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_finance_invoice_settings", x => x.Id); + }, + comment: "租户发票开票基础设置。"); + + migrationBuilder.CreateTable( + name: "finance_cost_entry_items", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + EntryId = table.Column(type: "bigint", nullable: false, comment: "关联汇总行标识。"), + Dimension = table.Column(type: "integer", nullable: false, comment: "统计维度。"), + StoreId = table.Column(type: "bigint", nullable: true, comment: "门店标识(租户汇总维度为空)。"), + CostMonth = table.Column(type: "timestamp with time zone", nullable: false, comment: "成本月份(统一存储为 UTC 每月第一天 00:00:00)。"), + Category = table.Column(type: "integer", nullable: false, comment: "成本分类。"), + ItemName = table.Column(type: "character varying(64)", maxLength: 64, nullable: false, comment: "明细名称。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "明细金额。"), + Quantity = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "数量(人工类可用)。"), + UnitPrice = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true, comment: "单价(人工类可用)。"), + SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 100, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_finance_cost_entry_items", x => x.Id); + table.ForeignKey( + name: "FK_finance_cost_entry_items_finance_cost_entries_EntryId", + column: x => x.EntryId, + principalTable: "finance_cost_entries", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }, + comment: "成本录入明细项实体。"); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_entries_TenantId_Dimension_StoreId_CostMonth", + table: "finance_cost_entries", + columns: new[] { "TenantId", "Dimension", "StoreId", "CostMonth" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_entries_TenantId_Dimension_StoreId_CostMonth_C~", + table: "finance_cost_entries", + columns: new[] { "TenantId", "Dimension", "StoreId", "CostMonth", "Category" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_entry_items_EntryId", + table: "finance_cost_entry_items", + column: "EntryId"); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_entry_items_TenantId_Dimension_StoreId_CostMon~", + table: "finance_cost_entry_items", + columns: new[] { "TenantId", "Dimension", "StoreId", "CostMonth", "Category", "SortOrder" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_invoice_records_TenantId_InvoiceNo", + table: "finance_invoice_records", + columns: new[] { "TenantId", "InvoiceNo" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_finance_invoice_records_TenantId_InvoiceType_AppliedAt", + table: "finance_invoice_records", + columns: new[] { "TenantId", "InvoiceType", "AppliedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_invoice_records_TenantId_OrderNo", + table: "finance_invoice_records", + columns: new[] { "TenantId", "OrderNo" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_invoice_records_TenantId_Status_AppliedAt", + table: "finance_invoice_records", + columns: new[] { "TenantId", "Status", "AppliedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_invoice_records_TenantId_Status_IssuedAt", + table: "finance_invoice_records", + columns: new[] { "TenantId", "Status", "IssuedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_invoice_settings_TenantId", + table: "finance_invoice_settings", + column: "TenantId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "finance_cost_entry_items"); + + migrationBuilder.DropTable( + name: "finance_invoice_records"); + + migrationBuilder.DropTable( + name: "finance_invoice_settings"); + + migrationBuilder.DropTable( + name: "finance_cost_entries"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260305090000_AddFinanceBusinessReportModule.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260305090000_AddFinanceBusinessReportModule.cs new file mode 100644 index 0000000..44eb941 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/20260305090000_AddFinanceBusinessReportModule.cs @@ -0,0 +1,164 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.App.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations; + +/// +/// 新增经营报表快照与成本配置表结构。 +/// +[DbContext(typeof(TakeoutAppDbContext))] +[Migration("20260305090000_AddFinanceBusinessReportModule")] +public sealed class AddFinanceBusinessReportModule : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "finance_business_report_snapshots", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "所属门店 ID。"), + PeriodType = table.Column(type: "integer", nullable: false, comment: "周期类型。"), + PeriodStartAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "周期开始时间(UTC,含)。"), + PeriodEndAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "周期结束时间(UTC,不含)。"), + Status = table.Column(type: "integer", nullable: false, comment: "生成状态。"), + RevenueAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "营业额。"), + OrderCount = table.Column(type: "integer", nullable: false, comment: "订单数。"), + AverageOrderValue = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "客单价。"), + RefundRate = table.Column(type: "numeric(9,4)", precision: 9, scale: 4, nullable: false, comment: "退款率(0-1)。"), + CostTotalAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "成本总额。"), + NetProfitAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "净利润。"), + ProfitRate = table.Column(type: "numeric(9,4)", precision: 9, scale: 4, nullable: false, comment: "利润率(0-1)。"), + KpiComparisonJson = table.Column(type: "text", nullable: false, comment: "KPI 比较快照 JSON(同比/环比)。"), + IncomeBreakdownJson = table.Column(type: "text", nullable: false, comment: "收入明细快照 JSON(按渠道)。"), + CostBreakdownJson = table.Column(type: "text", nullable: false, comment: "成本明细快照 JSON(按类别)。"), + StartedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "生成开始时间(UTC)。"), + FinishedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "生成完成时间(UTC)。"), + LastError = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: true, comment: "最近一次失败信息。"), + RetryCount = table.Column(type: "integer", nullable: false, defaultValue: 0, comment: "重试次数。"), + HangfireJobId = table.Column(type: "character varying(64)", maxLength: 64, nullable: true, comment: "调度任务 ID。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_finance_business_report_snapshots", x => x.Id); + }, + comment: "经营报表快照实体。"); + + migrationBuilder.CreateTable( + name: "finance_cost_profiles", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店 ID。"), + Category = table.Column(type: "integer", nullable: false, comment: "成本分类。"), + CalcMode = table.Column(type: "integer", nullable: false, comment: "计算模式。"), + Ratio = table.Column(type: "numeric(9,6)", precision: 9, scale: 6, nullable: false, comment: "比例值(0-1)。"), + FixedDailyAmount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "固定日金额。"), + EffectiveFrom = table.Column(type: "timestamp with time zone", nullable: false, comment: "生效开始日期(UTC 日期)。"), + EffectiveTo = table.Column(type: "timestamp with time zone", nullable: true, comment: "生效结束日期(UTC 日期,含)。"), + IsEnabled = table.Column(type: "boolean", nullable: false, comment: "是否启用。"), + SortOrder = table.Column(type: "integer", nullable: false, defaultValue: 100, comment: "排序值。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_finance_cost_profiles", x => x.Id); + }, + comment: "成本配置实体(类别级规则)。"); + + migrationBuilder.CreateTable( + name: "finance_cost_daily_overrides", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + StoreId = table.Column(type: "bigint", nullable: false, comment: "门店 ID。"), + BusinessDate = table.Column(type: "timestamp with time zone", nullable: false, comment: "业务日期(UTC 日期)。"), + Category = table.Column(type: "integer", nullable: false, comment: "成本分类。"), + Amount = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false, comment: "覆盖金额。"), + Remark = table.Column(type: "character varying(256)", maxLength: 256, nullable: true, comment: "备注。"), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false, comment: "创建时间(UTC)。"), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "最近一次更新时间(UTC),从未更新时为 null。"), + DeletedAt = table.Column(type: "timestamp with time zone", nullable: true, comment: "软删除时间(UTC),未删除时为 null。"), + CreatedBy = table.Column(type: "bigint", nullable: true, comment: "创建人用户标识,匿名或系统操作时为 null。"), + UpdatedBy = table.Column(type: "bigint", nullable: true, comment: "最后更新人用户标识,匿名或系统操作时为 null。"), + DeletedBy = table.Column(type: "bigint", nullable: true, comment: "删除人用户标识(软删除),未删除时为 null。"), + TenantId = table.Column(type: "bigint", nullable: false, comment: "所属租户 ID。") + }, + constraints: table => + { + table.PrimaryKey("PK_finance_cost_daily_overrides", x => x.Id); + }, + comment: "成本日覆盖实体。"); + + migrationBuilder.CreateIndex( + name: "IX_finance_business_report_snapshots_TenantId_Status_CreatedAt", + table: "finance_business_report_snapshots", + columns: new[] { "TenantId", "Status", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_business_report_snapshots_TenantId_StoreId_PeriodType_S~", + table: "finance_business_report_snapshots", + columns: new[] { "TenantId", "StoreId", "PeriodType", "Status", "PeriodStartAt" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_business_report_snapshots_TenantId_StoreId_PeriodType_P~", + table: "finance_business_report_snapshots", + columns: new[] { "TenantId", "StoreId", "PeriodType", "PeriodStartAt" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_daily_overrides_TenantId_StoreId_BusinessDate", + table: "finance_cost_daily_overrides", + columns: new[] { "TenantId", "StoreId", "BusinessDate" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_daily_overrides_TenantId_StoreId_BusinessDate_Cate~", + table: "finance_cost_daily_overrides", + columns: new[] { "TenantId", "StoreId", "BusinessDate", "Category" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_profiles_TenantId_StoreId_Category_EffectiveFrom_E~", + table: "finance_cost_profiles", + columns: new[] { "TenantId", "StoreId", "Category", "EffectiveFrom", "EffectiveTo" }); + + migrationBuilder.CreateIndex( + name: "IX_finance_cost_profiles_TenantId_StoreId_IsEnabled_SortOrder", + table: "finance_cost_profiles", + columns: new[] { "TenantId", "StoreId", "IsEnabled", "SortOrder" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "finance_business_report_snapshots"); + + migrationBuilder.DropTable( + name: "finance_cost_daily_overrides"); + + migrationBuilder.DropTable( + name: "finance_cost_profiles"); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20260305093000_SeedFinanceReportMenuAndPermissions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20260305093000_SeedFinanceReportMenuAndPermissions.cs new file mode 100644 index 0000000..c88f324 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20260305093000_SeedFinanceReportMenuAndPermissions.cs @@ -0,0 +1,249 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using TakeoutSaaS.Infrastructure.Identity.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb; + +/// +/// 写入经营报表菜单与权限定义。 +/// +[DbContext(typeof(IdentityDbContext))] +[Migration("20260305093000_SeedFinanceReportMenuAndPermissions")] +public sealed class SeedFinanceReportMenuAndPermissions : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + DO $$ + DECLARE + v_parent_permission_id bigint; + v_view_permission_id bigint; + v_export_permission_id bigint; + v_parent_menu_id bigint; + v_report_menu_id bigint; + v_permission_seed_base bigint := 840200000000000000; + v_menu_seed_base bigint := 850200000000000000; + BEGIN + -- 1. 确保统计权限分组存在。 + SELECT "Id" + INTO v_parent_permission_id + FROM public.permissions + WHERE "Code" = 'group:tenant:statistics' + ORDER BY "Id" + LIMIT 1; + + IF v_parent_permission_id IS NULL THEN + v_parent_permission_id := v_permission_seed_base + 1; + INSERT INTO public.permissions ( + "Id", "Name", "Code", "Description", + "CreatedAt", "UpdatedAt", "DeletedAt", + "CreatedBy", "UpdatedBy", "DeletedBy", + "ParentId", "SortOrder", "Type", "Portal") + VALUES ( + v_parent_permission_id, '经营分析', 'group:tenant:statistics', '经营分析权限分组', + NOW(), NULL, NULL, + NULL, NULL, NULL, + 0, 5300, 'group', 1) + ON CONFLICT ("Code") DO NOTHING; + END IF; + + -- 2. Upsert 经营报表查看权限。 + INSERT INTO public.permissions ( + "Id", "Name", "Code", "Description", + "CreatedAt", "UpdatedAt", "DeletedAt", + "CreatedBy", "UpdatedBy", "DeletedBy", + "ParentId", "SortOrder", "Type", "Portal") + VALUES ( + v_permission_seed_base + 11, '经营报表查看', 'tenant:statistics:report:view', '查看经营报表列表与详情', + NOW(), NULL, NULL, + NULL, NULL, NULL, + v_parent_permission_id, 5310, 'leaf', 1) + ON CONFLICT ("Code") DO UPDATE + SET "Name" = EXCLUDED."Name", + "Description" = EXCLUDED."Description", + "ParentId" = EXCLUDED."ParentId", + "SortOrder" = EXCLUDED."SortOrder", + "Type" = EXCLUDED."Type", + "Portal" = EXCLUDED."Portal", + "DeletedAt" = NULL, + "DeletedBy" = NULL, + "UpdatedAt" = NOW(); + + -- 3. Upsert 经营报表导出权限。 + INSERT INTO public.permissions ( + "Id", "Name", "Code", "Description", + "CreatedAt", "UpdatedAt", "DeletedAt", + "CreatedBy", "UpdatedBy", "DeletedBy", + "ParentId", "SortOrder", "Type", "Portal") + VALUES ( + v_permission_seed_base + 12, '经营报表导出', 'tenant:statistics:report:export', '导出经营报表 PDF / Excel / ZIP', + NOW(), NULL, NULL, + NULL, NULL, NULL, + v_parent_permission_id, 5320, 'leaf', 1) + ON CONFLICT ("Code") DO UPDATE + SET "Name" = EXCLUDED."Name", + "Description" = EXCLUDED."Description", + "ParentId" = EXCLUDED."ParentId", + "SortOrder" = EXCLUDED."SortOrder", + "Type" = EXCLUDED."Type", + "Portal" = EXCLUDED."Portal", + "DeletedAt" = NULL, + "DeletedBy" = NULL, + "UpdatedAt" = NOW(); + + SELECT "Id" INTO v_view_permission_id + FROM public.permissions + WHERE "Code" = 'tenant:statistics:report:view' + LIMIT 1; + + SELECT "Id" INTO v_export_permission_id + FROM public.permissions + WHERE "Code" = 'tenant:statistics:report:export' + LIMIT 1; + + -- 4. 确保租户端财务父菜单存在。 + SELECT "Id" + INTO v_parent_menu_id + FROM public.menu_definitions + WHERE "Portal" = 1 AND "Path" = '/finance' AND "DeletedAt" IS NULL + ORDER BY "Id" + LIMIT 1; + + IF v_parent_menu_id IS NULL THEN + v_parent_menu_id := v_menu_seed_base + 1; + INSERT INTO public.menu_definitions ( + "Id", "ParentId", "Name", "Path", "Component", "Title", "Icon", + "IsIframe", "Link", "KeepAlive", "SortOrder", + "RequiredPermissions", "MetaPermissions", "MetaRoles", "AuthListJson", + "CreatedAt", "UpdatedAt", "DeletedAt", "CreatedBy", "UpdatedBy", "DeletedBy", "Portal") + VALUES ( + v_parent_menu_id, 0, 'Finance', '/finance', 'BasicLayout', '财务中心', 'lucide:wallet', + FALSE, NULL, FALSE, 500, + '', '', '', NULL, + NOW(), NULL, NULL, NULL, NULL, NULL, 1) + ON CONFLICT ("Id") DO NOTHING; + END IF; + + -- 5. Upsert 经营报表菜单。 + SELECT "Id" + INTO v_report_menu_id + FROM public.menu_definitions + WHERE "Portal" = 1 + AND ("Path" = '/finance/report' OR ("Path" = 'report' AND "Component" = '/finance/report/index')) + ORDER BY "DeletedAt" NULLS FIRST, "Id" + LIMIT 1; + + IF v_report_menu_id IS NULL THEN + v_report_menu_id := v_menu_seed_base + 11; + INSERT INTO public.menu_definitions ( + "Id", "ParentId", "Name", "Path", "Component", "Title", "Icon", + "IsIframe", "Link", "KeepAlive", "SortOrder", + "RequiredPermissions", "MetaPermissions", "MetaRoles", "AuthListJson", + "CreatedAt", "UpdatedAt", "DeletedAt", "CreatedBy", "UpdatedBy", "DeletedBy", "Portal") + VALUES ( + v_report_menu_id, v_parent_menu_id, 'BusinessReport', '/finance/report', '/finance/report/index', '经营报表', 'lucide:file-bar-chart-2', + FALSE, NULL, TRUE, 530, + 'tenant:statistics:report:view', 'tenant:statistics:report:view,tenant:statistics:report:export', '', NULL, + NOW(), NULL, NULL, NULL, NULL, NULL, 1) + ON CONFLICT ("Id") DO NOTHING; + ELSE + UPDATE public.menu_definitions + SET "ParentId" = v_parent_menu_id, + "Name" = 'BusinessReport', + "Path" = '/finance/report', + "Component" = '/finance/report/index', + "Title" = '经营报表', + "Icon" = 'lucide:file-bar-chart-2', + "IsIframe" = FALSE, + "Link" = NULL, + "KeepAlive" = TRUE, + "SortOrder" = 530, + "RequiredPermissions" = 'tenant:statistics:report:view', + "MetaPermissions" = 'tenant:statistics:report:view,tenant:statistics:report:export', + "MetaRoles" = '', + "DeletedAt" = NULL, + "DeletedBy" = NULL, + "UpdatedAt" = NOW(), + "Portal" = 1 + WHERE "Id" = v_report_menu_id; + END IF; + + -- 6. 为 tenant-admin 角色授予权限。 + INSERT INTO public.role_permissions ( + "Id", "RoleId", "PermissionId", "CreatedAt", "UpdatedAt", "DeletedAt", + "CreatedBy", "UpdatedBy", "DeletedBy", "TenantId", "Portal") + SELECT + ABS(HASHTEXTEXTENDED('tenant-admin:report:' || role."Id"::text || ':' || permission_id::text, 0)), + role."Id", + permission_id, + NOW(), NULL, NULL, + NULL, NULL, NULL, + role."TenantId", + 1 + FROM public.roles role + CROSS JOIN LATERAL ( + SELECT UNNEST(ARRAY[v_view_permission_id, v_export_permission_id]) AS permission_id + ) item + WHERE role."Code" = 'tenant-admin' + AND role."DeletedAt" IS NULL + AND item.permission_id IS NOT NULL + ON CONFLICT ("RoleId", "PermissionId") DO UPDATE + SET "DeletedAt" = NULL, + "DeletedBy" = NULL, + "UpdatedAt" = NOW(), + "Portal" = 1; + + -- 7. 为 tenant-admin 角色模板授予权限。 + INSERT INTO public.role_template_permissions ( + "Id", "RoleTemplateId", "PermissionCode", + "CreatedAt", "UpdatedAt", "DeletedAt", + "CreatedBy", "UpdatedBy", "DeletedBy") + SELECT + ABS(HASHTEXTEXTENDED('template-report:' || template."Id"::text || ':' || item.permission_code, 0)), + template."Id", + item.permission_code, + NOW(), NULL, NULL, + NULL, NULL, NULL + FROM public.role_templates template + CROSS JOIN LATERAL ( + SELECT UNNEST(ARRAY['tenant:statistics:report:view', 'tenant:statistics:report:export']) AS permission_code + ) item + WHERE template."TemplateCode" = 'tenant-admin' + AND template."DeletedAt" IS NULL + ON CONFLICT ("RoleTemplateId", "PermissionCode") DO UPDATE + SET "DeletedAt" = NULL, + "DeletedBy" = NULL, + "UpdatedAt" = NOW(); + END $$; + """); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + """ + DO $$ + BEGIN + DELETE FROM public.role_permissions + WHERE "PermissionId" IN ( + SELECT "Id" + FROM public.permissions + WHERE "Code" IN ('tenant:statistics:report:view', 'tenant:statistics:report:export')); + + DELETE FROM public.role_template_permissions + WHERE "PermissionCode" IN ('tenant:statistics:report:view', 'tenant:statistics:report:export'); + + DELETE FROM public.menu_definitions + WHERE "Portal" = 1 AND "Path" = '/finance/report'; + + DELETE FROM public.permissions + WHERE "Code" IN ('tenant:statistics:report:view', 'tenant:statistics:report:export'); + END $$; + """); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs index ff583c6..a0d3f98 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Extensions/SchedulerServiceCollectionExtensions.cs @@ -71,6 +71,7 @@ public static class SchedulerServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/FinanceBusinessReportRefreshJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/FinanceBusinessReportRefreshJob.cs new file mode 100644 index 0000000..de394f6 --- /dev/null +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/FinanceBusinessReportRefreshJob.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.Logging; +using TakeoutSaaS.Domain.Finance.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Module.Scheduler.Jobs; + +/// +/// 经营报表快照刷新任务:消费排队中的报表快照并触发生成。 +/// +public sealed class FinanceBusinessReportRefreshJob( + IFinanceBusinessReportRepository financeBusinessReportRepository, + ITenantContextAccessor tenantContextAccessor, + ILogger logger) +{ + private const int BatchSize = 80; + + /// + /// 执行报表快照刷新。 + /// + public async Task ExecuteAsync() + { + // 1. 拉取待处理快照任务。 + var pendingList = await financeBusinessReportRepository.GetPendingSnapshotsAsync(BatchSize, CancellationToken.None); + if (pendingList.Count == 0) + { + logger.LogDebug("定时任务:经营报表快照刷新无待处理任务"); + return; + } + + // 2. 逐租户上下文执行生成。 + var successCount = 0; + var failedCount = 0; + foreach (var pending in pendingList) + { + var previousContext = tenantContextAccessor.Current; + try + { + tenantContextAccessor.Current = new TenantContext( + pending.TenantId, + $"tenant-{pending.TenantId}", + "scheduler"); + + await financeBusinessReportRepository.GenerateSnapshotAsync( + pending.SnapshotId, + CancellationToken.None); + successCount += 1; + } + catch (Exception ex) + { + failedCount += 1; + logger.LogError( + ex, + "定时任务:经营报表快照刷新失败 SnapshotId={SnapshotId}, TenantId={TenantId}", + pending.SnapshotId, + pending.TenantId); + } + finally + { + tenantContextAccessor.Current = previousContext; + } + } + + // 3. 记录执行结果。 + logger.LogInformation( + "定时任务:经营报表快照刷新完成,处理 {TotalCount} 条,成功 {SuccessCount} 条,失败 {FailedCount} 条", + pendingList.Count, + successCount, + failedCount); + } +} diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs index 94ccb5e..2763fae 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Services/RecurringJobRegistrar.cs @@ -45,7 +45,13 @@ public sealed class RecurringJobRegistrar( job => job.ExecuteAsync(), billingOptions.OverdueBillingProcessCron); - // 4. (空行后) 门店管理自动化任务 + // 4. 经营报表快照刷新任务 + RecurringJob.AddOrUpdate( + "finance.business-report-refresh", + job => job.ExecuteAsync(), + "*/10 * * * *"); + + // 5. (空行后) 门店管理自动化任务 RecurringJob.AddOrUpdate( "stores.business-status-auto-switch", job => job.ExecuteAsync(),