using FATrace.Com; using FATrace.Model; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Timers; using FreeSql; namespace FATrace.WPLApp.Services { /// /// Excel 文件读取与定时导入服务 /// - 每小时扫描一次源目录中的 *.xlsx 文件(文件名: yyyyMMddHHmmss.xlsx,每天一个) /// - 按 Sheet 名映射到对应的 FileModel 实体表 /// - 使用 FreeSql 批量插入数据 /// - 导入完成后将文件移动到归档目录,并在 FileImportLog 中记录详细信息 /// public class ReadFileServices : IDisposable { private readonly ILogService _log; private readonly IFreeSql _fsql; private readonly System.Timers.Timer _timer; private readonly string _sourceDirectory; private readonly string _archiveDirectory; private bool _disposed; private volatile bool _isRunning; /// /// 构造函数:初始化路径配置与定时器 /// public ReadFileServices(ILogService logService, IFreeSql freeSql) { _log = logService; _fsql = freeSql; // 路径从 App.config 读取,支持相对路径(相对于应用程序根目录) var sourceSetting = FATrace.Com.ConfigHelper.GetStringOrDefault("ExcelImportSourceDir", "ExcelFile"); var archiveSetting = FATrace.Com.ConfigHelper.GetStringOrDefault("ExcelImportArchiveDir", Path.Combine("ExcelFile", "Archive")); _sourceDirectory = ResolvePath(sourceSetting); _archiveDirectory = ResolvePath(archiveSetting); try { Directory.CreateDirectory(_sourceDirectory); Directory.CreateDirectory(_archiveDirectory); } catch (Exception ex) { _log.Error($"创建 Excel 导入目录失败: src={_sourceDirectory}, archive={_archiveDirectory}, ex={ex.Message}"); } // 每小时扫描一次(3600000 ms),如有需要可后续改为配置项 _timer = new System.Timers.Timer(TimeSpan.FromMinutes(1).TotalMilliseconds)//TimeSpan.FromHours(1).TotalMilliseconds { AutoReset = true, Enabled = true }; _timer.Elapsed += async (s, e) => await OnTimerElapsedAsync().ConfigureAwait(false); // 启动后立即执行一次检查,避免必须等一小时 Task.Run(async () => { try { await OnTimerElapsedAsync().ConfigureAwait(false); } catch (Exception ex) { _log.Error($"Excel 首次导入检查异常: {ex.Message}"); } }); } /// /// 将配置中的路径转换为绝对路径(支持相对路径) /// private static string ResolvePath(string pathSetting) { var path = (pathSetting ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(path)) { return AppDomain.CurrentDomain.BaseDirectory; } // 兼容配置中写成 "C:\Dir" 或 'C:\Dir' 的情况 if (path.Length >= 2) { if ((path.StartsWith("\"") && path.EndsWith("\"")) || (path.StartsWith("'") && path.EndsWith("'"))) { path = path.Substring(1, path.Length - 2).Trim(); } } // 支持环境变量:%USERPROFILE%\xxx path = Environment.ExpandEnvironmentVariables(path); // 支持 ~ 作为用户目录(主要用于开发/测试环境) if (path.StartsWith("~", StringComparison.Ordinal)) { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var tail = path.Substring(1).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); path = Path.Combine(home, tail); } // 仅当不是绝对路径时,才基于程序目录做相对路径拼接 if (!Path.IsPathRooted(path)) { path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); } return Path.GetFullPath(path); } /// /// 定时器回调:避免重入,统一调度导入任务 /// private async Task OnTimerElapsedAsync() { if (_isRunning) return; _isRunning = true; try { await CheckAndImportAsync().ConfigureAwait(false); } catch (Exception ex) { _log.Error($"Excel 导入定时任务异常: {ex}"); } finally { _isRunning = false; } } /// /// 对外公开的手动触发方法,方便调试与单元测试 /// public Task TriggerImportOnceAsync() { return CheckAndImportAsync(); } /// /// 扫描源目录,按文件执行导入 /// private async Task CheckAndImportAsync() { if (!Directory.Exists(_sourceDirectory)) { _log.Warn($"Excel 导入目录不存在: {_sourceDirectory}"); return; } string[] files; try { files = Directory.GetFiles(_sourceDirectory, "*.xlsx", SearchOption.TopDirectoryOnly); } catch (Exception ex) { _log.Error($"扫描 Excel 导入目录失败: dir={_sourceDirectory}, ex={ex.Message}"); return; } if (files.Length == 0) { return; } // 按文件名排序,确保按时间顺序处理 foreach (var filePath in files.OrderBy(f => Path.GetFileName(f))) { try { await ImportSingleFileAsync(filePath).ConfigureAwait(false); } catch (Exception ex) { _log.Error($"导入 Excel 文件失败: {filePath}, ex={ex}"); } } } /// /// 导入单个 Excel 文件:按 Sheet 写入各表、记录日志并移动文件 /// private async Task ImportSingleFileAsync(string filePath) { var fileName = Path.GetFileName(filePath); if (string.IsNullOrWhiteSpace(fileName)) return; // 若已存在成功记录,则不再重复导入 var alreadySuccess = await _fsql.Select() .Where(x => x.FileName == fileName && x.Status == "Success") .AnyAsync().ConfigureAwait(false); if (alreadySuccess) { return; } var log = new FileImportLog { FileName = fileName, SourcePath = filePath, StartTime = DateTime.Now, Status = "Running" }; var sheetStats = new Dictionary(); try { using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { IWorkbook workbook = new XSSFWorkbook(fs); var formatter = new DataFormatter(); sheetStats["FactoryInbound"] = ImportFactoryInbound(workbook, formatter); sheetStats["FactoryMaterialWithdrawal"] = ImportFactoryMaterialWithdrawal(workbook, formatter); sheetStats["FactoryInventoryTransaction"] = ImportFactoryInventoryTransaction(workbook, formatter); sheetStats["FactoryProductionRecord"] = ImportFactoryProductionRecord(workbook, formatter); sheetStats["FactoryOutbound"] = ImportFactoryOutbound(workbook, formatter); sheetStats["OEMInbound"] = ImportOEMInbound(workbook, formatter); sheetStats["OEMOutbound"] = ImportOEMOutbound(workbook, formatter); sheetStats["OEMInventoryTransaction"] = ImportOEMInventoryTransaction(workbook, formatter); sheetStats["OEMRawUsageInfo"] = ImportOEMRawUsageInfo(workbook, formatter); } // 构造统计摘要 log.SheetRowStats = string.Join(";", sheetStats.Select(kv => $"{kv.Key}={kv.Value}")); // 移动文件到归档目录 var archivePath = Path.Combine(_archiveDirectory, fileName); try { if (File.Exists(archivePath)) { var newName = $"{Path.GetFileNameWithoutExtension(fileName)}_{DateTime.Now:HHmmss}{Path.GetExtension(fileName)}"; archivePath = Path.Combine(_archiveDirectory, newName); } File.Move(filePath, archivePath); log.ArchivePath = archivePath; } catch (Exception moveEx) { log.ArchivePath = archivePath; _log.Error($"移动 Excel 文件到归档目录失败: src={filePath}, dest={archivePath}, ex={moveEx.Message}"); // 视为部分成功 } log.Status = "Success"; log.Message = "OK"; } catch (Exception ex) { log.Status = "Failed"; log.Message = ex.Message; if (sheetStats.Count > 0 && string.IsNullOrEmpty(log.SheetRowStats)) { log.SheetRowStats = string.Join(";", sheetStats.Select(kv => $"{kv.Key}={kv.Value}")); } _log.Error($"导入 Excel 文件过程中出现异常: {filePath}, ex={ex}"); } finally { log.EndTime = DateTime.Now; try { await _fsql.Insert(log).ExecuteAffrowsAsync().ConfigureAwait(false); } catch (Exception ex) { _log.Error($"写入 FileImportLog 失败: file={fileName}, ex={ex.Message}"); } } } #region 各 Sheet 导入实现 private static bool IsRowEmpty(IRow? row, DataFormatter formatter) { if (row == null) return true; for (int i = row.FirstCellNum; i < row.LastCellNum; i++) { var cell = row.GetCell(i); if (cell != null && !string.IsNullOrWhiteSpace(formatter.FormatCellValue(cell))) { return false; } } return true; } private static string GetCellString(IRow row, int columnIndex, DataFormatter formatter) { var cell = row.GetCell(columnIndex); return cell == null ? string.Empty : formatter.FormatCellValue(cell).Trim(); } private int ImportFactoryInbound(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("工厂-入库"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new FactoryInbound { Origin = GetCellString(row, 0, formatter), RawCode = GetCellString(row, 1, formatter), RawName = GetCellString(row, 2, formatter), Weight = GetCellString(row, 3, formatter), LoginDate = GetCellString(row, 4, formatter), LoginTime = GetCellString(row, 5, formatter), LoginDateTime = GetCellString(row, 6, formatter) }; if (IsAllEmpty(entity.Origin, entity.RawCode, entity.RawName, entity.Weight, entity.LoginDate, entity.LoginTime, entity.LoginDateTime)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportFactoryMaterialWithdrawal(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("工厂-领料"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new FactoryMaterialWithdrawal { Origin = GetCellString(row, 0, formatter), RawCode = GetCellString(row, 1, formatter), RawName = GetCellString(row, 2, formatter), Weight = GetCellString(row, 3, formatter), LoginDate = GetCellString(row, 4, formatter), LoginTime = GetCellString(row, 5, formatter), LoginDateTime = GetCellString(row, 6, formatter) }; if (IsAllEmpty(entity.Origin, entity.RawCode, entity.RawName, entity.Weight, entity.LoginDate, entity.LoginTime, entity.LoginDateTime)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportFactoryInventoryTransaction(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("工厂-出入库"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new FactoryInventoryTransaction { InTime = GetCellString(row, 0, formatter), OutTime = GetCellString(row, 1, formatter), Origin = GetCellString(row, 2, formatter), RawCode = GetCellString(row, 3, formatter), RawName = GetCellString(row, 4, formatter), TotalInWeightKg = GetCellString(row, 5, formatter), TotalOutWeightKg = GetCellString(row, 6, formatter), RemainWeightKg = GetCellString(row, 7, formatter) }; if (IsAllEmpty(entity.InTime, entity.OutTime, entity.Origin, entity.RawCode, entity.RawName, entity.TotalInWeightKg, entity.TotalOutWeightKg, entity.RemainWeightKg)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportFactoryProductionRecord(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("工厂-原料生产信息"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new FactoryProductionRecord { RawCode = GetCellString(row, 0, formatter), RawName = GetCellString(row, 1, formatter), Origin = GetCellString(row, 2, formatter), InBagCode = GetCellString(row, 3, formatter), BoxCode = GetCellString(row, 4, formatter), Batch = GetCellString(row, 5, formatter), ShelfLife = GetCellString(row, 6, formatter), Weight = GetCellString(row, 7, formatter), DeliveryDate = GetCellString(row, 8, formatter), RemainWeight = GetCellString(row, 9, formatter), StockWeight = GetCellString(row, 10, formatter), WeightTime = GetCellString(row, 11, formatter), OpUser = GetCellString(row, 12, formatter), CheckUser = GetCellString(row, 13, formatter), BoxScanTime = GetCellString(row, 14, formatter) }; if (IsAllEmpty(entity.RawCode, entity.RawName, entity.Origin, entity.InBagCode, entity.BoxCode, entity.Batch, entity.ShelfLife, entity.Weight, entity.DeliveryDate, entity.RemainWeight, entity.StockWeight, entity.WeightTime, entity.OpUser, entity.CheckUser, entity.BoxScanTime)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportFactoryOutbound(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("工厂-成品出库"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new FactoryOutbound { Batch = GetCellString(row, 0, formatter), Weight = GetCellString(row, 1, formatter), ShelfLife = GetCellString(row, 2, formatter), Origin = GetCellString(row, 3, formatter), RawCode = GetCellString(row, 4, formatter), RawName = GetCellString(row, 5, formatter), SequenceNo = GetCellString(row, 6, formatter), LoginDate = GetCellString(row, 7, formatter), LoginTime = GetCellString(row, 8, formatter), LoginDateTime = GetCellString(row, 9, formatter) }; if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName, entity.SequenceNo, entity.LoginDate, entity.LoginTime, entity.LoginDateTime)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportOEMInbound(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("OEM-入库"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new OEMInbound { Batch = GetCellString(row, 0, formatter), Weight = GetCellString(row, 1, formatter), ShelfLife = GetCellString(row, 2, formatter), Origin = GetCellString(row, 3, formatter), RawCode = GetCellString(row, 4, formatter), RawName = GetCellString(row, 5, formatter), SequenceNo = GetCellString(row, 6, formatter), LoginDate = GetCellString(row, 7, formatter), LoginTime = GetCellString(row, 8, formatter), LoginDateTime = GetCellString(row, 9, formatter) }; if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName, entity.SequenceNo, entity.LoginDate, entity.LoginTime, entity.LoginDateTime)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportOEMOutbound(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("OEM-出库"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new OEMOutbound { Batch = GetCellString(row, 0, formatter), Weight = GetCellString(row, 1, formatter), ShelfLife = GetCellString(row, 2, formatter), Origin = GetCellString(row, 3, formatter), RawCode = GetCellString(row, 4, formatter), RawName = GetCellString(row, 5, formatter), SequenceNo = GetCellString(row, 6, formatter), LoginDate = GetCellString(row, 7, formatter), LoginTime = GetCellString(row, 8, formatter), LoginDateTime = GetCellString(row, 9, formatter) }; if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName, entity.SequenceNo, entity.LoginDate, entity.LoginTime, entity.LoginDateTime)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportOEMInventoryTransaction(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("OEM-出入库"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new OEMInventoryTransaction { InTime = GetCellString(row, 0, formatter), OutTime = GetCellString(row, 1, formatter), Origin = GetCellString(row, 2, formatter), RawCode = GetCellString(row, 3, formatter), RawName = GetCellString(row, 4, formatter), TotalInCase = GetCellString(row, 5, formatter), TotalOutCase = GetCellString(row, 6, formatter), RemainCase = GetCellString(row, 7, formatter) }; if (IsAllEmpty(entity.InTime, entity.OutTime, entity.Origin, entity.RawCode, entity.RawName, entity.TotalInCase, entity.TotalOutCase, entity.RemainCase)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private int ImportOEMRawUsageInfo(IWorkbook workbook, DataFormatter formatter) { var sheet = workbook.GetSheet("OEM-原料使用信息"); if (sheet == null) return 0; var list = new List(); for (int i = 1; i <= sheet.LastRowNum; i++) { var row = sheet.GetRow(i); if (IsRowEmpty(row, formatter)) continue; var entity = new OEMRawUsageInfo { RawUseTime = GetCellString(row, 0, formatter), InBagCode = GetCellString(row, 1, formatter), Origin = GetCellString(row, 2, formatter), RawName = GetCellString(row, 3, formatter), RawCode = GetCellString(row, 4, formatter), VideoUrl = GetCellString(row, 5, formatter) }; if (IsAllEmpty(entity.RawUseTime, entity.InBagCode, entity.Origin, entity.RawName, entity.RawCode, entity.VideoUrl)) continue; list.Add(entity); } if (list.Count > 0) { _fsql.Insert(list).ExecuteAffrows(); } return list.Count; } private static bool IsAllEmpty(params string?[] values) { if (values == null || values.Length == 0) return true; foreach (var v in values) { if (!string.IsNullOrWhiteSpace(v)) return false; } return true; } #endregion /// /// 释放定时器等资源 /// public void Dispose() { if (_disposed) return; _disposed = true; try { _timer?.Stop(); _timer?.Dispose(); } catch { // 忽略定时器释放异常 } } } }