using Microsoft.Extensions.Configuration; using Prism.Events; using System.IO; using System.Net.Http; 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.RubberQuickTestStd; /// 胶料快检实验标准:MES 只读拉取 + 本地缓存,断网读缓存,联网刷新 public class RubberQuickTestStdService : IRubberQuickTestStdService, 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 _localCache = new(); private static readonly JsonSerializerOptions _jsonOpts = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, Converters = { new NullableDateTimeJsonConverter() } }; public RubberQuickTestStdService( 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, "mes-xsl-rubber-quick-test-std-cache.json"); LoadCacheFromDisk(); _logger.Information($"[快检实验标准] 服务初始化,缓存={_localCache.Count},在线={_networkMonitor.IsOnline}"); _networkMonitor.StatusChanged += OnNetworkStatusChanged; if (_networkMonitor.IsOnline) _ = Task.Run(() => SyncFromRemoteAsync(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? stdName = null, string? rubberMaterialName = null, string? enableStatus = null, CancellationToken ct = default) { if (_networkMonitor.IsOnline) { try { await SyncFromRemoteAsync(ct).ConfigureAwait(false); } catch (Exception ex) { _logger.Warning($"[快检实验标准] 列表拉取失败,使用本地缓存:{ex.Message}"); } } List source; lock (_cacheLock) source = _localCache.Select(CloneMain).ToList(); var filtered = ApplyFilters(source, stdName, rubberMaterialName, enableStatus); var total = filtered.Count; var records = filtered .Skip(Math.Max(0, (pageNo - 1) * pageSize)) .Take(pageSize) .ToList(); return new RubberQuickTestStdPageResult(records, total, pageNo, pageSize); } public async Task GetByIdAsync(string id, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(id)) return null; if (_networkMonitor.IsOnline) { try { var url = $"{BaseUrl}/xslmes/mesXslRubberQuickTestStd/anon/queryById?id={Uri.EscapeDataString(id)}"; 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)) { var entity = resultEl.Deserialize(_jsonOpts); if (entity != null) { UpsertLocalCacheMain(entity); return CloneMain(entity); } } } } catch (Exception ex) { _logger.Warning($"[快检实验标准] 详情拉取失败 id={id},回退缓存:{ex.Message}"); } } lock (_cacheLock) { var found = _localCache.FirstOrDefault(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase)); return found != null ? CloneMain(found) : null; } } public async Task SyncFromRemoteAsync(CancellationToken ct = default) { await _syncLock.WaitAsync(ct).ConfigureAwait(false); try { if (!_networkMonitor.IsOnline) return; var query = HttpUtility.ParseQueryString(string.Empty); query["pageNo"] = "1"; query["pageSize"] = "10000"; query["tenantId"] = DefaultTenantId.ToString(); var url = $"{BaseUrl}/xslmes/mesXslRubberQuickTestStd/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 records = doc.RootElement.GetProperty("result").GetProperty("records") .Deserialize>(_jsonOpts) ?? new(); lock (_cacheLock) { _localCache = records.Select(CloneMain).ToList(); SaveCacheToDiskUnsafe(); } _logger.Information($"[快检实验标准] 同步完成 count={records.Count}"); } catch (Exception ex) { _logger.Warning($"[快检实验标准] 远程同步失败:{ex.Message}"); } finally { _syncLock.Release(); } } private void OnNetworkStatusChanged(bool isOnline) { if (!isOnline) return; _ = Task.Run(async () => { await SyncFromRemoteAsync(CancellationToken.None).ConfigureAwait(false); _eventAggregator.GetEvent() .Publish(new RubberQuickTestStdChangedPayload { Action = "reconnect" }); }); } private static List ApplyFilters( List source, string? stdName, string? rubberMaterialName, string? enableStatus) { IEnumerable q = source; if (!string.IsNullOrWhiteSpace(stdName)) q = q.Where(x => (x.StdName ?? "").Contains(stdName.Trim(), StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(rubberMaterialName)) q = q.Where(x => (x.RubberMaterialName ?? "").Contains(rubberMaterialName.Trim(), StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrWhiteSpace(enableStatus)) q = q.Where(x => string.Equals(x.EnableStatus, enableStatus.Trim(), StringComparison.OrdinalIgnoreCase)); return q.OrderByDescending(x => x.CreateTime ?? DateTime.MinValue).ToList(); } private void UpsertLocalCacheMain(MesXslRubberQuickTestStd entity) { if (string.IsNullOrWhiteSpace(entity.Id)) return; lock (_cacheLock) { var idx = _localCache.FindIndex(x => string.Equals(x.Id, entity.Id, StringComparison.OrdinalIgnoreCase)); var copy = CloneMain(entity); if (idx >= 0) _localCache[idx] = copy; else _localCache.Insert(0, copy); SaveCacheToDiskUnsafe(); } } private void LoadCacheFromDisk() { try { if (!File.Exists(_cacheFilePath)) return; _localCache = JsonSerializer.Deserialize>( File.ReadAllText(_cacheFilePath), _jsonOpts) ?? new(); } catch { _localCache = new(); } } private void SaveCacheToDiskUnsafe() => File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts)); private static MesXslRubberQuickTestStd CloneMain(MesXslRubberQuickTestStd x) => new() { Id = x.Id, StdName = x.StdName, TestMethodId = x.TestMethodId, TestMethodName = x.TestMethodName, MixerType = x.MixerType, RubberMaterialId = x.RubberMaterialId, RubberMaterialName = x.RubberMaterialName, PsCompileId = x.PsCompileId, IssueNumber = x.IssueNumber, IssueDate = x.IssueDate, IssueDeptId = x.IssueDeptId, IssueDeptName = x.IssueDeptName, EnableStatus = x.EnableStatus, AuditStatus = x.AuditStatus, TenantId = x.TenantId, SysOrgCode = x.SysOrgCode, CreateBy = x.CreateBy, CreateTime = x.CreateTime, UpdateBy = x.UpdateBy, UpdateTime = x.UpdateTime, LineList = x.LineList?.Select(l => new MesXslRubberQuickTestStdLine { Id = l.Id, StdId = l.StdId, 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() }; private sealed class NullableDateTimeJsonConverter : JsonConverter { private static readonly string[] Formats = [ "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, Formats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeLocal, out var dt)) return dt; if (DateTime.TryParse(raw, out var fb)) return fb; } throw new JsonException($"无法转换为 DateTime?,token={reader.TokenType}"); } public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options) { if (value.HasValue) writer.WriteStringValue(value.Value.ToString("yyyy-MM-dd HH:mm:ss")); else writer.WriteNullValue(); } } }