using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; using System.Globalization; using TakeoutSaaS.Domain.Common.Enums; using TakeoutSaaS.Domain.Merchants.Entities; using TakeoutSaaS.Domain.Merchants.Enums; using TakeoutSaaS.Domain.Merchants.Services; using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Enums; namespace TakeoutSaaS.Infrastructure.App.Services; /// /// 商户导出服务实现(PDF)。 /// public sealed class MerchantExportService : IMerchantExportService { public MerchantExportService() { QuestPDF.Settings.License = LicenseType.Community; } /// public Task ExportToPdfAsync( Merchant merchant, string? tenantName, IReadOnlyList stores, IReadOnlyList auditLogs, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(merchant); var safeStores = stores ?? Array.Empty(); var safeAuditLogs = auditLogs ?? Array.Empty(); var document = Document.Create(container => { container.Page(page => { page.Size(PageSizes.A4); page.Margin(24); page.DefaultTextStyle(x => x.FontSize(10)); page.Content().Column(column => { column.Spacing(10); column.Item().Text("Merchant Export").FontSize(16).SemiBold(); column.Item().Element(section => BuildBasicSection(section, merchant, tenantName)); column.Item().Element(section => BuildStoresSection(section, safeStores, cancellationToken)); column.Item().Element(section => BuildAuditSection(section, safeAuditLogs, cancellationToken)); }); }); }); return Task.FromResult(document.GeneratePdf()); } private static void BuildBasicSection(IContainer container, Merchant merchant, string? tenantName) { container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column => { column.Spacing(4); column.Item().Text("Basic Information").SemiBold(); column.Item().Text($"Merchant: {merchant.BrandName}"); column.Item().Text($"Tenant: {tenantName ?? "-"} (ID: {merchant.TenantId})"); column.Item().Text($"Operating Mode: {ResolveOperatingMode(merchant.OperatingMode)}"); column.Item().Text($"Status: {merchant.Status}"); column.Item().Text($"Frozen: {(merchant.IsFrozen ? "Yes" : "No")}"); column.Item().Text($"License Number: {merchant.BusinessLicenseNumber ?? "-"}"); column.Item().Text($"Legal Representative: {merchant.LegalPerson ?? "-"}"); column.Item().Text($"Registered Address: {merchant.Address ?? "-"}"); column.Item().Text($"Contact Phone: {merchant.ContactPhone}"); column.Item().Text($"Contact Email: {merchant.ContactEmail ?? "-"}"); column.Item().Text($"Approved At: {FormatDateTime(merchant.ApprovedAt)}"); column.Item().Text($"Approved By: {merchant.ApprovedBy?.ToString() ?? "-"}"); }); } private static void BuildStoresSection(IContainer container, IReadOnlyList stores, CancellationToken cancellationToken) { container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column => { column.Spacing(4); column.Item().Text("Stores").SemiBold(); if (stores.Count == 0) { column.Item().Text("No stores."); return; } for (var i = 0; i < stores.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); var store = stores[i]; column.Item().Text($"{i + 1}. {store.Name} | {ResolveStoreStatus(store.Status)} | {store.Address ?? "-"} | {store.Phone ?? "-"}"); } }); } private static void BuildAuditSection(IContainer container, IReadOnlyList auditLogs, CancellationToken cancellationToken) { container.Border(1).BorderColor(Colors.Grey.Lighten2).Padding(10).Column(column => { column.Spacing(4); column.Item().Text("Audit History").SemiBold(); if (auditLogs.Count == 0) { column.Item().Text("No audit records."); return; } for (var i = 0; i < auditLogs.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); var log = auditLogs[i]; var title = string.IsNullOrWhiteSpace(log.Title) ? log.Action.ToString() : log.Title; column.Item().Text($"{i + 1}. {title} | {log.OperatorName ?? "-"} | {FormatDateTime(log.CreatedAt)}"); if (!string.IsNullOrWhiteSpace(log.Description)) { column.Item().Text($" {log.Description}"); } } }); } private static string ResolveOperatingMode(OperatingMode? mode) => mode switch { OperatingMode.SameEntity => "SameEntity", OperatingMode.DifferentEntity => "DifferentEntity", _ => "-" }; private static string ResolveStoreStatus(StoreStatus status) => status switch { StoreStatus.Closed => "Closed", StoreStatus.Preparing => "Preparing", StoreStatus.Operating => "Operating", StoreStatus.Suspended => "Suspended", _ => status.ToString() }; private static string FormatDateTime(DateTime? value) => value.HasValue ? value.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) : "-"; }