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
{
// 忽略定时器释放异常
}
}
}
}