Files
qhmes/yy-admin-master/YY.Admin.Services/Service/MixerMaterialTareStrategy/MixerMaterialTareStrategyService.cs

713 lines
29 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 System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
using Prism.Events;
using YY.Admin.Core;
using YY.Admin.Core.Entity;
using YY.Admin.Core.Events;
using YY.Admin.Core.Services;
namespace YY.Admin.Services.Service.MixerMaterialTareStrategy;
public class MixerMaterialTareStrategyService : IMixerMaterialTareStrategyService, 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 _pendingOpsFilePath;
private readonly string _cacheFilePath;
private List<TareStrategyPendingOperation> _pendingOps = new();
private List<MesXslMixerMaterialTareStrategy> _localCache = new();
private static readonly JsonSerializerOptions _jsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new NullableDateTimeJsonConverter() }
};
public MixerMaterialTareStrategyService(
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);
_pendingOpsFilePath = Path.Combine(appDataDir, "mes-xsl-tare-strategy-pending-ops.json");
_cacheFilePath = Path.Combine(appDataDir, "mes-xsl-tare-strategy-cache.json");
LoadPendingOpsFromDisk();
LoadCacheFromDisk();
_logger.Information($"[密炼物料皮重策略同步] 服务初始化,缓存={_localCache.Count},待上传={_pendingOps.Count},在线={_networkMonitor.IsOnline}");
_networkMonitor.StatusChanged += OnNetworkStatusChanged;
if (_networkMonitor.IsOnline)
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
}
private const int MaxPendingRetries = 5;
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<MixerMaterialTareStrategyPageResult> PageAsync(
int pageNo, int pageSize,
string? mixerMaterialName = null, string? supplierName = null,
CancellationToken ct = default)
{
List<MesXslMixerMaterialTareStrategy>? source = null;
if (_networkMonitor.IsOnline)
{
try
{
source = await FetchRemoteListAsync(ct).ConfigureAwait(false);
lock (_cacheLock)
{
_localCache = source.Select(Clone).ToList();
SaveCacheToDiskUnsafe();
}
}
catch (Exception ex)
{
source = null;
_logger.Warning($"[密炼物料皮重策略列表] 远端拉取失败,回退缓存:{ex.Message}");
}
}
lock (_cacheLock)
{
source ??= _localCache.Select(Clone).ToList();
source = ApplyPendingOpsSnapshotUnsafe(source);
}
var filtered = ApplyFilters(source, mixerMaterialName, supplierName);
var total = filtered.Count;
var records = filtered.Skip(Math.Max(0, (pageNo - 1) * pageSize)).Take(pageSize).ToList();
return new MixerMaterialTareStrategyPageResult(records, total, pageNo, pageSize);
}
public async Task<IReadOnlyList<MesXslMixerMaterialTareStrategy>> GetAllForMatchAsync(CancellationToken ct = default)
{
var page = await PageAsync(1, 10000, null, null, ct).ConfigureAwait(false);
return page.Records;
}
public async Task<MesXslMixerMaterialTareStrategy?> GetByIdAsync(string id, CancellationToken ct = default)
{
if (_networkMonitor.IsOnline)
{
try
{
var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/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 null;
return resultEl.Deserialize<MesXslMixerMaterialTareStrategy>(_jsonOpts);
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略详情] 远端查询失败 id={id},回退缓存:{ex.Message}");
}
}
lock (_cacheLock)
{
return _localCache.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found
? Clone(found) : null;
}
}
public async Task<bool> AddAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default)
{
if (!strategy.TenantId.HasValue || strategy.TenantId.Value <= 0)
strategy.TenantId = DefaultTenantId;
var local = Clone(strategy);
if (string.IsNullOrWhiteSpace(local.Id))
local.Id = $"local-{Guid.NewGuid():N}";
if (_networkMonitor.IsOnline)
{
try
{
var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false);
if (ok) { UpsertLocalCache(local); return true; }
return false;
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略新增] 远端失败,转离线入队:{ex.Message}");
}
}
EnqueuePendingOperation(new TareStrategyPendingOperation
{
OpType = TareStrategyOperationType.Add,
TareStrategyId = local.Id,
Strategy = local
});
UpsertLocalCache(local);
return true;
}
public async Task<bool> EditAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct = default)
{
if (!strategy.TenantId.HasValue || strategy.TenantId.Value <= 0)
strategy.TenantId = DefaultTenantId;
var local = Clone(strategy);
if (_networkMonitor.IsOnline)
{
try
{
var (ok, _) = await RemoteEditAsync(local, ct).ConfigureAwait(false);
if (ok) { UpsertLocalCache(local); return true; }
return false;
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略编辑] 远端失败,转离线入队:{ex.Message}");
}
}
EnqueuePendingOperation(new TareStrategyPendingOperation
{
OpType = TareStrategyOperationType.Edit,
TareStrategyId = local.Id,
Strategy = local,
AnchorUpdateTime = local.UpdateTime
});
UpsertLocalCache(local);
return true;
}
public async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
{
if (_networkMonitor.IsOnline)
{
try
{
var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false);
if (ok) { RemoveFromLocalCache(id); return true; }
return false;
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略删除] 远端失败,转离线入队:{ex.Message}");
}
}
DateTime? anchor;
lock (_cacheLock)
{
anchor = _localCache.FirstOrDefault(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
}
EnqueuePendingOperation(new TareStrategyPendingOperation
{
OpType = TareStrategyOperationType.Delete,
TareStrategyId = id,
AnchorUpdateTime = anchor
});
RemoveFromLocalCache(id);
return true;
}
public async Task<List<MesXslUnit>> GetUnitsAsync(CancellationToken ct = default)
{
if (!_networkMonitor.IsOnline)
return new List<MesXslUnit>();
try
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["pageNo"] = "1";
query["pageSize"] = "1000";
query["tenantId"] = DefaultTenantId.ToString();
var url = $"{BaseUrl}/xslmes/mesXslUnit/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<MesXslUnit>>(_jsonOpts) ?? new();
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略] 单位列表拉取失败:{ex.Message}");
return new List<MesXslUnit>();
}
}
private async Task<List<MesXslMixerMaterialTareStrategy>> FetchRemoteListAsync(CancellationToken ct)
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["pageNo"] = "1";
query["pageSize"] = "10000";
query["tenantId"] = DefaultTenantId.ToString();
var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/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<MesXslMixerMaterialTareStrategy>>(_jsonOpts) ?? new();
}
private async Task<bool> RemoteAddAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/add?tenantId={DefaultTenantId}";
var payload = Clone(strategy);
if (IsLocalTempId(payload.Id)) payload.Id = null;
return await PostJsonAsync(url, payload, ct).ConfigureAwait(false);
}
private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslMixerMaterialTareStrategy strategy, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/edit?tenantId={DefaultTenantId}";
return await PostJsonCheckVersionAsync(url, strategy, ct).ConfigureAwait(false);
}
private async Task<bool> RemoteDeleteAsync(string id, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
using var client = CreateClient();
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
private async Task<bool> PostJsonAsync(string url, object body, CancellationToken ct)
{
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
using var client = CreateClient();
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
private async Task<(bool Ok, bool IsVersionConflict)> PostJsonCheckVersionAsync(string url, object body, CancellationToken ct)
{
var content = new StringContent(JsonSerializer.Serialize(body, _jsonOpts), Encoding.UTF8, "application/json");
using var client = CreateClient();
var resp = await client.PostAsync(url, 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 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();
return true;
}
catch { return true; }
}
private void OnNetworkStatusChanged(bool isOnline)
{
if (!isOnline) return;
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
}
private async Task SyncAfterReconnectAsync(CancellationToken ct)
{
var pushResult = await PushPendingOnReconnectAsync(ct).ConfigureAwait(false);
if (!_networkMonitor.IsOnline) return;
try
{
var remote = await FetchRemoteListAsync(ct).ConfigureAwait(false);
lock (_cacheLock)
{
_localCache = remote.Select(Clone).ToList();
SaveCacheToDiskUnsafe();
}
_eventAggregator.GetEvent<MixerMaterialTareStrategyChangedEvent>()
.Publish(new MixerMaterialTareStrategyChangedPayload { Action = "pull" });
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略重连] 全量回拉失败:{ex.Message}");
}
var hasActivity = pushResult.PushedCount > 0 || pushResult.ConflictCount > 0 || pushResult.NewRecordsPushed > 0;
if (hasActivity)
{
_eventAggregator.GetEvent<SyncConflictEvent>()
.Publish(new SyncConflictPayload
{
EntityName = "密炼物料皮重策略",
PushedCount = pushResult.PushedCount,
ConflictCount = pushResult.ConflictCount,
NewRecordsPushed = pushResult.NewRecordsPushed
});
}
}
private async Task<PushPendingResult> PushPendingOnReconnectAsync(CancellationToken ct)
{
if (!await _syncLock.WaitAsync(0, ct).ConfigureAwait(false))
return new PushPendingResult(0, 0, 0);
try
{
List<TareStrategyPendingOperation> snapshot;
lock (_cacheLock) { snapshot = _pendingOps.OrderBy(x => x.CreatedAt).ToList(); }
int pushed = 0, conflicts = 0, newPushed = 0;
foreach (var op in snapshot)
{
if (!_networkMonitor.IsOnline) break;
lock (_cacheLock)
{
if (!_pendingOps.Any(x => x.Id == op.Id)) continue;
}
var result = await ExecutePendingOpWithConflictAsync(op, ct).ConfigureAwait(false);
if (!result.Ok)
{
lock (_cacheLock)
{
op.RetryCount++;
if (op.RetryCount >= MaxPendingRetries)
{
_pendingOps.RemoveAll(x => x.Id == op.Id);
SavePendingOpsToDiskUnsafe();
continue;
}
SavePendingOpsToDiskUnsafe();
}
break;
}
if (result.IsConflict)
{
conflicts++;
if (!string.IsNullOrWhiteSpace(result.EntityId))
RemovePendingOpsByTareStrategyId(result.EntityId);
}
else if (op.OpType == TareStrategyOperationType.Add)
{
newPushed++;
lock (_cacheLock)
{
_pendingOps.RemoveAll(x => x.Id == op.Id);
SavePendingOpsToDiskUnsafe();
}
}
else
{
pushed++;
lock (_cacheLock)
{
_pendingOps.RemoveAll(x => x.Id == op.Id);
SavePendingOpsToDiskUnsafe();
}
}
}
return new PushPendingResult(pushed, conflicts, newPushed);
}
finally { _syncLock.Release(); }
}
private sealed record PushPendingResult(int PushedCount, int ConflictCount, int NewRecordsPushed);
private sealed record PendingReplayResult(bool Ok, bool IsConflict, string? EntityId);
private async Task<PendingReplayResult> ExecutePendingOpWithConflictAsync(TareStrategyPendingOperation op, CancellationToken ct)
{
try
{
return op.OpType switch
{
TareStrategyOperationType.Add => await ExecuteAddAsync(op, ct).ConfigureAwait(false),
TareStrategyOperationType.Edit => await ExecuteEditWithConflictAsync(op, ct).ConfigureAwait(false),
TareStrategyOperationType.Delete => await ExecuteDeleteWithConflictAsync(op, ct).ConfigureAwait(false),
_ => new PendingReplayResult(true, false, null)
};
}
catch (Exception ex)
{
_logger.Warning($"[密炼物料皮重策略回放] 执行失败 op={op.OpType}{ex.Message}");
return new PendingReplayResult(false, false, null);
}
}
private async Task<PendingReplayResult> ExecuteAddAsync(TareStrategyPendingOperation op, CancellationToken ct)
{
var ok = op.Strategy != null && await RemoteAddAsync(op.Strategy, ct).ConfigureAwait(false);
return ok
? new PendingReplayResult(true, false, op.TareStrategyId)
: new PendingReplayResult(false, false, null);
}
private async Task<PendingReplayResult> ExecuteEditWithConflictAsync(TareStrategyPendingOperation op, CancellationToken ct)
{
var id = op.Strategy?.Id;
if (string.IsNullOrWhiteSpace(id))
return new PendingReplayResult(false, false, null);
var remote = await FetchRemoteSingleAsync(id!, ct).ConfigureAwait(false);
if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
{
UpsertLocalCache(remote);
return new PendingReplayResult(true, true, id);
}
var (ok, isVersionConflict) = await RemoteEditAsync(op.Strategy!, ct).ConfigureAwait(false);
if (isVersionConflict)
{
var fresh = await FetchRemoteSingleAsync(id!, ct).ConfigureAwait(false);
if (fresh != null) UpsertLocalCache(fresh);
return new PendingReplayResult(true, true, id);
}
return ok
? new PendingReplayResult(true, false, id)
: new PendingReplayResult(false, false, null);
}
private async Task<PendingReplayResult> ExecuteDeleteWithConflictAsync(TareStrategyPendingOperation op, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(op.TareStrategyId))
return new PendingReplayResult(false, false, null);
var id = op.TareStrategyId!;
var remote = await FetchRemoteSingleAsync(id, ct).ConfigureAwait(false);
if (remote == null)
return new PendingReplayResult(true, false, id);
if (op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
{
UpsertLocalCache(remote);
return new PendingReplayResult(true, true, id);
}
var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false);
return ok
? new PendingReplayResult(true, false, id)
: new PendingReplayResult(false, false, null);
}
private void RemovePendingOpsByTareStrategyId(string tareStrategyId)
{
lock (_cacheLock)
{
_pendingOps.RemoveAll(x =>
(x.TareStrategyId != null && string.Equals(x.TareStrategyId, tareStrategyId, StringComparison.OrdinalIgnoreCase)) ||
(x.Strategy?.Id != null && string.Equals(x.Strategy.Id, tareStrategyId, StringComparison.OrdinalIgnoreCase)));
SavePendingOpsToDiskUnsafe();
}
}
private async Task<MesXslMixerMaterialTareStrategy?> FetchRemoteSingleAsync(string id, CancellationToken ct)
{
try
{
var url = $"{BaseUrl}/xslmes/mesXslMixerMaterialTareStrategy/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<MesXslMixerMaterialTareStrategy>(_jsonOpts);
return null;
}
catch { return null; }
}
private static List<MesXslMixerMaterialTareStrategy> ApplyFilters(
List<MesXslMixerMaterialTareStrategy> source,
string? mixerMaterialName, string? supplierName)
{
IEnumerable<MesXslMixerMaterialTareStrategy> q = source;
if (!string.IsNullOrWhiteSpace(mixerMaterialName))
q = q.Where(c => (c.MixerMaterialName ?? "").Contains(mixerMaterialName, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(supplierName))
q = q.Where(c => (c.SupplierName ?? "").Contains(supplierName, StringComparison.OrdinalIgnoreCase));
return q.OrderByDescending(c => c.EffectiveStartDate ?? DateTime.MinValue).ToList();
}
private List<MesXslMixerMaterialTareStrategy> ApplyPendingOpsSnapshotUnsafe(List<MesXslMixerMaterialTareStrategy> source)
{
var map = source.Where(c => !string.IsNullOrWhiteSpace(c.Id))
.ToDictionary(c => c.Id!, Clone, StringComparer.OrdinalIgnoreCase);
foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt))
{
switch (op.OpType)
{
case TareStrategyOperationType.Add:
case TareStrategyOperationType.Edit:
if (op.Strategy?.Id != null) map[op.Strategy.Id] = Clone(op.Strategy);
break;
case TareStrategyOperationType.Delete:
if (op.TareStrategyId != null) map.Remove(op.TareStrategyId);
break;
}
}
return map.Values.ToList();
}
private void EnqueuePendingOperation(TareStrategyPendingOperation op)
{
lock (_cacheLock) { _pendingOps.Add(op); SavePendingOpsToDiskUnsafe(); }
}
private void UpsertLocalCache(MesXslMixerMaterialTareStrategy strategy)
{
lock (_cacheLock)
{
var idx = _localCache.FindIndex(c => string.Equals(c.Id, strategy.Id, StringComparison.OrdinalIgnoreCase));
if (idx >= 0) _localCache[idx] = Clone(strategy);
else _localCache.Insert(0, Clone(strategy));
SaveCacheToDiskUnsafe();
}
}
private void RemoveFromLocalCache(string id)
{
lock (_cacheLock)
{
_localCache.RemoveAll(c => string.Equals(c.Id, id, StringComparison.OrdinalIgnoreCase));
SaveCacheToDiskUnsafe();
}
}
private void LoadPendingOpsFromDisk()
{
try
{
if (!File.Exists(_pendingOpsFilePath)) return;
_pendingOps = JsonSerializer.Deserialize<List<TareStrategyPendingOperation>>(
File.ReadAllText(_pendingOpsFilePath), _jsonOpts) ?? new();
}
catch { _pendingOps = new(); }
}
private void LoadCacheFromDisk()
{
try
{
if (!File.Exists(_cacheFilePath)) return;
_localCache = JsonSerializer.Deserialize<List<MesXslMixerMaterialTareStrategy>>(
File.ReadAllText(_cacheFilePath), _jsonOpts) ?? new();
}
catch { _localCache = new(); }
}
private void SavePendingOpsToDiskUnsafe() =>
File.WriteAllText(_pendingOpsFilePath, JsonSerializer.Serialize(_pendingOps, _jsonOpts));
private void SaveCacheToDiskUnsafe() =>
File.WriteAllText(_cacheFilePath, JsonSerializer.Serialize(_localCache, _jsonOpts));
private static bool IsLocalTempId(string? id) =>
!string.IsNullOrWhiteSpace(id) && id.StartsWith("local-", StringComparison.OrdinalIgnoreCase);
private static MesXslMixerMaterialTareStrategy Clone(MesXslMixerMaterialTareStrategy c) => new()
{
Id = c.Id,
TenantId = c.TenantId,
MixerMaterialId = c.MixerMaterialId,
MixerMaterialName = c.MixerMaterialName,
SupplierId = c.SupplierId,
SupplierName = c.SupplierName,
MaterialSpec = c.MaterialSpec,
TareWeight = c.TareWeight,
PalletWeight = c.PalletWeight,
UnitId = c.UnitId,
UnitName = c.UnitName,
EffectiveStartDate = c.EffectiveStartDate,
EffectiveEndDate = c.EffectiveEndDate,
MaintainBy = c.MaintainBy,
CreateBy = c.CreateBy,
CreateTime = c.CreateTime,
UpdateBy = c.UpdateBy,
UpdateTime = c.UpdateTime,
SysOrgCode = c.SysOrgCode,
DelFlag = c.DelFlag
};
private sealed class TareStrategyPendingOperation
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public TareStrategyOperationType OpType { get; set; }
public string? TareStrategyId { get; set; }
public MesXslMixerMaterialTareStrategy? Strategy { get; set; }
public DateTime? AnchorUpdateTime { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public int RetryCount { get; set; }
}
private enum TareStrategyOperationType { Add = 1, Edit = 2, Delete = 3 }
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
{
private static readonly string[] Formats =
[
"yyyy-MM-dd", "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"
];
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();
}
}
}