Files
FATrace/FATrace.WPLApp/Services/ReadFileServices.cs
2026-01-28 15:04:16 +08:00

746 lines
28 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 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
{
/// <summary>
/// Excel 文件读取与定时导入服务
/// - 每小时扫描一次源目录中的 *.xlsx 文件(文件名: yyyyMMddHHmmss.xlsx每天一个
/// - 按 Sheet 名映射到对应的 FileModel 实体表
/// - 使用 FreeSql 批量插入数据
/// - 导入完成后将文件移动到归档目录,并在 FileImportLog 中记录详细信息
/// </summary>
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;
/// <summary>
/// 构造函数:初始化路径配置与定时器
/// </summary>
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.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}");
}
});
}
/// <summary>
/// 将配置中的路径转换为绝对路径(支持相对路径)
/// </summary>
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);
}
/// <summary>
/// 定时器回调:避免重入,统一调度导入任务
/// </summary>
private async Task OnTimerElapsedAsync()
{
if (_isRunning) return;
_isRunning = true;
try
{
await CheckAndImportAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_log.Error($"Excel 导入定时任务异常: {ex}");
}
finally
{
_isRunning = false;
}
}
/// <summary>
/// 对外公开的手动触发方法,方便调试与单元测试
/// </summary>
public Task TriggerImportOnceAsync()
{
return CheckAndImportAsync();
}
/// <summary>
/// 扫描源目录,按文件执行导入
/// </summary>
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}");
}
}
}
/// <summary>
/// 导入单个 Excel 文件:按 Sheet 写入各表、记录日志并移动文件
/// </summary>
private async Task ImportSingleFileAsync(string filePath)
{
var fileName = Path.GetFileName(filePath);
if (string.IsNullOrWhiteSpace(fileName)) return;
// 若已存在成功记录,则不再重复导入
var alreadySuccess = await _fsql.Select<FileImportLog>()
.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<string, int>();
try
{
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
IWorkbook workbook = new XSSFWorkbook(fs);
var formatter = new DataFormatter();
sheetStats["FactoryRawInbound"] = ImportFactoryRawInbound(workbook, formatter);
sheetStats["FactoryRawOutbound"] = ImportFactoryRawOutbound(workbook, formatter);
sheetStats["FactoryRawInOutbound"] = ImportFactoryRawInOutbound(workbook, formatter);
sheetStats["FactoryProductionRecord"] = ImportFactoryProductionRecord(workbook, formatter);
sheetStats["FactoryInbound"] = ImportFactoryInbound(workbook, formatter);
sheetStats["FactoryOutbound"] = ImportFactoryOutbound(workbook, formatter);
sheetStats["FactoryInOutbound"] = ImportFactoryInOutbound(workbook, formatter);
sheetStats["OEMInbound"] = ImportOEMInbound(workbook, formatter);
sheetStats["OEMOutbound"] = ImportOEMOutbound(workbook, formatter);
sheetStats["OEMInOutbound"] = ImportOEMInOutbound(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 ImportFactoryRawInbound(IWorkbook workbook, DataFormatter formatter)
{
var sheet = workbook.GetSheet("工厂-原料入库");
if (sheet == null) return 0;
var list = new List<FactoryRawInbound>();
for (int i = 1; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (IsRowEmpty(row, formatter)) continue;
var entity = new FactoryRawInbound
{
Origin = GetCellString(row, 0, formatter),
RawCode = GetCellString(row, 1, formatter),
RawName = GetCellString(row, 2, formatter),
Weight = GetCellString(row, 3, formatter),
LoginDateTime = GetCellString(row, 4, formatter)
};
if (IsAllEmpty(entity.Origin, entity.RawCode, entity.RawName, entity.Weight, entity.LoginDateTime))
continue;
list.Add(entity);
}
if (list.Count > 0)
{
_fsql.Insert(list).ExecuteAffrows();
}
return list.Count;
}
private int ImportFactoryRawOutbound(IWorkbook workbook, DataFormatter formatter)
{
var sheet = workbook.GetSheet("工厂-原料出库");
if (sheet == null) return 0;
var list = new List<FactoryRawOutbound>();
for (int i = 1; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (IsRowEmpty(row, formatter)) continue;
var entity = new FactoryRawOutbound
{
Origin = GetCellString(row, 0, formatter),
RawCode = GetCellString(row, 1, formatter),
RawName = GetCellString(row, 2, formatter),
Weight = GetCellString(row, 3, formatter),
LoginDateTime = GetCellString(row, 4, formatter)
};
if (IsAllEmpty(entity.Origin, entity.RawCode, entity.RawName, entity.Weight, entity.LoginDateTime))
continue;
list.Add(entity);
}
if (list.Count > 0)
{
_fsql.Insert(list).ExecuteAffrows();
}
return list.Count;
}
private int ImportFactoryRawInOutbound(IWorkbook workbook, DataFormatter formatter)
{
var sheet = workbook.GetSheet("工厂-原料出入库");
if (sheet == null) return 0;
var list = new List<FactoryRawInOutbound>();
for (int i = 1; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (IsRowEmpty(row, formatter)) continue;
var entity = new FactoryRawInOutbound
{
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<FactoryProductionRecord>();
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 ImportFactoryInbound(IWorkbook workbook, DataFormatter formatter)
{
var sheet = workbook.GetSheet("工厂-成品入库信息");
if (sheet == null) return 0;
var list = new List<FactoryInbound>();
for (int i = 1; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (IsRowEmpty(row, formatter)) continue;
var entity = new FactoryInbound
{
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),
LoginDateTime = GetCellString(row, 7, formatter)
};
if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName, entity.SequenceNo, entity.LoginDateTime))
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<FactoryOutbound>();
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),
LoginDateTime = GetCellString(row, 7, formatter)
};
if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName,
entity.SequenceNo, entity.LoginDateTime))
continue;
list.Add(entity);
}
if (list.Count > 0)
{
_fsql.Insert(list).ExecuteAffrows();
}
return list.Count;
}
private int ImportFactoryInOutbound(IWorkbook workbook, DataFormatter formatter)
{
var sheet = workbook.GetSheet("工厂-成品入库与出库信息");
if (sheet == null) return 0;
var list = new List<FactoryInOutbound>();
for (int i = 1; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (IsRowEmpty(row, formatter)) continue;
var entity = new FactoryInOutbound
{
InTime = GetCellString(row, 0, formatter),
OutTime = GetCellString(row, 1, formatter),
Origin = GetCellString(row, 2, formatter),
Batch = GetCellString(row, 3, formatter),
RawCode = GetCellString(row, 4, formatter),
RawName = GetCellString(row, 5, formatter),
TotalInPcs = GetCellString(row, 6, formatter),
TotalOutPcs = GetCellString(row, 7, formatter),
RemainPcs = GetCellString(row, 8, formatter)
};
if (IsAllEmpty(entity.InTime, entity.OutTime, entity.Origin, entity.Batch, entity.RawCode, entity.RawName, entity.TotalInPcs, entity.TotalOutPcs, entity.RemainPcs))
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<OEMInbound>();
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),
LoginDateTime = GetCellString(row, 7, formatter)
};
if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName,
entity.SequenceNo, 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<OEMOutbound>();
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),
LoginDateTime = GetCellString(row, 7, formatter)
};
if (IsAllEmpty(entity.Batch, entity.Weight, entity.ShelfLife, entity.Origin, entity.RawCode, entity.RawName,
entity.SequenceNo, entity.LoginDateTime))
continue;
list.Add(entity);
}
if (list.Count > 0)
{
_fsql.Insert(list).ExecuteAffrows();
}
return list.Count;
}
private int ImportOEMInOutbound(IWorkbook workbook, DataFormatter formatter)
{
var sheet = workbook.GetSheet("OEM-出入库");
if (sheet == null) return 0;
var list = new List<OEMInOutbound>();
for (int i = 1; i <= sheet.LastRowNum; i++)
{
var row = sheet.GetRow(i);
if (IsRowEmpty(row, formatter)) continue;
var entity = new OEMInOutbound
{
InTime = GetCellString(row, 0, formatter),
OutTime = GetCellString(row, 1, formatter),
Origin = GetCellString(row, 2, formatter),
Batch = GetCellString(row, 3, formatter),
RawCode = GetCellString(row, 4, formatter),
RawName = GetCellString(row, 5, formatter),
TotalInCase = GetCellString(row, 6, formatter),
TotalOutCase = GetCellString(row, 7, formatter),
RemainCase = GetCellString(row, 8, formatter)
};
if (IsAllEmpty(entity.InTime, entity.OutTime, entity.Origin, entity.Batch, 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<OEMRawUsageInfo>();
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)
};
if (IsAllEmpty(entity.RawUseTime, entity.InBagCode, entity.Origin, entity.RawName, entity.RawCode))
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
/// <summary>
/// 释放定时器等资源
/// </summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
try
{
_timer?.Stop();
_timer?.Dispose();
}
catch
{
// 忽略定时器释放异常
}
}
}
}