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

686 lines
30 KiB
C#
Raw Normal View History

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"));
}
}