WorkCamera/client/WorkCameraExport/Services/ExportService.cs
2026-01-06 00:42:25 +08:00

636 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.IO.Compression;
using WorkCameraExport.Models;
using WorkCameraExport.Services.Interfaces;
namespace WorkCameraExport.Services
{
/// <summary>
/// 导出服务实现 - 负责在客户端本地生成 Excel 和 ZIP 文件
/// </summary>
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
/// <summary>
/// 导出工作记录到 Excel按查询条件导出全部
/// </summary>
public async Task ExportWorkRecordsAsync(
WorkRecordQueryDto query,
string outputPath,
IProgress<ExportProgress>? progress = null,
CancellationToken cancellationToken = default)
{
var tempDir = CreateTempDirectory();
try
{
_logService?.Info($"开始导出工作记录: {outputPath}");
// 获取配置
var config = _configService.LoadConfig();
var concurrency = config?.ImageDownloadConcurrency ?? DefaultConcurrency;
var imageQuality = config?.ImageCompressQuality ?? DefaultImageQuality;
// 1. 分页获取所有数据
var allRecords = await FetchAllRecordsAsync(query, progress, cancellationToken);
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();
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);
// 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} 条记录, {allImageUrls.Count} 张图片");
}
finally
{
// 清理临时目录
CleanupTempDirectory(tempDir);
}
}
/// <summary>
/// 导出指定 ID 的工作记录到 Excel
/// </summary>
public async Task ExportWorkRecordsByIdsAsync(
List<int> ids,
string outputPath,
IProgress<ExportProgress>? 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<WorkRecordExportDto>();
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);
}
}
/// <summary>
/// 分页获取所有记录
/// </summary>
private async Task<List<WorkRecordExportDto>> FetchAllRecordsAsync(
WorkRecordQueryDto query,
IProgress<ExportProgress>? progress,
CancellationToken cancellationToken)
{
var allRecords = new List<WorkRecordExportDto>();
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;
}
/// <summary>
/// 下载图片并返回 URL 到本地路径的映射
/// </summary>
private async Task<Dictionary<string, string?>> DownloadImagesAsync(
List<string> imageUrls,
string outputDir,
int concurrency,
int quality,
Action<int>? progressCallback,
CancellationToken cancellationToken)
{
if (imageUrls.Count == 0)
{
return new Dictionary<string, string?>();
}
var downloadProgress = new Progress<int>(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;
}
/// <summary>
/// 构建导出项列表
/// </summary>
private List<WorkRecordExportItem> BuildExportItems(
List<WorkRecordExportDto> records,
Dictionary<string, string?> 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<string>()
.ToList();
return item;
}).ToList();
}
/// <summary>
/// 转换 WorkRecordDto 为 WorkRecordExportDto
/// </summary>
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<string>(),
Images = dto.Images.Select(i => i.Url).ToList(),
CreateTime = dto.CreateTime,
UpdateTime = dto.UpdateTime
};
}
#endregion
#region
/// <summary>
/// 导出月报表到 Excel
/// </summary>
public async Task ExportMonthlyReportAsync(
List<MonthlyReportDto> 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
/// <summary>
/// 下载指定月份的照片并打包成 ZIP
/// </summary>
public async Task DownloadPhotosZipAsync(
string yearMonth,
string outputPath,
IProgress<DownloadProgress>? 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);
}
}
/// <summary>
/// 创建目录结构并返回所有目录路径
/// 目录结构:/workfiles/{yyyyMM}/{yyyyMMdd}/当日照片/、参与人员/{人员姓名}/、工作内容/{工作内容}/、部门/{部门名称}/
/// </summary>
private List<string> CreateDirectoryStructure(string baseDir, string yearMonth, MonthImageDto record)
{
var directories = new List<string>();
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
/// <summary>
/// 创建临时目录
/// </summary>
private string CreateTempDirectory()
{
var tempDir = Path.Combine(Path.GetTempPath(), "WorkCameraExport", $"export_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
return tempDir;
}
/// <summary>
/// 清理临时目录
/// </summary>
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}");
}
}
/// <summary>
/// 确保目录存在
/// </summary>
private void EnsureDirectoryExists(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
/// <summary>
/// 从 URL 提取文件名
/// </summary>
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";
}
/// <summary>
/// 清理文件名中的非法字符
/// </summary>
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;
}
/// <summary>
/// 确保文件路径唯一
/// </summary>
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;
}
/// <summary>
/// 报告导出进度
/// </summary>
private void ReportProgress(
IProgress<ExportProgress>? 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
});
}
/// <summary>
/// 报告下载进度
/// </summary>
private void ReportDownloadProgress(
IProgress<DownloadProgress>? progress,
int totalImages,
int downloadedImages,
string status)
{
progress?.Report(new DownloadProgress
{
TotalImages = totalImages,
DownloadedImages = downloadedImages,
Status = status
});
}
#endregion
}
}