DataServices 现场调试OK

This commit is contained in:
2025-11-26 16:46:48 +08:00
parent 979afae645
commit 010272aa0b
31 changed files with 1866 additions and 267 deletions

View File

@@ -10,12 +10,15 @@ using CsvHelper;
using CsvHelper.Configuration;
using FATrace.WPLApp.ModelDto;
using Prism.Mvvm;
using FATrace.WPLApp.CsvModel;
using FATrace.Com;
namespace FATrace.WPLApp.Services
{
/// <summary>
/// CSV服务
///导出CSV文件
///读取CSV 文件
/// </summary>
public class CsvServices:BindableBase
{
@@ -26,74 +29,121 @@ namespace FATrace.WPLApp.Services
{
LogService = logService;
FreeSql = freeSql;
RawProUseCsvPath = GetConfiguredOutputDirectory();
}
/// <summary>
/// 导出单条记录为 CSV 文件,文件名即记录的 InBagCode自动追加 .csv 扩展名)。
/// 导出Csv目录
/// </summary>
/// <param name="item">要导出的数据项,类型为 RawProUserCsvDto</param>
/// <param name="outputDirectory">导出目录,不存在将自动创建</param>
/// <param name="overwrite">若目标文件已存在,是否允许覆盖</param>
/// <returns>导出后的完整文件路径</returns>
/// <exception cref="ArgumentNullException">item 或 outputDirectory 为 null</exception>
/// <exception cref="ArgumentException">InBagCode 为空或仅空白</exception>
/// <exception cref="IOException">文件已存在且不允许覆盖</exception>
public string ExportSingle(RawProUserCsvDto item, string outputDirectory, bool overwrite = false)
private string RawProUseCsvPath { get; set; }
/// <summary>
/// 从 App.config 读取导出目录key: RawProUseCsvPath
/// </summary>
private string GetConfiguredOutputDirectory()
{
if (item is null) throw new ArgumentNullException(nameof(item));
if (outputDirectory is null) throw new ArgumentNullException(nameof(outputDirectory));
var dir = ConfigHelper.GetValue("RawProUseCsvPath");
// 支持环境变量(如 %USERPROFILE%
dir = Environment.ExpandEnvironmentVariables(dir ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(dir))
throw new InvalidOperationException("配置项 RawProUseCsvPath 为空");
return dir;
}
var inBagCode = (item.InBagCode ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(inBagCode))
throw new ArgumentException("InBagCode 不能为空", nameof(item.InBagCode));
// 目录确保存在
Directory.CreateDirectory(outputDirectory);
var safeName = SanitizeFileName(inBagCode);
var filePath = Path.Combine(outputDirectory, safeName + ".csv");
if (File.Exists(filePath) && !overwrite)
/// <summary>
/// 导出单条记录为 CSV 文件,文件名为记录的 InBagCode自动追加 .csv 扩展名)。
/// 使用 App.config 中的 RawProUseCsvPath 作为导出目录,目录不存在会自动创建。
/// 失败时记录错误日志并返回空字符串。
/// </summary>
/// <param name="item">要导出的数据项,类型为 RawProUserCsvDtoInBagCode 不能为空)</param>
/// <param name="overwrite">若目标文件已存在,是否允许覆盖</param>
/// <returns>成功:返回导出后的完整文件路径;失败:返回空字符串</returns>
public string ExportSingle(RawProUserCsvDto item, bool overwrite = false)
{
try
{
throw new IOException($"文件已存在且不允许覆盖: {filePath}");
if (item is null) throw new ArgumentNullException(nameof(item));
// 线程安全:使用局部缓存目录变量,必要时从配置读取一次并回写字段。
var outputDir = RawProUseCsvPath;
if (string.IsNullOrWhiteSpace(outputDir))
{
outputDir = GetConfiguredOutputDirectory();
RawProUseCsvPath = outputDir;
}
var inBagCode = (item.InBagCode ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(inBagCode))
throw new ArgumentException("InBagCode 不能为空", nameof(item.InBagCode));
// 目录确保存在
Directory.CreateDirectory(outputDir);
var safeName = SanitizeFileName(inBagCode);
var filePath = Path.Combine(outputDir, safeName + ".csv");
if (File.Exists(filePath) && !overwrite)
{
throw new IOException($"文件已存在且不允许覆盖: {filePath}");
}
// 使用 UTF8 BOM便于 Excel 正确识别中文
using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
// 固定列顺序映射,并设置日期格式
csv.Context.RegisterClassMap<RawProUserCsvDtoMap>();
csv.WriteHeader<RawProUserCsvDto>();
csv.NextRecord();
csv.WriteRecord(item);
csv.NextRecord();
return filePath;
}
catch (Exception ex)
{
var code = item?.InBagCode ?? string.Empty;
var outputDir = RawProUseCsvPath;
LogService.Error($"导出CSV失败目录={outputDir}, InBagCode={code}", ex);
return string.Empty;
}
// 使用 UTF8 BOM便于 Excel 正确识别中文
using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
// 固定列顺序映射,并设置日期格式
csv.Context.RegisterClassMap<RawProUserCsvDtoMap>();
csv.WriteHeader<RawProUserCsvDto>();
csv.NextRecord();
csv.WriteRecord(item);
csv.NextRecord();
return filePath;
}
/// <summary>
/// 批量导出:把每一条记录分别导出为以各自 InBagCode 命名的 CSV 文件
/// 异步导出单条记录(包装线程池执行)
/// </summary>
/// <param name="items">数据集合</param>
/// <param name="outputDirectory">导出目录,不存在将自动创建</param>
/// <param name="overwrite">若目标文件已存在,是否允许覆盖</param>
/// <returns>成功导出的文件完整路径集合</returns>
public IEnumerable<string> ExportMany(IEnumerable<RawProUserCsvDto> items, string outputDirectory, bool overwrite = false)
public Task<string> ExportSingleAsync(RawProUserCsvDto item, bool overwrite = false)
=> Task.Run(() => ExportSingle(item, overwrite));
/// <summary>
/// 批量导出:将集合中的每一条记录分别导出为以 InBagCode 命名的 CSV 文件。
/// 成功项返回其路径,失败项返回空字符串(并已记录日志)。
/// </summary>
public IEnumerable<string> ExportMany(IEnumerable<RawProUserCsvDto> items, bool overwrite = false)
{
if (items is null) throw new ArgumentNullException(nameof(items));
var results = new List<string>();
foreach (var item in items)
foreach (var it in items)
{
// 逐条导出,沿用相同规则
var path = ExportSingle(item, outputDirectory, overwrite);
results.Add(path);
results.Add(ExportSingle(it, overwrite));
}
return results;
}
/// <summary>
/// 异步批量导出(并行度受限于线程池;如需更精细控制,可改为并行批处理)。
/// </summary>
public async Task<List<string>> ExportManyAsync(IEnumerable<RawProUserCsvDto> items, bool overwrite = false)
{
if (items is null) throw new ArgumentNullException(nameof(items));
var list = items.ToList();
var tasks = list.Select(it => Task.Run(() => ExportSingle(it, overwrite)));
var paths = await Task.WhenAll(tasks);
return paths.ToList();
}
/// <summary>
/// 清理文件名中的非法字符。
/// </summary>
@@ -108,44 +158,6 @@ namespace FATrace.WPLApp.Services
return sb.ToString();
}
/// <summary>
/// RawProUserCsvDto 的 CSV 列映射,固定输出顺序并设置日期格式。
/// </summary>
private sealed class RawProUserCsvDtoMap : ClassMap<RawProUserCsvDto>
{
public RawProUserCsvDtoMap()
{
//Map(x => x.RawCode).Index(0).Name(nameof(RawProUserCsvDto.RawCode));
//Map(x => x.RawName).Index(1).Name(nameof(RawProUserCsvDto.RawName));
//Map(x => x.InBagCode).Index(2).Name(nameof(RawProUserCsvDto.InBagCode));
//Map(x => x.BoxCode).Index(3).Name(nameof(RawProUserCsvDto.BoxCode));
//Map(x => x.Batch).Index(4).Name(nameof(RawProUserCsvDto.Batch));
//Map(x => x.ShelfLife).Index(5).Name(nameof(RawProUserCsvDto.ShelfLife));
//Map(x => x.Weight).Index(6).Name(nameof(RawProUserCsvDto.Weight));
//Map(x => x.DeliveryDate).Index(7).Name(nameof(RawProUserCsvDto.DeliveryDate));
//Map(x => x.RemainWeight).Index(8).Name(nameof(RawProUserCsvDto.RemainWeight));
//Map(x => x.StockWeight).Index(9).Name(nameof(RawProUserCsvDto.StockWeight));
//Map(x => x.WeightTime).Index(10).Name(nameof(RawProUserCsvDto.WeightTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss");
//Map(x => x.OpUser).Index(11).Name(nameof(RawProUserCsvDto.OpUser));
//Map(x => x.CheckUser).Index(12).Name(nameof(RawProUserCsvDto.CheckUser));
//Map(x => x.OutTime).Index(13).Name(nameof(RawProUserCsvDto.OutTime)).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss");
Map(x => x.RawCode).Index(0).Name("原料编号");
Map(x => x.RawName).Index(1).Name("原料名称");
Map(x => x.InBagCode).Index(2).Name("内袋二维码");
Map(x => x.BoxCode).Index(3).Name("外箱二维码");
Map(x => x.Batch).Index(4).Name("批号");
Map(x => x.ShelfLife).Index(5).Name("保质期");
Map(x => x.Weight).Index(6).Name("称重重量(g)");
Map(x => x.DeliveryDate).Index(7).Name("配料日期");
Map(x => x.RemainWeight).Index(8).Name("剩余重量(Kg)");
Map(x => x.StockWeight).Index(9).Name("入库总重量(Kg)");
Map(x => x.WeightTime).Index(10).Name("称重时间").TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss");
Map(x => x.OpUser).Index(11).Name("操作者");
Map(x => x.CheckUser).Index(12).Name("确认者");
Map(x => x.OutTime).Index(13).Name("出库时间").TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss");
}
}
}
}

View File

@@ -1,13 +1,15 @@
using AutoMapper;
using FATrace.Model;
using FATrace.WPLApp.Events;
using FATrace.WPLApp.ModelDto;
using FATrace.WPLApp.Models;
using HslCommunication;
using HslCommunication.Profinet.Keyence;
using Prism.Events;
using Prism.Mvvm;
using System.Collections.Concurrent;
using System.Windows;
using ComConfigHelper = FATrace.Com.ConfigHelper;
using Prism.Events;
using FATrace.WPLApp.Events;
namespace FATrace.WPLApp.Services
{
@@ -17,13 +19,14 @@ namespace FATrace.WPLApp.Services
/// </summary>
public class DataServices : BindableBase, IDisposable
{
public DataServices(ILogService logService, IFreeSql freeSql, IEventAggregator eventAggregator, SysRunService sysRunService)
public DataServices(ILogService logService, IFreeSql freeSql, IEventAggregator eventAggregator, IMapper mapper, SysRunService sysRunService, CsvServices csvServices)
{
LogService = logService;
FreeSql = freeSql;
EventAggregator = eventAggregator;
Mapper = mapper;
SysRunService = sysRunService;
CsvServices = csvServices;
LineSglModel = new LineSglModel();
LineSglModel.WeightScanCodeHandle += LineSglModel_WeightScanCodeHandle;
LineSglModel.BoxSprayCodeReqHandle += LineSglModel_BoxSprayCodeReqHandle;
@@ -32,6 +35,10 @@ namespace FATrace.WPLApp.Services
PLCLinkInitial();
StartPlcScan();
//var DD= RevData("DYG05030013,20250923,802,3,01,0001,");
//var dd1= RevData("DYG05030013,251111,10193,6,01,3");
//var dd2 = RevData("DYG05030013,251111,2116,6,01,3");
}
@@ -53,9 +60,13 @@ namespace FATrace.WPLApp.Services
if (ReqData.Count > 0)
{
var CodeItem = ReqData.FirstOrDefault();
KeyencePlcMcNet.Write("D1150", RevData(CodeItem!.Code + ",A"));
Console.WriteLine($"外箱喷码:{CodeItem.Code}-发送OK");
var BoxSprayCodeSource = CodeItem!.Code + ",A";
var BoxSprayCodeRev = RevData(BoxSprayCodeSource);
KeyencePlcMcNet.Write("D1150", BoxSprayCodeRev);
Console.WriteLine($"外箱喷码:{BoxSprayCodeSource}-发送OK");
//这里需要删除数据吗?还是等扫码后删除,会不会过来新的箱子
@@ -65,6 +76,7 @@ namespace FATrace.WPLApp.Services
//不存在请求的数据,报错
KeyencePlcMcNet.Write("D1102", (Int16)1);
Console.WriteLine($"外箱喷码:不存在请求的数据");
AddAlarm("外箱喷码", "不存在请求的数据");
}
@@ -72,10 +84,9 @@ namespace FATrace.WPLApp.Services
{
await Task.Delay(1000).ConfigureAwait(false);
KeyencePlcMcNet!.Write("D1102", (Int16)0);
//KeyencePlcMcNet.Write("D1150", new Int16[30]);
});
}
/// <summary>
@@ -89,17 +100,20 @@ namespace FATrace.WPLApp.Services
//首先复位PLC信号
KeyencePlcMcNet!.Write("D1200", (Int16)0);
string BoxScanCode = string.Empty;
BoxScanCode = this.BoxScanCode;
if (!string.IsNullOrEmpty(BoxScanCode))
{
//var IsExist = FreeSql.Select<RawProUse>().Where(a => a.BoxCode == BoxScanCode);
var IsExist = FreeSql.Select<RawProUse>().Where(a => a.BoxCode!.Contains(BoxScanCode));// 内包和外包一样的条码测试用 ???????????????
var IsExist = FreeSql.Select<RawProUse>().Where(a => a.BoxCode == BoxScanCode);
//var IsExist = FreeSql.Select<RawProUse>().Where(a => a.BoxCode!.Contains(BoxScanCode));// 内包和外包一样的条码测试用 ???????????????
if (IsExist.Count() > 0)
{
//存在条码数据 OK返回PLC结果
KeyencePlcMcNet!.Write("D1210", (Int16)1);
Console.WriteLine($"外箱扫描码:{BoxScanCode}-存在找到");
//// 加入队列
// 加入队列
//EnqueueMessage(WeightScanCode);
LogService.Info($"外箱扫描码:{BoxScanCode}");
@@ -109,28 +123,40 @@ namespace FATrace.WPLApp.Services
.ExecuteAffrows();
if (DeleteResult > 0)
{
Console.WriteLine($"外箱扫描码:{WeightScanCode}-删除临时队列数据成功");
LogService.Info($"外箱扫描码:{WeightScanCode}-删除临时队列数据成功");
Console.WriteLine($"外箱扫描码:{BoxScanCode}-删除临时队列数据成功");
LogService.Info($"外箱扫描码:{BoxScanCode}-删除临时队列数据成功");
}
else
{
Console.WriteLine($"外箱扫描码:{WeightScanCode}-删除临时队列数据失败");
LogService.Info($"外箱扫描码:{WeightScanCode}-删除临时队列数据失败");
Console.WriteLine($"外箱扫描码:{BoxScanCode}-删除临时队列数据失败");
LogService.Info($"外箱扫描码:{BoxScanCode}-删除临时队列数据失败");
AddAlarm("外箱扫描", $"{BoxScanCode}-删除临时队列数据失败");
}
var Result = FreeSql.Update<RawProUse>()
var UpdatedResult = FreeSql.Update<RawProUse>()
.Set(p => p.OutTime, DateTime.Now)
.Where(p => p.BoxCode == BoxScanCode + ",A")//外箱二维码就是外箱扫描码
.ExecuteAffrows();
if (Result > 0)
.Where(p => p.BoxCode == BoxScanCode)//外箱二维码就是外箱扫描码
.ExecuteUpdated();
if (UpdatedResult.Count() > 0)
{
Console.WriteLine($"外箱扫描码:{WeightScanCode}更新时间成功");
LogService.Info($"外箱扫描码:{WeightScanCode}更新时间成功");
var Data = CsvServices.ExportSingle(Mapper.Map<RawProUserCsvDto>(UpdatedResult.First()), true);
if (Data != null && !string.IsNullOrEmpty(Data))
{
Console.WriteLine($"外箱扫描码:{BoxScanCode} CSV文件生成");
}
else
{
Console.WriteLine($"外箱扫描码:{BoxScanCode} CSV文件失败");
}
Console.WriteLine($"外箱扫描码:{BoxScanCode}更新时间成功");
LogService.Info($"外箱扫描码:{BoxScanCode}更新时间成功");
}
else
{
Console.WriteLine($"外箱扫描码:{WeightScanCode}更新时间失败");
LogService.Warn($"外箱扫描码:{WeightScanCode}更新时间失败");
Console.WriteLine($"外箱扫描码:{BoxScanCode}更新时间失败");
LogService.Warn($"外箱扫描码:{BoxScanCode}更新时间失败");
AddAlarm("外箱扫描", $"{BoxScanCode}更新时间失败");
}
}
else
@@ -138,14 +164,16 @@ namespace FATrace.WPLApp.Services
//不存在条码数据 NG返回PLC结果报警
KeyencePlcMcNet!.Write("D1210", (Int16)2);
Console.WriteLine($"外箱扫描码:{WeightScanCode}-未发现外箱扫码数据在数据库中");
LogService.Warn($"外箱扫描码:{WeightScanCode}-未发现外箱扫码数据在数据库中");
Console.WriteLine($"外箱扫描码:{BoxScanCode}-未发现外箱扫码数据在数据库中");
LogService.Warn($"外箱扫描码:{BoxScanCode}-未发现外箱扫码数据在数据库中");
AddAlarm("外箱扫描", $"{BoxScanCode}-未发现外箱扫码数据在数据库中");
}
}
else
{
Console.WriteLine($"外箱扫描码为空");
LogService.Warn("外箱扫描码为空");
AddAlarm("外箱扫描", "外箱扫描码为空");
}
Task.Run(async () =>
@@ -155,7 +183,10 @@ namespace FATrace.WPLApp.Services
KeyencePlcMcNet.Write("D1250", new Int16[30]);
});
try { EventAggregator?.GetEvent<DashboardRefreshEvent>()?.Publish(true); } catch { }
//try { EventAggregator?.GetEvent<DashboardRefreshEvent>()?.Publish(true); } catch { }
}
/// <summary>
@@ -200,6 +231,7 @@ namespace FATrace.WPLApp.Services
{
Console.WriteLine($"称重扫描码:{WeightScanCode}更新时间失败");
LogService.Warn($"称重扫描码:{WeightScanCode}更新时间失败");
AddAlarm("称重扫描", $"{WeightScanCode}更新时间失败");
}
}
else
@@ -209,6 +241,7 @@ namespace FATrace.WPLApp.Services
Console.WriteLine($"称重扫描码:{WeightScanCode}-未发现内包扫码数据在数据库中");
LogService.Warn($"称重扫描码:{WeightScanCode}-未发现内包扫码数据在数据库中");
AddAlarm("称重扫描", $"{WeightScanCode}-未发现内包扫码数据在数据库中");
}
Task.Run(async () =>
@@ -225,6 +258,7 @@ namespace FATrace.WPLApp.Services
{
Console.WriteLine($"称重扫描码为空");
LogService.Warn("称重扫描码为空");
AddAlarm("称重扫描", "称重扫描码为空");
}
}
@@ -232,7 +266,9 @@ namespace FATrace.WPLApp.Services
public ILogService LogService { get; }
public IFreeSql FreeSql { get; }
public IEventAggregator EventAggregator { get; }
public IMapper Mapper { get; }
public SysRunService SysRunService { get; }
public CsvServices CsvServices { get; }
private bool _plcConnected;
public bool PlcConnected
@@ -281,27 +317,6 @@ namespace FATrace.WPLApp.Services
/// </summary>
private readonly ConcurrentQueue<string> _messageQueue = new();
/// <summary>
/// 入队:将字符串加入 FIFO 队列(先进先出)。允许空字符串,不允许 null。
/// </summary>
/// <param name="message">要加入队列的字符串</param>
/// <exception cref="ArgumentNullException">当 message 为 null 时抛出</exception>
public void EnqueueMessage(string message)
{
if (message is null) throw new ArgumentNullException(nameof(message));
_messageQueue.Enqueue(message);
}
/// <summary>
/// 出队:尝试从 FIFO 队列取出一个字符串。
/// </summary>
/// <param name="message">输出参数:若成功则为取出的字符串;若失败(队列为空)则为 default</param>
/// <returns>是否成功取出</returns>
public bool TryDequeueMessage(out string message)
{
return _messageQueue.TryDequeue(out message);
}
/// <summary>
/// 产线信号模型
/// </summary>
@@ -327,7 +342,6 @@ namespace FATrace.WPLApp.Services
KeyencePlcMcNet.Port = Port;
KeyencePlcMcNet.ConnectClose();
KeyencePlcMcNet.ConnectTimeOut = 3000; // 连接3秒超时
OperateResult connect = KeyencePlcMcNet.ConnectServer();
if (connect.IsSuccess)//初始连接状态的显示判断
@@ -443,7 +457,7 @@ namespace FATrace.WPLApp.Services
OperateResultWeightScanCode = KeyencePlcMcNet!.ReadString("D1050", 20);
if (OperateResultWeightScanCode.IsSuccess)
{
WeightScanCode = RevData(OperateResultWeightScanCode.Content).Replace("\r", "").Replace("\n", "");
WeightScanCode = RevData(OperateResultWeightScanCode.Content).Replace("\r", "").Replace("\n", "").Trim();
}
OperateResultWeightScanSgl = KeyencePlcMcNet!.ReadInt16("D1000");
if (OperateResultWeightScanSgl.IsSuccess)
@@ -462,7 +476,8 @@ namespace FATrace.WPLApp.Services
OperateResultBoxScanCode = KeyencePlcMcNet!.ReadString("D1250", 20);
if (OperateResultBoxScanCode.IsSuccess)
{
BoxScanCode = RevData(OperateResultBoxScanCode.Content).Replace("\r", "").Replace("\n", "");
BoxScanCode = RevData(OperateResultBoxScanCode.Content).Replace("\r", "").Replace("\n", "").Trim();
//BoxScanCode = OperateResultBoxScanCode.Content.Replace("\r", "").Replace("\n", "");
}
OperateResultBoxScanSgl = KeyencePlcMcNet!.ReadInt16("D1200");
if (OperateResultBoxScanSgl.IsSuccess)
@@ -558,7 +573,7 @@ namespace FATrace.WPLApp.Services
/// <summary>
/// 把字符串对调
/// </summary>
private string RevData(string Code)
public string RevData(string Code)
{
// 需求PLC 字符串按“字”(2字节)进行高低字节对调,导致 ABCDEF -> BADCFE
// 纠正方式:按两个字符一组交换顺序,奇数长度保留最后一个字符
@@ -578,6 +593,8 @@ namespace FATrace.WPLApp.Services
else
{
// 奇数长度,最后一个字符原样保留
//sb.Append((char)0x0);
sb.Append('\0');
sb.Append(src[i]);
}
}
@@ -585,5 +602,25 @@ namespace FATrace.WPLApp.Services
return sb.ToString();
}
/// <summary>
/// 写入历史报警
/// </summary>
private void AddAlarm(string category, string message)
{
try
{
FreeSql.Insert(new HistoryAlarm
{
Category = category,
Message = message,
CreateTime = DateTime.Now
}).ExecuteAffrows();
}
catch (Exception ex)
{
LogService.Error($"写入历史报警失败: {ex.Message}");
}
}
}
}

View File

@@ -1,10 +1,5 @@
using FATrace.WPLApp.ModelDto;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FATrace.WPLApp.Services
{