216 lines
7.8 KiB
C#
216 lines
7.8 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Net.Http;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Model.Model;
|
||
using Newtonsoft.Json;
|
||
|
||
namespace DeviceCommand.Base
|
||
{
|
||
|
||
public class EnovaDataReporter : IEnovaDataReporter
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
|
||
// ========= 静态注册表:用于 EnovaDataController 反向查找设备实例并分发数据 =========
|
||
private static readonly List<EnovaDataReporter> _instances = new List<EnovaDataReporter>();
|
||
private static readonly object _registryLock = new object();
|
||
|
||
/// <summary>
|
||
/// 当前已注册的所有 EnovaDataReporter 实例(线程安全快照)
|
||
/// </summary>
|
||
public static IReadOnlyList<EnovaDataReporter> Instances
|
||
{
|
||
get
|
||
{
|
||
lock (_registryLock)
|
||
{
|
||
return _instances.ToList();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设备编码:用于在多设备场景下按 deviceCode 过滤分发
|
||
/// 留空表示该实例接收所有上报数据
|
||
/// </summary>
|
||
public string DeviceCode { get; set; } = string.Empty;
|
||
|
||
// 显式实现/自动属性,方便外部随时更新配置
|
||
public string TargetUrl { get; set; } = "http://127.0.0.1:8080/api/channel/state";
|
||
public int TimeoutMilliseconds { get; set; } = 5000;
|
||
|
||
/// <summary>
|
||
/// 收到下位机 POST 上报数据时触发
|
||
/// </summary>
|
||
public event EventHandler<EnovaChannelDataReceivedEventArgs>? ChannelDataReceived;
|
||
|
||
|
||
public EnovaDataReporter(HttpClient httpClient)
|
||
{
|
||
_httpClient = httpClient ?? new HttpClient();
|
||
|
||
// 自动注册到静态实例表,便于 Controller 反向找到本实例
|
||
lock (_registryLock)
|
||
{
|
||
_instances.Add(this);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从静态注册表中注销当前实例(设备销毁/释放时调用)
|
||
/// </summary>
|
||
public virtual void Unregister()
|
||
{
|
||
lock (_registryLock)
|
||
{
|
||
_instances.Remove(this);
|
||
}
|
||
}
|
||
|
||
public async Task<ApiResponse> ReportChannelStateAsync(List<EnovaChannelData> dataList, CancellationToken ct = default)
|
||
{
|
||
if (dataList == null || dataList.Count == 0)
|
||
{
|
||
return new ApiResponse { Success = false, ErrorInfo = "上报数据集合为空" };
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(TargetUrl))
|
||
{
|
||
return new ApiResponse { Success = false, ErrorInfo = "目标上报 URL 未配置" };
|
||
}
|
||
|
||
try
|
||
{
|
||
// 1. 序列化为标准 JSON 字符串
|
||
string jsonPayload = JsonConvert.SerializeObject(dataList);
|
||
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||
|
||
// 2. 绑定联动超时控制
|
||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||
if (TimeoutMilliseconds > 0)
|
||
{
|
||
cts.CancelAfter(TimeoutMilliseconds);
|
||
}
|
||
|
||
HttpResponseMessage response = await _httpClient.PostAsync(TargetUrl, content, cts.Token);
|
||
|
||
// 4. 解析返回值
|
||
if (response.IsSuccessStatusCode)
|
||
{
|
||
string responseContent = await response.Content.ReadAsStringAsync();
|
||
var result = JsonConvert.DeserializeObject<ApiResponse>(responseContent);
|
||
return result ?? new ApiResponse { Success = true }; // 防止对方返回空Body
|
||
}
|
||
else
|
||
{
|
||
return new ApiResponse
|
||
{
|
||
Success = false,
|
||
ErrorInfo = $"服务器响应错误代码: {(int)response.StatusCode} {response.ReasonPhrase}"
|
||
};
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 完美承接你上位机原有的异常日志记录器逻辑
|
||
// Logger.LoggerHelper.ErrorWithNotify($"Enova3 数据上传失败: {ex.Message}");
|
||
return new ApiResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理 Controller 转发过来的下位机上报数据,并触发事件
|
||
/// </summary>
|
||
public virtual ApiResponse HandleIncomingChannelData(List<EnovaChannelData> dataList)
|
||
{
|
||
if (dataList == null || dataList.Count == 0)
|
||
{
|
||
return new ApiResponse { Success = false, ErrorInfo = "接收到的数据为空" };
|
||
}
|
||
|
||
try
|
||
{
|
||
// 触发事件,让派生类(如 CTS3)或外部订阅者处理具体业务
|
||
ChannelDataReceived?.Invoke(this, new EnovaChannelDataReceivedEventArgs(dataList));
|
||
return new ApiResponse { Success = true, ErrorInfo = string.Empty };
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return new ApiResponse { Success = false, ErrorInfo = $"处理上报数据时异常: {ex.Message}" };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 由 Controller 调用:将下位机上报的数据广播到所有匹配的实例
|
||
/// 当数据中含 DeviceCode 时,按 DeviceCode 精确匹配;否则广播给所有实例
|
||
/// </summary>
|
||
public static ApiResponse Dispatch(List<EnovaChannelData> dataList)
|
||
{
|
||
if (dataList == null || dataList.Count == 0)
|
||
{
|
||
return new ApiResponse { Success = false, ErrorInfo = "接收到的数据为空" };
|
||
}
|
||
|
||
EnovaDataReporter[] snapshot;
|
||
lock (_registryLock)
|
||
{
|
||
snapshot = _instances.ToArray();
|
||
}
|
||
|
||
if (snapshot.Length == 0)
|
||
{
|
||
return new ApiResponse { Success = false, ErrorInfo = "无可用的设备实例接收数据" };
|
||
}
|
||
|
||
// 按 DeviceCode 分组分发:同一批数据可能来自多个 deviceCode
|
||
var groups = dataList
|
||
.GroupBy(d => d?.DeviceCode ?? string.Empty)
|
||
.ToList();
|
||
|
||
var errors = new List<string>();
|
||
int successCount = 0;
|
||
|
||
foreach (var group in groups)
|
||
{
|
||
string deviceCode = group.Key;
|
||
var items = group.ToList();
|
||
|
||
// 选取目标:1) 设置了相同 DeviceCode 的实例;2) 没设置 DeviceCode 的实例(通用接收者)
|
||
var targets = snapshot
|
||
.Where(r => string.Equals(r.DeviceCode, deviceCode, StringComparison.OrdinalIgnoreCase)
|
||
|| string.IsNullOrEmpty(r.DeviceCode))
|
||
.ToList();
|
||
|
||
if (targets.Count == 0)
|
||
{
|
||
errors.Add($"DeviceCode={deviceCode} 无匹配的设备实例");
|
||
continue;
|
||
}
|
||
|
||
foreach (var target in targets)
|
||
{
|
||
var resp = target.HandleIncomingChannelData(items);
|
||
if (resp != null && resp.Success)
|
||
{
|
||
successCount++;
|
||
}
|
||
else
|
||
{
|
||
errors.Add(resp?.ErrorInfo ?? "未知错误");
|
||
}
|
||
}
|
||
}
|
||
|
||
return new ApiResponse
|
||
{
|
||
Success = errors.Count == 0,
|
||
ErrorInfo = errors.Count == 0 ? string.Empty : string.Join(";", errors)
|
||
};
|
||
}
|
||
}
|
||
}
|