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 _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("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/'); private int DefaultTenantId => (int?)_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1002; private HttpClient CreateClient() => _httpClientFactory.CreateClient("JeecgApi"); public async Task 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 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(_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 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() .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() .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> BuildAllRowsAsync(CancellationToken ct) { var rows = new List(); var seenRecordNos = new HashSet(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 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> 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>(_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 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 ApplyFilters( List source, string? filterRecordNo, string? filterRubberMaterialName, string? filterPlanNo) { IEnumerable 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>(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 { 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")); } } }