Files
qhmes/yy-admin-master/YY.Admin.Services/Service/RubberQuickTest/RubberQuickTestRecordService.cs

503 lines
20 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 Microsoft.Extensions.Configuration;
using Prism.Events;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
using YY.Admin.Core;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Events;
using YY.Admin.Core.Services;
namespace YY.Admin.Services.Service.RubberQuickTest;
public class RubberQuickTestRecordService : IRubberQuickTestRecordService, ISingletonDependency
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly INetworkMonitor _networkMonitor;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private readonly SemaphoreSlim _syncLock = new(1, 1);
private readonly object _cacheLock = new();
private readonly string _cacheFilePath;
private List<RubberQuickTestRecordLocalItem> _localItems = new();
private static readonly JsonSerializerOptions _jsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new NullableDateTimeJsonConverter() }
};
public RubberQuickTestRecordService(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
INetworkMonitor networkMonitor,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_networkMonitor = networkMonitor;
_eventAggregator = eventAggregator;
_logger = logger;
var appDataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"YY.Admin", "sync-cache");
Directory.CreateDirectory(appDataDir);
_cacheFilePath = Path.Combine(appDataDir, "rubber-quick-test-record-items.json");
LoadCacheFromDisk();
_logger.Information($"[快检记录同步] 初始化完成,本地记录={_localItems.Count}");
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
if (_networkMonitor.IsOnline)
_ = Task.Run(() => PushPendingAsync(CancellationToken.None));
}
private string BaseUrl => (_configuration.GetValue<string>("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/');
private int DefaultTenantId => (int?)_configuration.GetValue<long?>("JeecgIntegration:DefaultTenantId") ?? 1002;
private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi");
public async Task<RubberQuickTestRecordPageResult> PageAsync(
int pageNo, int pageSize,
string? filterRecordNo = null,
string? filterRubberMaterialName = null,
string? filterPlanNo = null,
CancellationToken ct = default)
{
var rows = await BuildAllRowsAsync(ct).ConfigureAwait(false);
var filtered = ApplyFilters(rows, filterRecordNo, filterRubberMaterialName, filterPlanNo);
var total = filtered.Count;
var records = filtered
.OrderByDescending(r => r.InspectDate ?? DateTime.MinValue)
.Skip(Math.Max(0, (pageNo - 1) * pageSize))
.Take(pageSize)
.ToList();
return new RubberQuickTestRecordPageResult(records, total, pageNo, pageSize);
}
public async Task<MesXslRubberQuickTestRecord?> GetByIdAsync(string id, CancellationToken ct = default)
{
RubberQuickTestRecordLocalItem? local = null;
lock (_cacheLock)
{
local = _localItems.FirstOrDefault(x =>
string.Equals(x.LocalId, id, StringComparison.OrdinalIgnoreCase)
|| string.Equals(x.MesId, id, StringComparison.OrdinalIgnoreCase)
|| string.Equals(x.Record.RecordNo, id, StringComparison.OrdinalIgnoreCase));
}
if (local != null)
return CloneRecord(local.Record);
if (_networkMonitor.IsOnline)
{
try
{
var url = $"{BaseUrl}/xslmes/mesXslRubberQuickTestRecord/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
using var client = CreateClient();
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
if (resp.IsSuccessStatusCode)
{
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("result", out var resultEl))
return resultEl.Deserialize<MesXslRubberQuickTestRecord>(_jsonOpts);
}
}
catch (Exception ex)
{
_logger.Warning($"[快检记录详情] 远端查询异常 id={id}: {ex.Message}");
}
}
return null;
}
public RubberQuickTestRecordLocalItem? GetByLocalId(string localId)
{
lock (_cacheLock)
{
var item = _localItems.FirstOrDefault(x => string.Equals(x.LocalId, localId, StringComparison.OrdinalIgnoreCase));
return item == null ? null : CloneLocalItem(item);
}
}
public async Task<RubberQuickTestRecordSaveResult> SaveAsync(MesXslRubberQuickTestRecord entity, CancellationToken ct = default)
{
var record = CloneRecord(entity);
record.CreateTime ??= DateTime.Now;
record.InspectTime ??= record.CreateTime;
var item = new RubberQuickTestRecordLocalItem
{
LocalId = Guid.NewGuid().ToString("N"),
LocalCreateTime = DateTime.Now,
SyncStatus = "Pending",
Record = record
};
if (_networkMonitor.IsOnline)
{
try
{
var recordNo = await RemoteAddAsync(record, ct).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(recordNo))
{
item.Record.RecordNo = recordNo;
item.SyncStatus = "Synced";
}
}
catch (Exception ex)
{
item.SyncStatus = "Failed";
item.SyncError = ex.Message;
_logger.Warning($"[快检记录新增] 远端失败,保留本地:{ex.Message}");
}
}
lock (_cacheLock)
{
_localItems.Add(CloneLocalItem(item));
SaveCacheToDiskUnsafe();
}
_eventAggregator.GetEvent<RubberQuickTestRecordChangedEvent>()
.Publish(new RubberQuickTestRecordChangedPayload { Action = "add", RecordId = item.LocalId });
return new RubberQuickTestRecordSaveResult
{
LocalId = item.LocalId,
SyncStatus = item.SyncStatus,
Record = CloneRecord(item.Record)
};
}
public bool DeleteFailedLocal(string localId)
{
if (string.IsNullOrWhiteSpace(localId)) return false;
bool removed;
lock (_cacheLock)
{
var item = _localItems.FirstOrDefault(x =>
string.Equals(x.LocalId, localId.Trim(), StringComparison.OrdinalIgnoreCase));
if (item == null || !string.Equals(item.SyncStatus, "Failed", StringComparison.OrdinalIgnoreCase))
return false;
removed = _localItems.Remove(item);
if (removed)
SaveCacheToDiskUnsafe();
}
if (!removed) return false;
_eventAggregator.GetEvent<RubberQuickTestRecordChangedEvent>()
.Publish(new RubberQuickTestRecordChangedPayload { Action = "delete", RecordId = localId });
_logger.Information($"[快检记录删除] 已删除同步失败本地记录 localId={localId}");
return true;
}
public string GenerateRecordNo(string rubberMaterialName)
{
var dateStr = DateTime.Now.ToString("yyyyMMdd");
var material = rubberMaterialName.Trim();
int maxSeq = 0;
lock (_cacheLock)
{
foreach (var item in _localItems)
{
var no = item.Record.RecordNo;
if (string.IsNullOrWhiteSpace(no) || !no.StartsWith(dateStr, StringComparison.Ordinal) || !no.EndsWith(material, StringComparison.Ordinal))
continue;
var seqPart = no.Substring(dateStr.Length, Math.Min(4, no.Length - dateStr.Length - material.Length));
if (int.TryParse(seqPart, out var seq))
maxSeq = Math.Max(maxSeq, seq);
}
}
return dateStr + (maxSeq + 1).ToString("D4") + material;
}
private async Task<List<RubberQuickTestRecordListRow>> BuildAllRowsAsync(CancellationToken ct)
{
var rows = new List<RubberQuickTestRecordListRow>();
var seenRecordNos = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
lock (_cacheLock)
{
foreach (var item in _localItems)
{
rows.Add(ToListRow(item));
if (!string.IsNullOrWhiteSpace(item.Record.RecordNo))
seenRecordNos.Add(item.Record.RecordNo);
}
}
if (_networkMonitor.IsOnline)
{
try
{
foreach (var remote in await FetchRemoteListAsync(ct).ConfigureAwait(false))
{
if (!string.IsNullOrWhiteSpace(remote.RecordNo) && seenRecordNos.Contains(remote.RecordNo))
continue;
rows.Add(ToListRow(remote, "Synced", remote.Id));
}
}
catch (Exception ex)
{
_logger.Warning($"[快检记录列表] 远端拉取失败:{ex.Message}");
}
}
return rows;
}
private static RubberQuickTestRecordListRow ToListRow(RubberQuickTestRecordLocalItem item) =>
ToListRow(item.Record, item.SyncStatus, item.MesId, item.LocalId);
private static RubberQuickTestRecordListRow ToListRow(
MesXslRubberQuickTestRecord r,
string syncStatus,
string? mesId = null,
string? localId = null) => new()
{
LocalId = localId,
MesId = mesId ?? r.Id,
RecordNo = r.RecordNo,
ProductionDate = r.ProductionDate,
ProdEquipmentName = r.ProdEquipmentName,
WorkShiftDisplay = r.WorkShiftText ?? r.WorkShift,
ProductionPlanNo = r.ProductionPlanNo,
RubberMaterialName = r.RubberMaterialName,
StdName = r.StdName,
TestMethodName = r.TestMethodName,
QuickTestTypeName = r.QuickTestTypeName,
TrainNo = r.TrainNo,
InspectTimes = r.InspectTimes,
InspectorRealname = r.InspectorRealname,
InspectDate = r.CreateTime ?? r.InspectTime,
InspectResultDisplay = r.InspectResultText ?? (r.InspectResult == "1" ? "合格" : r.InspectResult == "0" ? "不合格" : ""),
SyncStatus = syncStatus
};
private async Task<string?> RemoteAddAsync(MesXslRubberQuickTestRecord entity, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslRubberQuickTestRecord/anon/add?tenantId={DefaultTenantId}";
var payload = CloneRecord(entity);
payload.Id = null;
using var client = CreateClient();
var body = JsonSerializer.Serialize(payload, _jsonOpts);
using var content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("success", out var successEl) || !successEl.GetBoolean())
{
var msg = doc.RootElement.TryGetProperty("message", out var msgEl) ? msgEl.GetString() : "保存失败";
throw new InvalidOperationException(msg ?? "保存失败");
}
if (doc.RootElement.TryGetProperty("result", out var resultEl) && resultEl.ValueKind == JsonValueKind.String)
return resultEl.GetString();
return entity.RecordNo;
}
private async Task<List<MesXslRubberQuickTestRecord>> FetchRemoteListAsync(CancellationToken ct)
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["pageNo"] = "1";
query["pageSize"] = "10000";
query["tenantId"] = DefaultTenantId.ToString();
var url = $"{BaseUrl}/xslmes/mesXslRubberQuickTestRecord/anon/list?{query}";
using var client = CreateClient();
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var result = doc.RootElement.GetProperty("result");
return result.GetProperty("records").Deserialize<List<MesXslRubberQuickTestRecord>>(_jsonOpts) ?? new();
}
private void OnNetworkStatusChanged(bool isOnline)
{
if (!isOnline) return;
_ = Task.Run(() => PushPendingAsync(CancellationToken.None));
}
private async Task PushPendingAsync(CancellationToken ct)
{
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false)) return;
try
{
List<RubberQuickTestRecordLocalItem> pending;
lock (_cacheLock)
{
pending = _localItems.Where(x => x.SyncStatus != "Synced").Select(CloneLocalItem).ToList();
}
foreach (var item in pending)
{
if (!_networkMonitor.IsOnline) break;
try
{
var recordNo = await RemoteAddAsync(item.Record, ct).ConfigureAwait(false);
lock (_cacheLock)
{
var target = _localItems.FirstOrDefault(x => x.LocalId == item.LocalId);
if (target == null) continue;
if (!string.IsNullOrWhiteSpace(recordNo))
target.Record.RecordNo = recordNo;
target.SyncStatus = "Synced";
target.SyncError = null;
SaveCacheToDiskUnsafe();
}
}
catch (Exception ex)
{
lock (_cacheLock)
{
var target = _localItems.FirstOrDefault(x => x.LocalId == item.LocalId);
if (target != null)
{
target.SyncStatus = "Failed";
target.SyncError = ex.Message;
SaveCacheToDiskUnsafe();
}
}
}
}
}
finally
{
_syncLock.Release();
}
}
private static List<RubberQuickTestRecordListRow> ApplyFilters(
List<RubberQuickTestRecordListRow> source,
string? filterRecordNo,
string? filterRubberMaterialName,
string? filterPlanNo)
{
IEnumerable<RubberQuickTestRecordListRow> q = source;
if (!string.IsNullOrWhiteSpace(filterRecordNo))
q = q.Where(r => (r.RecordNo ?? "").Contains(filterRecordNo.Trim(), StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(filterRubberMaterialName))
q = q.Where(r => (r.RubberMaterialName ?? "").Contains(filterRubberMaterialName.Trim(), StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(filterPlanNo))
q = q.Where(r => (r.ProductionPlanNo ?? "").Contains(filterPlanNo.Trim(), StringComparison.OrdinalIgnoreCase));
return q.ToList();
}
private void LoadCacheFromDisk()
{
try
{
if (!File.Exists(_cacheFilePath)) return;
var data = JsonSerializer.Deserialize<List<RubberQuickTestRecordLocalItem>>(File.ReadAllText(_cacheFilePath), _jsonOpts);
_localItems = data ?? new();
}
catch { _localItems = new(); }
}
private void SaveCacheToDiskUnsafe() =>
File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localItems, _jsonOpts));
private static RubberQuickTestRecordLocalItem CloneLocalItem(RubberQuickTestRecordLocalItem src) => new()
{
LocalId = src.LocalId,
MesId = src.MesId,
SyncStatus = src.SyncStatus,
SyncError = src.SyncError,
LocalCreateTime = src.LocalCreateTime,
Record = CloneRecord(src.Record)
};
private static MesXslRubberQuickTestRecord CloneRecord(MesXslRubberQuickTestRecord src) => new()
{
Id = src.Id,
RecordNo = src.RecordNo,
RubberMaterialId = src.RubberMaterialId,
RubberMaterialName = src.RubberMaterialName,
StdId = src.StdId,
StdName = src.StdName,
TestMethodId = src.TestMethodId,
TestMethodName = src.TestMethodName,
ProdEquipmentLedgerId = src.ProdEquipmentLedgerId,
ProdEquipmentName = src.ProdEquipmentName,
ProductionDate = src.ProductionDate,
TrainNo = src.TrainNo,
WorkShift = src.WorkShift,
InspectTimes = src.InspectTimes,
InspectTime = src.InspectTime,
InspectorUserId = src.InspectorUserId,
InspectorUsername = src.InspectorUsername,
InspectorRealname = src.InspectorRealname,
QuickTestTypeId = src.QuickTestTypeId,
QuickTestTypeName = src.QuickTestTypeName,
InspectResult = src.InspectResult,
ProductionPlanNo = src.ProductionPlanNo,
CreateTime = src.CreateTime,
StdLineList = src.StdLineList?.Select(l => new MesXslRubberQuickTestRecordStdLine
{
Id = l.Id, RecordId = l.RecordId, DataPointId = l.DataPointId, PointName = l.PointName,
LowerLimit = l.LowerLimit, UpperLimit = l.UpperLimit, LowerWarn = l.LowerWarn,
UpperWarn = l.UpperWarn, TargetValue = l.TargetValue, SortNo = l.SortNo
}).ToList(),
RawLineList = src.RawLineList?.Select(l => new MesXslRubberQuickTestRecordRawLine
{
Id = l.Id, RecordId = l.RecordId, RowNo = l.RowNo, DataPointId = l.DataPointId,
InspectItem = l.InspectItem, LowerLimit = l.LowerLimit, UpperLimit = l.UpperLimit,
InspectValue = l.InspectValue, RowInspectResult = l.RowInspectResult, SortNo = l.SortNo
}).ToList(),
ChartPointList = src.ChartPointList?.Select(p => new MesXslRubberQuickTestRecordChartPoint
{
Id = p.Id, RecordId = p.RecordId, TimeMin = p.TimeMin, UpperTemp = p.UpperTemp,
LowerTemp = p.LowerTemp, TorqueS = p.TorqueS, SortNo = p.SortNo
}).ToList(),
WorkShiftText = src.WorkShiftText,
InspectResultText = src.InspectResultText
};
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
{
private static readonly string[] SupportedFormats =
[
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.fff",
"yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm:ss.fff",
"yyyy-MM-ddTHH:mm:ssZ", "yyyy-MM-ddTHH:mm:ss.fffZ",
"yyyy-MM-dd"
];
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
if (reader.TokenType == JsonTokenType.String)
{
var raw = reader.GetString();
if (string.IsNullOrWhiteSpace(raw)) return null;
if (DateTime.TryParseExact(raw, SupportedFormats,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeLocal, out var exact)) return exact;
if (DateTime.TryParse(raw, out var fallback)) return fallback;
}
throw new JsonException($"无法转换为 DateTime?token={reader.TokenType}");
}
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value == null) writer.WriteNullValue();
else writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss"));
}
}
}