using Microsoft.Extensions.Configuration; using System.Net.Http; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; using YY.Admin.Core; using YY.Admin.Core.Entity; using YY.Admin.Core.Services; namespace YY.Admin.Services.Service.Warehouse; /// /// 仓库数据只读服务:启动与重连时后台刷新远端写入缓存;GetAllAsync 优先读缓存,避免库区等业务每次打开都全量拉仓库。 /// public class WarehouseService : IWarehouseService, ISingletonDependency { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly INetworkMonitor _networkMonitor; private readonly ILoggerService _logger; private readonly object _cacheLock = new(); private readonly SemaphoreSlim _emptyLoadGate = new(1, 1); private readonly string _cacheFilePath; private List _localCache = new(); private static readonly JsonSerializerOptions _jsonOpts = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; public WarehouseService( IHttpClientFactory httpClientFactory, IConfiguration configuration, INetworkMonitor networkMonitor, ILoggerService logger) { _httpClientFactory = httpClientFactory; _configuration = configuration; _networkMonitor = networkMonitor; _logger = logger; var appDataDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "YY.Admin", "sync-cache"); Directory.CreateDirectory(appDataDir); _cacheFilePath = Path.Combine(appDataDir, "warehouse-cache.json"); LoadCacheFromDisk(); _logger.Information($"[仓库数据] 服务初始化,本地缓存={_localCache.Count} 条"); if (_networkMonitor.IsOnline) _ = Task.Run(() => RefreshFromRemoteAsync(CancellationToken.None)); _networkMonitor.StatusChanged += isOnline => { if (isOnline) _ = Task.Run(() => RefreshFromRemoteAsync(CancellationToken.None)); }; } private string BaseUrl => (_configuration.GetValue("JeecgIntegration:BaseUrl") ?? "http://localhost:8080/jeecg-boot").TrimEnd('/'); private int DefaultTenantId => (int?)_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1002; public async Task> GetAllAsync(CancellationToken ct = default) { lock (_cacheLock) { if (_localCache.Count > 0 || !_networkMonitor.IsOnline) return _localCache.ToList(); } // 缓存为空且在线:仅此时阻塞拉远端(首次安装或清缓存后) await _emptyLoadGate.WaitAsync(ct).ConfigureAwait(false); try { lock (_cacheLock) { if (_localCache.Count > 0) return _localCache.ToList(); } if (!_networkMonitor.IsOnline) { lock (_cacheLock) return _localCache.ToList(); } try { await RefreshFromRemoteAsync(ct).ConfigureAwait(false); } catch { /* 登录前拉取失败等场景,仍返回当前缓存(可能仍为空) */ } } finally { _emptyLoadGate.Release(); } lock (_cacheLock) return _localCache.ToList(); } private async Task RefreshFromRemoteAsync(CancellationToken ct) { try { var result = new List(); int pageNo = 1; const int pageSize = 500; while (true) { var url = $"{BaseUrl}/xslmes/mesXslWarehouse/anon/list?pageNo={pageNo}&pageSize={pageSize}&tenantId={DefaultTenantId}"; using var client = _httpClientFactory.CreateClient("JeecgApi"); 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); if (!doc.RootElement.TryGetProperty("result", out var resultEl)) break; if (resultEl.TryGetProperty("records", out var recordsEl)) { var page = recordsEl.Deserialize>(_jsonOpts); if (page != null) result.AddRange(page); } long total = 0; if (resultEl.TryGetProperty("total", out var totalEl)) total = totalEl.GetInt64(); if (result.Count >= total || (resultEl.TryGetProperty("records", out var r2) && r2.GetArrayLength() < pageSize)) break; pageNo++; } lock (_cacheLock) { _localCache = result; SaveCacheToDiskUnsafe(); } _logger.Information($"[仓库数据] 远端刷新成功,共 {result.Count} 条"); } catch (Exception ex) { _logger.Warning($"[仓库数据] 远端刷新失败,使用缓存: {ex.Message}"); } } private void LoadCacheFromDisk() { try { if (!File.Exists(_cacheFilePath)) return; var json = File.ReadAllText(_cacheFilePath); _localCache = JsonSerializer.Deserialize>(json, _jsonOpts) ?? new(); } catch { _localCache = new(); } } private void SaveCacheToDiskUnsafe() { try { File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts)); } catch { } } }