using System.IO.Compression; using WorkCameraExport.Models; using WorkCameraExport.Services.Interfaces; namespace WorkCameraExport.Services { /// /// 导出服务实现 - 负责在客户端本地生成 Excel 和 ZIP 文件 /// public class ExportService : IExportService { private readonly IApiService _apiService; private readonly IImageService _imageService; private readonly IConfigService _configService; private readonly ILogService? _logService; private readonly ExcelService _excelService; // 配置常量 private const int DefaultPageSize = 50; private const int DefaultConcurrency = 5; private const int DefaultImageQuality = 50; public ExportService( IApiService apiService, IImageService imageService, IConfigService configService, ILogService? logService = null) { _apiService = apiService; _imageService = imageService; _configService = configService; _logService = logService; _excelService = new ExcelService(logService); } #region 工作记录导出 /// /// 导出工作记录到 Excel(按查询条件导出全部) /// public async Task ExportWorkRecordsAsync( WorkRecordQueryDto query, string outputPath, IProgress? progress = null, CancellationToken cancellationToken = default) { var tempDir = CreateTempDirectory(); try { _logService?.Info($"[导出] 开始导出工作记录: {outputPath}"); _logService?.Info($"[导出] 临时目录: {tempDir}"); // 确保输出目录存在 var outputDir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); _logService?.Info($"[导出] 创建输出目录: {outputDir}"); } // 获取配置 var config = _configService.LoadConfig(); var concurrency = config?.ImageDownloadConcurrency ?? DefaultConcurrency; var imageQuality = config?.ImageCompressQuality ?? DefaultImageQuality; // 1. 分页获取所有数据 _logService?.Info("[导出] 开始获取数据..."); var allRecords = await FetchAllRecordsAsync(query, progress, cancellationToken); _logService?.Info($"[导出] 获取到 {allRecords.Count} 条记录"); if (allRecords.Count == 0) { _logService?.Warn("[导出] 没有找到符合条件的工作记录"); throw new InvalidOperationException("没有找到符合条件的工作记录"); } // 2. 收集所有图片 URL var allImageUrls = allRecords .SelectMany(r => r.Images) .Where(url => !string.IsNullOrEmpty(url)) .Distinct() .ToList(); _logService?.Info($"[导出] 共 {allImageUrls.Count} 张图片需要下载"); ReportProgress(progress, allRecords.Count, 0, allImageUrls.Count, 0, "正在下载图片..."); // 3. 并发下载图片 var imagePathMap = await DownloadImagesAsync( allImageUrls, tempDir, concurrency, imageQuality, downloaded => ReportProgress(progress, allRecords.Count, 0, allImageUrls.Count, downloaded, "正在下载图片..."), cancellationToken); _logService?.Info($"[导出] 图片下载完成,成功 {imagePathMap.Count(x => x.Value != null)} 张"); // 4. 构建导出数据 var exportItems = BuildExportItems(allRecords, imagePathMap); _logService?.Info($"[导出] 构建导出数据完成,共 {exportItems.Count} 条"); ReportProgress(progress, allRecords.Count, allRecords.Count, allImageUrls.Count, allImageUrls.Count, "正在生成 Excel..."); // 5. 生成 Excel _logService?.Info($"[导出] 开始生成 Excel: {outputPath}"); await _excelService.ExportWorkRecordsToExcelAsync(exportItems, outputPath, cancellationToken); // 验证文件是否生成 if (File.Exists(outputPath)) { var fileInfo = new FileInfo(outputPath); _logService?.Info($"[导出] Excel 文件已生成: {outputPath}, 大小: {fileInfo.Length} 字节"); } else { _logService?.Error($"[导出] Excel 文件未生成: {outputPath}"); } ReportProgress(progress, allRecords.Count, allRecords.Count, allImageUrls.Count, allImageUrls.Count, "导出完成"); _logService?.Info($"[导出] 工作记录导出完成: {allRecords.Count} 条记录, {allImageUrls.Count} 张图片"); } catch (Exception ex) { _logService?.Error($"[导出] 导出异常: {ex.Message}", ex); throw; } finally { // 清理临时目录 CleanupTempDirectory(tempDir); } } /// /// 导出指定 ID 的工作记录到 Excel /// public async Task ExportWorkRecordsByIdsAsync( List ids, string outputPath, IProgress? progress = null, CancellationToken cancellationToken = default) { if (ids == null || ids.Count == 0) { throw new ArgumentException("请选择要导出的记录", nameof(ids)); } var tempDir = CreateTempDirectory(); try { _logService?.Info($"开始导出选中的工作记录: {ids.Count} 条"); var config = _configService.LoadConfig(); var concurrency = config?.ImageDownloadConcurrency ?? DefaultConcurrency; var imageQuality = config?.ImageCompressQuality ?? DefaultImageQuality; // 1. 逐个获取记录详情 var allRecords = new List(); for (int i = 0; i < ids.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); ReportProgress(progress, ids.Count, i, 0, 0, $"正在获取记录 {i + 1}/{ids.Count}..."); var result = await _apiService.GetWorkRecordAsync(ids[i]); if (result.Success && result.Data != null) { allRecords.Add(ConvertToExportDto(result.Data)); } } if (allRecords.Count == 0) { throw new InvalidOperationException("没有找到有效的工作记录"); } // 2. 收集所有图片 URL var allImageUrls = allRecords .SelectMany(r => r.Images) .Where(url => !string.IsNullOrEmpty(url)) .Distinct() .ToList(); ReportProgress(progress, allRecords.Count, allRecords.Count, allImageUrls.Count, 0, "正在下载图片..."); // 3. 并发下载图片 var imagePathMap = await DownloadImagesAsync( allImageUrls, tempDir, concurrency, imageQuality, downloaded => ReportProgress(progress, allRecords.Count, allRecords.Count, allImageUrls.Count, downloaded, "正在下载图片..."), cancellationToken); // 4. 构建导出数据 var exportItems = BuildExportItems(allRecords, imagePathMap); ReportProgress(progress, allRecords.Count, allRecords.Count, allImageUrls.Count, allImageUrls.Count, "正在生成 Excel..."); // 5. 生成 Excel await _excelService.ExportWorkRecordsToExcelAsync(exportItems, outputPath, cancellationToken); ReportProgress(progress, allRecords.Count, allRecords.Count, allImageUrls.Count, allImageUrls.Count, "导出完成"); _logService?.Info($"选中记录导出完成: {allRecords.Count} 条记录"); } finally { CleanupTempDirectory(tempDir); } } /// /// 分页获取所有记录 /// private async Task> FetchAllRecordsAsync( WorkRecordQueryDto query, IProgress? progress, CancellationToken cancellationToken) { var allRecords = new List(); var pageNum = 1; var pageSize = DefaultPageSize; int totalRecords = 0; do { cancellationToken.ThrowIfCancellationRequested(); var pagedQuery = new WorkRecordQueryDto { PageNum = pageNum, PageSize = pageSize, StartDate = query.StartDate, EndDate = query.EndDate, DeptName = query.DeptName, Address = query.Address, Content = query.Content, WorkerName = query.WorkerName, StatusName = query.StatusName }; var result = await _apiService.GetWorkRecordsAsync(pagedQuery); if (!result.Success || result.Data == null) { _logService?.Error($"获取工作记录失败: {result.Message}"); throw new Exception($"获取工作记录失败: {result.Message}"); } if (pageNum == 1) { totalRecords = result.Data.TotalNum; ReportProgress(progress, totalRecords, 0, 0, 0, $"正在获取数据 (共 {totalRecords} 条)..."); } // 转换为导出 DTO foreach (var record in result.Data.Result) { allRecords.Add(ConvertToExportDto(record)); } ReportProgress(progress, totalRecords, allRecords.Count, 0, 0, $"正在获取数据 {allRecords.Count}/{totalRecords}..."); // 检查是否还有更多数据 if (result.Data.Result.Count < pageSize || allRecords.Count >= totalRecords) { break; } pageNum++; } while (true); return allRecords; } /// /// 下载图片并返回 URL 到本地路径的映射 /// private async Task> DownloadImagesAsync( List imageUrls, string outputDir, int concurrency, int quality, Action? progressCallback, CancellationToken cancellationToken) { if (imageUrls.Count == 0) { return new Dictionary(); } var downloadProgress = new Progress(count => progressCallback?.Invoke(count)); var results = await _imageService.DownloadImagesAsync( imageUrls, outputDir, concurrency, downloadProgress, cancellationToken); // 压缩下载的图片 foreach (var kvp in results.Where(r => r.Value != null)) { try { var originalPath = kvp.Value!; var imageData = await File.ReadAllBytesAsync(originalPath, cancellationToken); var compressedData = _imageService.CompressAndResizeImage(imageData, 100, 60, quality); await File.WriteAllBytesAsync(originalPath, compressedData, cancellationToken); } catch (Exception ex) { _logService?.Warn($"压缩图片失败: {kvp.Key}, {ex.Message}"); } } return results; } /// /// 构建导出项列表 /// private List BuildExportItems( List records, Dictionary imagePathMap) { return records.Select(r => { var item = WorkRecordExportItem.FromDto(r); item.ImagePaths = r.Images .Where(url => !string.IsNullOrEmpty(url) && imagePathMap.ContainsKey(url)) .Select(url => imagePathMap[url]) .Where(path => path != null) .Cast() .ToList(); return item; }).ToList(); } /// /// 转换 WorkRecordDto 为 WorkRecordExportDto /// private WorkRecordExportDto ConvertToExportDto(WorkRecordDto dto) { return new WorkRecordExportDto { Id = dto.Id, DeptName = dto.DeptName, RecordTime = dto.RecordTime, Longitude = dto.Longitude, Latitude = dto.Latitude, Address = dto.Address, Content = dto.Content, StatusName = dto.StatusName, Workers = dto.Workers?.Select(w => w.WorkerName).ToList() ?? new List(), Images = dto.Images.Select(i => i.Url).ToList(), CreateTime = dto.CreateTime, UpdateTime = dto.UpdateTime }; } #endregion #region 月报表导出 /// /// 导出月报表到 Excel /// public async Task ExportMonthlyReportAsync( List data, string outputPath) { if (data == null || data.Count == 0) { throw new ArgumentException("没有数据可导出", nameof(data)); } _logService?.Info($"开始导出月报表: {data.Count} 条记录"); await _excelService.ExportMonthlyReportToExcelAsync(data, outputPath); _logService?.Info($"月报表导出完成: {outputPath}"); } #endregion #region 照片 ZIP 下载 /// /// 下载指定月份的照片并打包成 ZIP /// public async Task DownloadPhotosZipAsync( string yearMonth, string outputPath, IProgress? progress = null, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(yearMonth)) { throw new ArgumentException("请选择月份", nameof(yearMonth)); } var tempDir = CreateTempDirectory(); try { _logService?.Info($"开始下载 {yearMonth} 月份照片"); var config = _configService.LoadConfig(); var concurrency = config?.ImageDownloadConcurrency ?? DefaultConcurrency; // 1. 获取月份图片列表 ReportDownloadProgress(progress, 0, 0, "正在获取图片列表..."); var result = await _apiService.GetMonthImagesAsync(yearMonth); if (!result.Success || result.Data == null || result.Data.Count == 0) { throw new InvalidOperationException($"没有找到 {yearMonth} 月份的图片"); } var monthImages = result.Data; var totalImages = monthImages.Sum(m => m.ImageUrls.Count); ReportDownloadProgress(progress, totalImages, 0, $"共 {totalImages} 张图片"); // 2. 按目录结构下载图片 var downloadedCount = 0; foreach (var record in monthImages) { cancellationToken.ThrowIfCancellationRequested(); // 创建目录结构 var directories = CreateDirectoryStructure(tempDir, yearMonth, record); // 下载图片到各个目录 foreach (var imageUrl in record.ImageUrls) { cancellationToken.ThrowIfCancellationRequested(); var imageData = await _imageService.DownloadImageAsync(imageUrl, cancellationToken); if (imageData != null) { var fileName = GetFileNameFromUrl(imageUrl); // 保存到各个分类目录 foreach (var dir in directories) { var filePath = Path.Combine(dir, fileName); filePath = EnsureUniqueFilePath(filePath); await File.WriteAllBytesAsync(filePath, imageData, cancellationToken); } } downloadedCount++; ReportDownloadProgress(progress, totalImages, downloadedCount, $"正在下载 {downloadedCount}/{totalImages}..."); } } // 3. 打包成 ZIP ReportDownloadProgress(progress, totalImages, downloadedCount, "正在打包 ZIP..."); // 确保输出目录存在 var outputDir = Path.GetDirectoryName(outputPath); if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); } // 删除已存在的文件 if (File.Exists(outputPath)) { File.Delete(outputPath); } ZipFile.CreateFromDirectory(tempDir, outputPath, CompressionLevel.Optimal, false); ReportDownloadProgress(progress, totalImages, downloadedCount, "下载完成"); _logService?.Info($"照片 ZIP 下载完成: {outputPath}, 共 {downloadedCount} 张图片"); } finally { CleanupTempDirectory(tempDir); } } /// /// 创建目录结构并返回所有目录路径 /// 目录结构:/workfiles/{yyyyMM}/{yyyyMMdd}/当日照片/、参与人员/{人员姓名}/、工作内容/{工作内容}/、部门/{部门名称}/ /// private List CreateDirectoryStructure(string baseDir, string yearMonth, MonthImageDto record) { var directories = new List(); var yyyyMM = yearMonth.Replace("-", ""); var yyyyMMdd = record.RecordDate?.ToString("yyyyMMdd") ?? yyyyMM + "01"; var basePath = Path.Combine(baseDir, "workfiles", yyyyMM, yyyyMMdd); // 1. 当日照片目录 var dailyDir = Path.Combine(basePath, "当日照片"); EnsureDirectoryExists(dailyDir); directories.Add(dailyDir); // 2. 参与人员目录 foreach (var worker in record.Workers.Where(w => !string.IsNullOrEmpty(w))) { var workerDir = Path.Combine(basePath, "参与人员", SanitizeFileName(worker)); EnsureDirectoryExists(workerDir); directories.Add(workerDir); } // 3. 工作内容目录 if (!string.IsNullOrEmpty(record.Content)) { var contentDir = Path.Combine(basePath, "工作内容", SanitizeFileName(record.Content)); EnsureDirectoryExists(contentDir); directories.Add(contentDir); } // 4. 部门目录 if (!string.IsNullOrEmpty(record.DeptName)) { var deptDir = Path.Combine(basePath, "部门", SanitizeFileName(record.DeptName)); EnsureDirectoryExists(deptDir); directories.Add(deptDir); } return directories; } #endregion #region 辅助方法 /// /// 创建临时目录 /// private string CreateTempDirectory() { var tempDir = Path.Combine(Path.GetTempPath(), "WorkCameraExport", $"export_{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); return tempDir; } /// /// 清理临时目录 /// private void CleanupTempDirectory(string tempDir) { try { var config = _configService.LoadConfig(); if (config?.AutoCleanTempFiles != false && Directory.Exists(tempDir)) { Directory.Delete(tempDir, true); _logService?.Info($"已清理临时目录: {tempDir}"); } } catch (Exception ex) { _logService?.Warn($"清理临时目录失败: {ex.Message}"); } } /// /// 确保目录存在 /// private void EnsureDirectoryExists(string path) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } } /// /// 从 URL 提取文件名 /// private string GetFileNameFromUrl(string url) { try { var uri = new Uri(url); var fileName = Path.GetFileName(uri.LocalPath); if (!string.IsNullOrWhiteSpace(fileName) && fileName.Contains('.')) { return SanitizeFileName(fileName); } } catch { } return $"{Guid.NewGuid():N}.jpg"; } /// /// 清理文件名中的非法字符 /// private string SanitizeFileName(string fileName) { var invalidChars = Path.GetInvalidFileNameChars(); var sanitized = fileName; foreach (var c in invalidChars) { sanitized = sanitized.Replace(c, '_'); } // 限制长度 if (sanitized.Length > 50) { var ext = Path.GetExtension(sanitized); sanitized = sanitized.Substring(0, 50 - ext.Length) + ext; } return sanitized; } /// /// 确保文件路径唯一 /// private string EnsureUniqueFilePath(string filePath) { if (!File.Exists(filePath)) { return filePath; } var directory = Path.GetDirectoryName(filePath) ?? ""; var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath); var extension = Path.GetExtension(filePath); var counter = 1; string newPath; do { newPath = Path.Combine(directory, $"{fileNameWithoutExt}_{counter}{extension}"); counter++; } while (File.Exists(newPath)); return newPath; } /// /// 报告导出进度 /// private void ReportProgress( IProgress? progress, int totalRecords, int processedRecords, int totalImages, int downloadedImages, string status) { progress?.Report(new ExportProgress { TotalRecords = totalRecords, ProcessedRecords = processedRecords, TotalImages = totalImages, DownloadedImages = downloadedImages, Status = status }); } /// /// 报告下载进度 /// private void ReportDownloadProgress( IProgress? progress, int totalImages, int downloadedImages, string status) { progress?.Report(new DownloadProgress { TotalImages = totalImages, DownloadedImages = downloadedImages, Status = status }); } #endregion } }