Files
qhmes/yy-admin-master/YY.Admin.Services/Service/Supplier/SupplierService.cs

686 lines
30 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.Supplier;
public class SupplierService : ISupplierService, ISingletonDependency
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly INetworkMonitor _networkMonitor;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private readonly object _cacheLock = new();
private readonly string _cacheFilePath;
private readonly string _pendingFilePath;
private List<MesXslSupplier> _localCache = new();
private const int MaxPendingRetries = 5;
// 断开期间被本地修改的条目(含版本锚点),重连后推送到后端
private readonly HashSet<string> _pendingLocalModifiedIds = new(StringComparer.OrdinalIgnoreCase);
// 断开期间被本地删除的条目
private readonly HashSet<string> _pendingLocalDeletedIds = new(StringComparer.OrdinalIgnoreCase);
// 版本锚点:最后一次从后端同步时该条目的 UpdateTime用于冲突检测
private readonly Dictionary<string, DateTime?> _anchors = new(StringComparer.OrdinalIgnoreCase);
// 每条待推送记录的重试次数,超过上限后放弃
private readonly Dictionary<string, int> _pendingRetryCount = new(StringComparer.OrdinalIgnoreCase);
private static readonly JsonSerializerOptions _jsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new NullableDateTimeJsonConverter() }
};
public SupplierService(
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-supplier-cache.json");
_pendingFilePath = Path.Combine(appDataDir, "mes-xsl-supplier-pending.json");
LoadCacheFromDisk();
LoadPendingFromDisk();
}
// ── 配置 ──────────────────────────────────────────────────────────────────
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<SupplierPageResult> PageAsync(
int pageNo, int pageSize,
string? supplierCode = null, string? supplierName = null,
string? supplierShortName = null, string? erpCode = null,
string? status = null, CancellationToken ct = default)
{
List<MesXslSupplier> source;
try
{
source = _networkMonitor.IsOnline
? await FetchRemoteListAsync(ct).ConfigureAwait(false)
: GetCacheSnapshot();
lock (_cacheLock)
{
MergeIntoCache(source);
SaveCacheToDiskUnsafe();
}
source = GetCacheSnapshot();
}
catch
{
source = GetCacheSnapshot();
}
IEnumerable<MesXslSupplier> q = source;
if (!string.IsNullOrWhiteSpace(supplierCode))
q = q.Where(x => (x.SupplierCode ?? "").Contains(supplierCode, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(supplierName))
q = q.Where(x => (x.SupplierName ?? "").Contains(supplierName, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(supplierShortName))
q = q.Where(x => (x.SupplierShortName ?? "").Contains(supplierShortName, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(erpCode))
q = q.Where(x => (x.ErpCode ?? "").Contains(erpCode, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(status))
q = q.Where(x => string.Equals(x.Status, status, StringComparison.OrdinalIgnoreCase));
var ordered = q.OrderByDescending(x => x.CreateTime ?? DateTime.MinValue).ToList();
var total = ordered.Count;
var records = ordered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList();
return new SupplierPageResult(records, total, pageNo, pageSize);
}
public async Task<MesXslSupplier?> GetByIdAsync(string id, CancellationToken ct = default)
{
// 有本地待同步改动时优先返回本地版本,避免编辑弹窗被后端旧版本覆盖
if (_networkMonitor.IsOnline && !_pendingLocalModifiedIds.Contains(id))
{
var url = $"{BaseUrl}/xslmes/mesXslSupplier/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<MesXslSupplier>(_jsonOpts);
}
}
return GetCacheSnapshot().FirstOrDefault(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase));
}
public Task<bool> AddAsync(MesXslSupplier supplier, CancellationToken ct = default) =>
PostAndRefreshAsync(
$"{BaseUrl}/xslmes/mesXslSupplier/anon/add?tenantId={DefaultTenantId}",
supplier, "add", supplier.Id, ct);
public Task<bool> EditAsync(MesXslSupplier supplier, CancellationToken ct = default) =>
PostAndRefreshAsync(
$"{BaseUrl}/xslmes/mesXslSupplier/anon/edit?tenantId={DefaultTenantId}",
supplier, "edit", supplier.Id, ct);
public async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
{
using var client = CreateClient();
var url = $"{BaseUrl}/xslmes/mesXslSupplier/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
if (IsDisconnectedResponse(resp))
{
var anchor = GetCacheSnapshot()
.FirstOrDefault(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
RemoveFromLocalCache(id);
MarkLocalDeleted(id, anchor);
_eventAggregator.GetEvent<SupplierChangedEvent>()
.Publish(new SupplierChangedPayload { Action = "delete", SupplierId = id });
return true;
}
var ok = resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
if (ok)
{
RemoveFromLocalCache(id);
ClearPending(id);
_eventAggregator.GetEvent<SupplierChangedEvent>()
.Publish(new SupplierChangedPayload { Action = "delete", SupplierId = id });
}
return ok;
}
public async Task<bool> UpdateStatusAsync(string id, string status, CancellationToken ct = default)
{
using var client = CreateClient();
var url = $"{BaseUrl}/xslmes/mesXslSupplier/anon/updateStatus?id={Uri.EscapeDataString(id)}&status={Uri.EscapeDataString(status)}&tenantId={DefaultTenantId}";
var resp = await client.PostAsync(url, null, ct).ConfigureAwait(false);
if (IsDisconnectedResponse(resp))
{
var anchor = GetCacheSnapshot()
.FirstOrDefault(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
UpdateLocalStatus(id, status);
MarkLocalModified(id, anchor);
_eventAggregator.GetEvent<SupplierChangedEvent>()
.Publish(new SupplierChangedPayload { Action = "status", SupplierId = id });
return true;
}
var ok = resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
if (ok)
{
UpdateLocalStatus(id, status);
ClearPending(id);
_eventAggregator.GetEvent<SupplierChangedEvent>()
.Publish(new SupplierChangedPayload { Action = "status", SupplierId = id });
}
return ok;
}
/// <summary>
/// 重连后将离线期间的本地改动推送到后端,并检测冲突。
/// 调用方应在此方法完成后再触发 UI 刷新,确保页面看到的是推送结果。
/// </summary>
public async Task<PushPendingResult> PushPendingOnReconnectAsync(CancellationToken ct = default)
{
int pushed = 0, conflicts = 0, newPushed = 0;
// ── 1. 推送离线期间修改的现有记录 ─────────────────────────────────────
foreach (var id in new List<string>(_pendingLocalModifiedIds))
{
try
{
var local = GetCacheSnapshot()
.FirstOrDefault(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase));
if (local == null) { ClearPending(id); continue; }
var remote = await FetchRemoteSingleAsync(id, ct);
if (remote != null && HasConflict(id, remote.UpdateTime))
{
// 后端也改了 → 后端版本获胜MES 多用户安全策略)
UpsertLocalCache(remote);
ClearPending(id);
conflicts++;
_logger.Warning($"[供应商同步] 冲突:{local.SupplierName}{id}),后端版本获胜");
}
else
{
// 仅本地改了 → 安全推送
var (ok, isVersionConflict) = await PushEditAsync(local, ct);
if (isVersionConflict)
{
var fresh = await FetchRemoteSingleAsync(id, ct);
if (fresh != null) UpsertLocalCache(fresh);
ClearPending(id);
conflicts++;
_logger.Warning($"[供应商同步] 服务端版本冲突:{local.SupplierName}{id}),后端版本获胜");
}
else if (ok)
{
ClearPending(id);
pushed++;
_logger.Information($"[供应商同步] 推送成功:{local.SupplierName}{id}");
}
else
{
_pendingRetryCount.TryGetValue(id, out var retries);
retries++;
if (retries >= MaxPendingRetries)
{
_logger.Warning($"[供应商同步] 推送超过最大重试次数({MaxPendingRetries}),放弃:{id}");
ClearPending(id);
}
else
{
_pendingRetryCount[id] = retries;
_logger.Warning($"[供应商同步] 推送失败,下次重连重试({retries}/{MaxPendingRetries}){id}");
}
}
}
}
catch (Exception ex)
{
_logger.Warning($"[供应商同步] 处理修改记录 {id} 时异常:{ex.Message}");
}
}
// ── 2. 推送离线期间删除的记录 ──────────────────────────────────────────
foreach (var id in new List<string>(_pendingLocalDeletedIds))
{
try
{
var remote = await FetchRemoteSingleAsync(id, ct);
if (remote == null) { ClearPending(id); continue; } // 后端已不存在,无需操作
if (HasConflict(id, remote.UpdateTime))
{
// 后端改动了但本地要删 → 保守策略:后端版本获胜,恢复到本地
UpsertLocalCache(remote);
ClearPending(id);
conflicts++;
_logger.Warning($"[供应商同步] 冲突:删除操作与后端改动冲突,已恢复记录 {id}");
}
else
{
if (await PushDeleteAsync(id, ct))
{
ClearPending(id);
pushed++;
}
else
{
_logger.Warning($"[供应商同步] 删除推送失败,下次重连重试:{id}");
}
}
}
catch (Exception ex)
{
_logger.Warning($"[供应商同步] 处理删除记录 {id} 时异常:{ex.Message}");
}
}
// ── 3. 推送离线期间新增的 local- 记录 ─────────────────────────────────
var localOnlyRecords = GetCacheSnapshot()
.Where(x => (x.Id ?? "").StartsWith("local-", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var record in localOnlyRecords)
{
try
{
if (await PushAddAsync(record, ct))
{
RemoveFromLocalCache(record.Id!);
newPushed++;
// 下次 FetchRemoteListAsync 时后端会返回真实 ID 版本
}
else
{
_logger.Warning($"[供应商同步] 新增推送失败,下次重连重试:{record.SupplierName}");
}
}
catch (Exception ex)
{
_logger.Warning($"[供应商同步] 推送新增 {record.SupplierName} 时异常:{ex.Message}");
}
}
return new PushPendingResult(pushed, conflicts, newPushed);
}
// ── 私有:写操作核心 ──────────────────────────────────────────────────────
private async Task<bool> PostAndRefreshAsync(
string url, MesXslSupplier supplier, string action, string? supplierId, CancellationToken ct)
{
if (!supplier.TenantId.HasValue || supplier.TenantId <= 0) supplier.TenantId = DefaultTenantId;
if (string.IsNullOrWhiteSpace(supplier.Status)) supplier.Status = "0";
var content = new StringContent(JsonSerializer.Serialize(supplier, _jsonOpts), Encoding.UTF8, "application/json");
using var client = CreateClient();
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
if (IsDisconnectedResponse(resp))
{
if (string.IsNullOrWhiteSpace(supplier.Id))
{
// 新增:分配 local- ID不进 pendingModifiedIds通过 local- 前缀识别)
supplier.Id = $"local-{Guid.NewGuid():N}";
supplier.UpdateTime = DateTime.Now;
UpsertLocalCache(supplier);
}
else
{
// 编辑已有记录:捕获版本锚点(修改前的后端 UpdateTime
var anchor = GetCacheSnapshot()
.FirstOrDefault(x => string.Equals(x.Id, supplier.Id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
supplier.UpdateTime = DateTime.Now;
UpsertLocalCache(supplier);
MarkLocalModified(supplier.Id, anchor);
}
_eventAggregator.GetEvent<SupplierChangedEvent>()
.Publish(new SupplierChangedPayload { Action = action, SupplierId = supplierId ?? supplier.Id });
return true;
}
var ok = resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
if (ok)
{
UpsertLocalCache(supplier);
ClearPending(supplierId ?? supplier.Id ?? "");
_eventAggregator.GetEvent<SupplierChangedEvent>()
.Publish(new SupplierChangedPayload { Action = action, SupplierId = supplierId ?? supplier.Id });
}
return ok;
}
// ── 私有:重连推送方法 ────────────────────────────────────────────────────
private async Task<(bool Ok, bool IsVersionConflict)> PushEditAsync(MesXslSupplier supplier, CancellationToken ct)
{
var content = new StringContent(
JsonSerializer.Serialize(supplier, _jsonOpts), Encoding.UTF8, "application/json");
using var client = CreateClient();
var resp = await client.PostAsync(
$"{BaseUrl}/xslmes/mesXslSupplier/anon/edit?tenantId={DefaultTenantId}", content, ct)
.ConfigureAwait(false);
if (!resp.IsSuccessStatusCode) return (false, false);
try
{
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
int code = 200;
if (doc.RootElement.TryGetProperty("code", out var codeEl)) code = codeEl.GetInt32();
if (code == 200) return (true, false);
if (doc.RootElement.TryGetProperty("message", out var msgEl))
{
var msg = msgEl.GetString() ?? "";
if (msg.Contains("已被他人修改")) return (false, true);
}
return (false, false);
}
catch { return (true, false); }
}
private async Task<bool> PushDeleteAsync(string id, CancellationToken ct)
{
using var client = CreateClient();
var url = $"{BaseUrl}/xslmes/mesXslSupplier/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
private async Task<bool> PushAddAsync(MesXslSupplier supplier, CancellationToken ct)
{
var payload = Clone(supplier);
payload.Id = null; // 让后端生成真实 ID
var content = new StringContent(
JsonSerializer.Serialize(payload, _jsonOpts), Encoding.UTF8, "application/json");
using var client = CreateClient();
var resp = await client.PostAsync(
$"{BaseUrl}/xslmes/mesXslSupplier/anon/add?tenantId={DefaultTenantId}", content, ct)
.ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
private async Task<MesXslSupplier?> FetchRemoteSingleAsync(string id, CancellationToken ct)
{
try
{
var url = $"{BaseUrl}/xslmes/mesXslSupplier/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
using var client = CreateClient();
var resp = await client.GetAsync(url, ct).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode) return null;
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<MesXslSupplier>(_jsonOpts);
return null;
}
catch { return null; }
}
// ── 私有:冲突检测 ────────────────────────────────────────────────────────
/// <summary>
/// 判断后端是否在断开期间修改了该记录:
/// 后端当前 UpdateTime 与本地锚点不同 → 两端都改了 → 冲突。
/// </summary>
private bool HasConflict(string id, DateTime? remoteUpdateTime)
{
if (!_anchors.TryGetValue(id, out var anchor)) return false;
return remoteUpdateTime != anchor;
}
private static bool IsDisconnectedResponse(HttpResponseMessage resp) =>
(int)resp.StatusCode == 499;
// ── 私有pending 状态管理 ────────────────────────────────────────────────
private void MarkLocalModified(string id, DateTime? anchor)
{
lock (_cacheLock)
{
_pendingLocalDeletedIds.Remove(id);
_pendingLocalModifiedIds.Add(id);
if (!_anchors.ContainsKey(id)) _anchors[id] = anchor; // 只记第一次(保留原始锚点)
SavePendingToDiskUnsafe();
}
}
private void MarkLocalDeleted(string id, DateTime? anchor)
{
lock (_cacheLock)
{
_pendingLocalModifiedIds.Remove(id);
_pendingLocalDeletedIds.Add(id);
if (!_anchors.ContainsKey(id)) _anchors[id] = anchor;
SavePendingToDiskUnsafe();
}
}
private void ClearPending(string id)
{
lock (_cacheLock)
{
_pendingLocalModifiedIds.Remove(id);
_pendingLocalDeletedIds.Remove(id);
_anchors.Remove(id);
_pendingRetryCount.Remove(id);
SavePendingToDiskUnsafe();
}
}
// ── 私有:缓存合并 ────────────────────────────────────────────────────────
/// <summary>
/// 后端全量数据与本地 pending 改动合并,保证本地未同步改动不被覆盖。
/// </summary>
private void MergeIntoCache(List<MesXslSupplier> backendList)
{
bool hasPending = _pendingLocalModifiedIds.Count > 0 || _pendingLocalDeletedIds.Count > 0;
if (!hasPending)
{
_localCache = backendList.Select(Clone).ToList();
return;
}
var localById = _localCache.ToDictionary(x => x.Id ?? "", StringComparer.OrdinalIgnoreCase);
var merged = new List<MesXslSupplier>(backendList.Count + 8);
foreach (var remote in backendList)
{
var id = remote.Id ?? "";
if (_pendingLocalDeletedIds.Contains(id)) continue; // 保持本地删除状态
if (_pendingLocalModifiedIds.Contains(id) && localById.TryGetValue(id, out var local))
merged.Add(Clone(local)); // 本地版本优先(尚未推送成功)
else
merged.Add(Clone(remote));
}
// 保留本地新增local- 前缀,尚未推送到后端)
foreach (var local in _localCache)
{
if ((local.Id ?? "").StartsWith("local-", StringComparison.OrdinalIgnoreCase))
merged.Add(Clone(local));
}
// 安全保留pending-modified 中后端未返回的条目(后端异常情况)
foreach (var modId in _pendingLocalModifiedIds)
{
if (!merged.Any(x => string.Equals(x.Id, modId, StringComparison.OrdinalIgnoreCase))
&& localById.TryGetValue(modId, out var orphan))
merged.Add(Clone(orphan));
}
_localCache = merged;
}
// ── 私有:磁盘持久化 ──────────────────────────────────────────────────────
private async Task<List<MesXslSupplier>> FetchRemoteListAsync(CancellationToken ct)
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["pageNo"] = "1";
query["pageSize"] = "10000";
query["tenantId"] = DefaultTenantId.ToString();
var url = $"{BaseUrl}/xslmes/mesXslSupplier/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);
return doc.RootElement.GetProperty("result").GetProperty("records")
.Deserialize<List<MesXslSupplier>>(_jsonOpts) ?? new();
}
private List<MesXslSupplier> GetCacheSnapshot()
{
lock (_cacheLock) return _localCache.Select(Clone).ToList();
}
private void LoadCacheFromDisk()
{
try
{
if (!File.Exists(_cacheFilePath)) return;
_localCache = JsonSerializer.Deserialize<List<MesXslSupplier>>(
File.ReadAllText(_cacheFilePath), _jsonOpts) ?? new();
}
catch { _localCache = new(); }
}
private void SaveCacheToDiskUnsafe() =>
File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts));
private void LoadPendingFromDisk()
{
try
{
if (!File.Exists(_pendingFilePath)) return;
var state = JsonSerializer.Deserialize<PendingState>(File.ReadAllText(_pendingFilePath)) ?? new();
foreach (var id in state.Modified) _pendingLocalModifiedIds.Add(id);
foreach (var id in state.Deleted) _pendingLocalDeletedIds.Add(id);
foreach (var (id, timeStr) in state.Anchors)
_anchors[id] = timeStr != null && DateTime.TryParse(timeStr, out var dt) ? dt : null;
}
catch { }
}
private void SavePendingToDiskUnsafe()
{
var state = new PendingState
{
Modified = _pendingLocalModifiedIds.ToList(),
Deleted = _pendingLocalDeletedIds.ToList(),
Anchors = _anchors.ToDictionary(
kv => kv.Key,
kv => kv.Value?.ToString("yyyy-MM-dd HH:mm:ss"))
};
File.WriteAllText(_pendingFilePath, JsonSerializer.Serialize(state));
}
private void UpsertLocalCache(MesXslSupplier supplier)
{
lock (_cacheLock)
{
if (string.IsNullOrWhiteSpace(supplier.Id))
{
_localCache.Insert(0, Clone(supplier));
}
else
{
var idx = _localCache.FindIndex(x =>
string.Equals(x.Id, supplier.Id, StringComparison.OrdinalIgnoreCase));
if (idx >= 0) _localCache[idx] = Clone(supplier);
else _localCache.Insert(0, Clone(supplier));
}
SaveCacheToDiskUnsafe();
}
}
private void RemoveFromLocalCache(string id)
{
lock (_cacheLock)
{
_localCache.RemoveAll(x => string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase));
SaveCacheToDiskUnsafe();
}
}
private void UpdateLocalStatus(string id, string status)
{
lock (_cacheLock)
{
var item = _localCache.FirstOrDefault(x =>
string.Equals(x.Id, id, StringComparison.OrdinalIgnoreCase));
if (item != null) { item.Status = status; SaveCacheToDiskUnsafe(); }
}
}
private static async Task<bool> IsSuccessResultAsync(HttpResponseMessage resp, CancellationToken ct)
{
try
{
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("code", out var code)) return code.GetInt32() == 200;
if (doc.RootElement.TryGetProperty("success", out var success)) return success.GetBoolean();
}
catch { }
return true;
}
private static MesXslSupplier Clone(MesXslSupplier x) => new()
{
Id = x.Id, SupplierCode = x.SupplierCode, SupplierName = x.SupplierName,
SupplierShortName = x.SupplierShortName, ErpCode = x.ErpCode, Remark = x.Remark,
Status = x.Status, TenantId = x.TenantId, CreateBy = x.CreateBy,
CreateTime = x.CreateTime, UpdateBy = x.UpdateBy, UpdateTime = x.UpdateTime,
SysOrgCode = x.SysOrgCode
};
private sealed class PendingState
{
public List<string> Modified { get; set; } = new();
public List<string> Deleted { get; set; } = new();
public Dictionary<string, string?> Anchors { get; set; } = new();
}
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> reader.TokenType == JsonTokenType.String
&& DateTime.TryParse(reader.GetString(), out var dt) ? dt : null;
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
=> writer.WriteStringValue(value?.ToString("yyyy-MM-dd HH:mm:ss"));
}
}