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}");
// 获取配置
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);
}
}
///
/// 导出指定 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
}
}