Files
qhmes/yy-admin-master/YY.Admin.Services/Service/Vehicle/VehicleService.cs

985 lines
38 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.Net.Http;
using System.IO;
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.Vehicle;
public class VehicleService : IVehicleService, 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<VehiclePendingOperation> _pendingOps = new();
private List<MesXslVehicle> _localCache = new();
private static readonly JsonSerializerOptions _jsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new NullableDateTimeJsonConverter()
}
};
public VehicleService(
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, "vehicle-pending-ops.json");
_cacheFilePath = Path.Combine(appDataDir, "vehicle-cache.json");
LoadPendingOpsFromDisk();
LoadCacheFromDisk();
_logger.Information($"[车辆同步] 服务初始化完成,缓存目录={appDataDir}, 本地缓存={_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()
{
var client = _httpClientFactory.CreateClient("JeecgApi");
return client;
}
public async Task<VehiclePageResult> PageAsync(int pageNo, int pageSize, string? plateNumber = null, string? vehicleBelong = null, string? status = null, CancellationToken ct = default)
{
List<MesXslVehicle>? source = null;
_logger.Information($"[车辆列表] 请求分页 pageNo={pageNo}, pageSize={pageSize}, plate={plateNumber}, belong={vehicleBelong}, status={status}, online={_networkMonitor.IsOnline}");
if (_networkMonitor.IsOnline)
{
try
{
source = await FetchRemoteListAsync(ct).ConfigureAwait(false);
lock (_cacheLock)
{
_localCache = source.Select(CloneVehicle).ToList();
SaveCacheToDiskUnsafe();
}
_logger.Information($"[车辆列表] 远端拉取成功,记录数={source.Count},已刷新本地缓存");
}
catch (Exception ex)
{
source = null;
_logger.Warning($"[车辆列表] 远端拉取失败,回退本地缓存:{ex.Message}");
}
}
lock (_cacheLock)
{
source ??= _localCache.Select(CloneVehicle).ToList();
source = ApplyPendingOpsSnapshotUnsafe(source);
}
var filtered = ApplyFilters(source, plateNumber, vehicleBelong, status);
var total = filtered.Count;
var pageRecords = filtered
.Skip(Math.Max(0, (pageNo - 1) * pageSize))
.Take(pageSize)
.ToList();
_logger.Information($"[车辆列表] 返回记录 total={total}, pageRecords={pageRecords.Count}, pending={_pendingOps.Count}");
return new VehiclePageResult(pageRecords, total, pageNo, pageSize);
}
public async Task<MesXslVehicle?> GetByIdAsync(string id, CancellationToken ct = default)
{
_logger.Information($"[车辆详情] 查询 id={id}, online={_networkMonitor.IsOnline}");
if (_networkMonitor.IsOnline)
{
try
{
var url = $"{BaseUrl}/xslmes/mesXslVehicle/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;
var result = resultEl.Deserialize<MesXslVehicle>(_jsonOpts);
_logger.Information($"[车辆详情] 远端查询成功 id={id}, found={result != null}");
return result;
}
catch (Exception ex)
{
_logger.Warning($"[车辆详情] 远端查询异常,回退本地缓存 id={id}, err={ex.Message}");
}
}
lock (_cacheLock)
{
return _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase)) is { } found
? CloneVehicle(found)
: null;
}
}
public async Task<bool> AddAsync(MesXslVehicle vehicle, CancellationToken ct = default)
{
if (!vehicle.TenantId.HasValue || vehicle.TenantId.Value <= 0)
{
vehicle.TenantId = DefaultTenantId;
}
var local = CloneVehicle(vehicle);
if (string.IsNullOrWhiteSpace(local.Id))
{
local.Id = $"local-{Guid.NewGuid():N}";
}
if (_networkMonitor.IsOnline)
{
try
{
_logger.Information($"[车辆新增] 尝试远端新增 id={local.Id}, plate={local.PlateNumber}");
var ok = await RemoteAddAsync(local, ct).ConfigureAwait(false);
if (ok)
{
UpsertLocalCache(local);
_logger.Information($"[车辆新增] 远端新增成功 id={local.Id}");
return true;
}
_logger.Warning($"[车辆新增] 远端新增返回失败 id={local.Id}");
return false;
}
catch (Exception ex)
{
_logger.Warning($"[车辆新增] 远端新增异常,转离线入队 id={local.Id}, err={ex.Message}");
}
}
EnqueuePendingOperation(new VehiclePendingOperation
{
OpType = VehicleOperationType.Add,
VehicleId = local.Id,
Vehicle = local,
CreatedAt = DateTime.UtcNow
});
UpsertLocalCache(local);
_logger.Information($"[车辆新增] 已离线入队 id={local.Id}, pending={_pendingOps.Count}");
return true;
}
public async Task<bool> EditAsync(MesXslVehicle vehicle, CancellationToken ct = default)
{
if (!vehicle.TenantId.HasValue || vehicle.TenantId.Value <= 0)
{
vehicle.TenantId = DefaultTenantId;
}
var local = CloneVehicle(vehicle);
if (_networkMonitor.IsOnline)
{
try
{
_logger.Information($"[车辆修改] 尝试远端修改 id={local.Id}, plate={local.PlateNumber}");
var (ok, _) = await RemoteEditAsync(local, ct).ConfigureAwait(false);
if (ok)
{
UpsertLocalCache(local);
_logger.Information($"[车辆修改] 远端修改成功 id={local.Id}");
return true;
}
_logger.Warning($"[车辆修改] 远端修改返回失败 id={local.Id}");
return false;
}
catch (Exception ex)
{
_logger.Warning($"[车辆修改] 远端修改异常,转离线入队 id={local.Id}, err={ex.Message}");
}
}
EnqueuePendingOperation(new VehiclePendingOperation
{
OpType = VehicleOperationType.Edit,
VehicleId = local.Id,
Vehicle = local,
AnchorUpdateTime = local.UpdateTime,
CreatedAt = DateTime.UtcNow
});
UpsertLocalCache(local);
_logger.Information($"[车辆修改] 已离线入队 id={local.Id}, pending={_pendingOps.Count}");
return true;
}
public async Task<bool> DeleteAsync(string id, CancellationToken ct = default)
{
if (_networkMonitor.IsOnline)
{
try
{
_logger.Information($"[车辆删除] 尝试远端删除 id={id}");
var ok = await RemoteDeleteAsync(id, ct).ConfigureAwait(false);
if (ok)
{
RemoveFromLocalCache(id);
_logger.Information($"[车辆删除] 远端删除成功 id={id}");
return true;
}
_logger.Warning($"[车辆删除] 远端删除返回失败 id={id}");
return false;
}
catch (Exception ex)
{
_logger.Warning($"[车辆删除] 远端删除异常,转离线入队 id={id}, err={ex.Message}");
}
}
DateTime? anchor;
lock (_cacheLock)
{
anchor = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
}
EnqueuePendingOperation(new VehiclePendingOperation
{
OpType = VehicleOperationType.Delete,
VehicleId = id,
AnchorUpdateTime = anchor,
CreatedAt = DateTime.UtcNow
});
RemoveFromLocalCache(id);
_logger.Information($"[车辆删除] 已离线入队 id={id}, pending={_pendingOps.Count}");
return true;
}
public async Task<bool> DeleteBatchAsync(string ids, CancellationToken ct = default)
{
var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var allSuccess = true;
foreach (var id in idList)
{
var ok = await DeleteAsync(id, ct).ConfigureAwait(false);
allSuccess &= ok;
}
return allSuccess;
}
public async Task<bool> UpdateStatusAsync(string id, string status, CancellationToken ct = default)
{
if (_networkMonitor.IsOnline)
{
try
{
_logger.Information($"[车辆状态] 尝试远端更新 id={id}, status={status}");
var ok = await RemoteUpdateStatusAsync(id, status, ct).ConfigureAwait(false);
if (ok)
{
UpdateLocalStatus(id, status);
_logger.Information($"[车辆状态] 远端更新成功 id={id}, status={status}");
return true;
}
_logger.Warning($"[车辆状态] 远端更新返回失败 id={id}, status={status}");
return false;
}
catch (Exception ex)
{
_logger.Warning($"[车辆状态] 远端更新异常,转离线入队 id={id}, status={status}, err={ex.Message}");
}
}
DateTime? anchor;
lock (_cacheLock)
{
anchor = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase))?.UpdateTime;
}
EnqueuePendingOperation(new VehiclePendingOperation
{
OpType = VehicleOperationType.UpdateStatus,
VehicleId = id,
Status = status,
AnchorUpdateTime = anchor,
CreatedAt = DateTime.UtcNow
});
UpdateLocalStatus(id, status);
_logger.Information($"[车辆状态] 已离线入队 id={id}, status={status}, pending={_pendingOps.Count}");
return true;
}
private async Task<List<MesXslVehicle>> FetchRemoteListAsync(CancellationToken ct)
{
var query = HttpUtility.ParseQueryString(string.Empty);
query["pageNo"] = "1";
query["pageSize"] = "10000";
query["tenantId"] = DefaultTenantId.ToString();
var url = $"{BaseUrl}/xslmes/mesXslVehicle/anon/list?{query}";
using var client = CreateClient();
_logger.Information($"[车辆远端] GET {url}");
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 result = doc.RootElement.GetProperty("result");
var records = result.GetProperty("records").Deserialize<List<MesXslVehicle>>(_jsonOpts) ?? new();
_logger.Information($"[车辆远端] 列表拉取成功 count={records.Count}");
return records;
}
private async Task<bool> RemoteAddAsync(MesXslVehicle vehicle, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslVehicle/anon/add?tenantId={DefaultTenantId}";
var payload = CloneVehicle(vehicle);
// 离线本地临时ID不能直接入后端主键需置空让Jeecg自动生成雪花ID
if (IsLocalTempId(payload.Id))
{
_logger.Information($"[车辆远端] 新增检测到本地临时ID自动清空 id={payload.Id}");
payload.Id = null;
}
return await PostJsonAsync(url, payload, ct).ConfigureAwait(false);
}
private async Task<(bool Ok, bool IsVersionConflict)> RemoteEditAsync(MesXslVehicle vehicle, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslVehicle/anon/edit?tenantId={DefaultTenantId}";
return await PostJsonCheckVersionAsync(url, vehicle, ct).ConfigureAwait(false);
}
private async Task<bool> RemoteDeleteAsync(string id, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslVehicle/anon/delete?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
using var client = CreateClient();
_logger.Information($"[车辆远端] DELETE {url}");
var resp = await client.DeleteAsync(url, ct).ConfigureAwait(false);
return resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
}
private async Task<bool> RemoteUpdateStatusAsync(string id, string status, CancellationToken ct)
{
var url = $"{BaseUrl}/xslmes/mesXslVehicle/anon/updateStatus?id={Uri.EscapeDataString(id)}&status={Uri.EscapeDataString(status)}&tenantId={DefaultTenantId}";
using var client = CreateClient();
_logger.Information($"[车辆远端] POST {url}");
var resp = await client.PostAsync(url, null, 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();
_logger.Information($"[车辆远端] POST {url}, bodyType={body.GetType().Name}");
var resp = await client.PostAsync(url, content, ct).ConfigureAwait(false);
var ok = resp.IsSuccessStatusCode && await IsSuccessResultAsync(resp, ct).ConfigureAwait(false);
_logger.Information($"[车辆远端] POST完成 url={url}, status={(int)resp.StatusCode}, ok={ok}");
return ok;
}
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();
_logger.Information($"[车辆远端] POST {url}, bodyType={body.GetType().Name}");
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)
{
_logger.Information($"[车辆网络] 状态变化 online={isOnline}");
if (!isOnline) return;
_ = Task.Run(() => SyncAfterReconnectAsync(CancellationToken.None));
}
private async Task SyncAfterReconnectAsync(CancellationToken cancellationToken)
{
_logger.Information("[车辆重连] 开始执行重连同步");
var pushResult = await PushPendingOnReconnectAsync(cancellationToken).ConfigureAwait(false);
if (!_networkMonitor.IsOnline)
{
return;
}
try
{
var remote = await FetchRemoteListAsync(cancellationToken).ConfigureAwait(false);
lock (_cacheLock)
{
_localCache = remote.Select(CloneVehicle).ToList();
SaveCacheToDiskUnsafe();
}
// 拉取成功后主动通知页面刷新,避免用户手动点查询
_eventAggregator.GetEvent<VehicleChangedEvent>().Publish(new VehicleChangedPayload
{
Action = "pull",
VehicleId = null
});
_logger.Information($"[车辆重连] 全量回拉成功 count={remote.Count},已发布刷新事件");
}
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 sealed record PendingReplayResult(bool Ok, bool IsConflict, string? EntityId);
private async Task<PushPendingResult> PushPendingOnReconnectAsync(CancellationToken cancellationToken)
{
if (!await _syncLock.WaitAsync(0, cancellationToken).ConfigureAwait(false))
{
_logger.Information("[车辆回放] 已有回放任务在执行,本次跳过");
return new PushPendingResult(0, 0, 0);
}
try
{
List<VehiclePendingOperation> snapshot;
lock (_cacheLock)
{
snapshot = _pendingOps.OrderBy(x => x.CreatedAt).ToList();
}
_logger.Information($"[车辆推送] 开始推送 pending={snapshot.Count}");
int pushed = 0, conflicts = 0, newPushed = 0;
foreach (var op in snapshot)
{
if (!_networkMonitor.IsOnline)
{
break;
}
// 如果该条 pending 在上一轮冲突中已被清理,则跳过
lock (_cacheLock)
{
if (!_pendingOps.Any(x => x.Id == op.Id))
continue;
}
var result = await ExecutePendingOperationWithConflictAsync(op, cancellationToken).ConfigureAwait(false);
if (!result.Ok)
{
lock (_cacheLock)
{
op.RetryCount++;
if (op.RetryCount >= MaxPendingRetries)
{
_logger.Warning($"[车辆推送] op={op.OpType} 超过最大重试次数({MaxPendingRetries}),放弃 vehicleId={op.VehicleId}");
_pendingOps.RemoveAll(x => x.Id == op.Id);
SavePendingOpsToDiskUnsafe();
continue;
}
SavePendingOpsToDiskUnsafe();
}
_logger.Warning($"[车辆推送] 推送中断 op={op.OpType}, vehicleId={op.VehicleId}, retry={op.RetryCount}");
break;
}
if (result.IsConflict)
{
conflicts++;
if (!string.IsNullOrWhiteSpace(result.EntityId))
RemovePendingOpsByVehicleId(result.EntityId!);
continue;
}
lock (_cacheLock)
{
if (op.OpType == VehicleOperationType.Add)
newPushed++;
else
pushed++;
_pendingOps.RemoveAll(x => x.Id == op.Id);
SavePendingOpsToDiskUnsafe();
}
_logger.Information($"[车辆推送] 推送成功 op={op.OpType}, vehicleId={op.VehicleId}, remain={_pendingOps.Count}");
}
return new PushPendingResult(pushed, conflicts, newPushed);
}
finally
{
_syncLock.Release();
}
}
private async Task<PendingReplayResult> ExecutePendingOperationWithConflictAsync(VehiclePendingOperation op, CancellationToken cancellationToken)
{
try
{
_logger.Information($"[车辆推送] 执行 op={op.OpType}, vehicleId={op.VehicleId}");
switch (op.OpType)
{
case VehicleOperationType.Add:
{
var ok = op.Vehicle != null && await RemoteAddAsync(op.Vehicle, cancellationToken).ConfigureAwait(false);
return ok
? new PendingReplayResult(true, false, op.VehicleId)
: new PendingReplayResult(false, false, null);
}
case VehicleOperationType.Edit:
{
if (op.Vehicle == null || string.IsNullOrWhiteSpace(op.Vehicle.Id))
return new PendingReplayResult(false, false, null);
var id = op.Vehicle.Id;
var remote = await FetchRemoteSingleAsync(id, cancellationToken).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.Vehicle, cancellationToken).ConfigureAwait(false);
if (isVersionConflict)
{
var fresh = await FetchRemoteSingleAsync(id, cancellationToken).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);
}
case VehicleOperationType.Delete:
{
if (string.IsNullOrWhiteSpace(op.VehicleId))
return new PendingReplayResult(false, false, null);
var id = op.VehicleId!;
var remote = await FetchRemoteSingleAsync(id, cancellationToken).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, cancellationToken).ConfigureAwait(false);
return ok
? new PendingReplayResult(true, false, id)
: new PendingReplayResult(false, false, null);
}
case VehicleOperationType.UpdateStatus:
{
if (string.IsNullOrWhiteSpace(op.VehicleId) || string.IsNullOrWhiteSpace(op.Status))
return new PendingReplayResult(false, false, null);
var id = op.VehicleId!;
var remote = await FetchRemoteSingleAsync(id, cancellationToken).ConfigureAwait(false);
if (remote != null && op.AnchorUpdateTime != null && remote.UpdateTime != op.AnchorUpdateTime)
{
UpsertLocalCache(remote);
return new PendingReplayResult(true, true, id);
}
var ok = await RemoteUpdateStatusAsync(id, op.Status!, cancellationToken).ConfigureAwait(false);
return ok
? new PendingReplayResult(true, false, id)
: new PendingReplayResult(false, false, null);
}
default:
return new PendingReplayResult(true, false, null);
}
}
catch (Exception ex)
{
_logger.Warning($"[车辆推送] 执行异常 op={op.OpType}, vehicleId={op.VehicleId}, err={ex.Message}");
return new PendingReplayResult(false, false, null);
}
}
private void RemovePendingOpsByVehicleId(string vehicleId)
{
lock (_cacheLock)
{
_pendingOps.RemoveAll(x =>
(!string.IsNullOrWhiteSpace(x.VehicleId) &&
string.Equals(x.VehicleId, vehicleId, StringComparison.OrdinalIgnoreCase)) ||
(x.Vehicle?.Id != null && string.Equals(x.Vehicle.Id, vehicleId, StringComparison.OrdinalIgnoreCase)));
SavePendingOpsToDiskUnsafe();
}
}
private async Task<MesXslVehicle?> FetchRemoteSingleAsync(string id, CancellationToken cancellationToken)
{
try
{
var url = $"{BaseUrl}/xslmes/mesXslVehicle/anon/queryById?id={Uri.EscapeDataString(id)}&tenantId={DefaultTenantId}";
using var client = CreateClient();
var resp = await client.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (!resp.IsSuccessStatusCode) return null;
var json = await resp.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("result", out var resultEl))
return resultEl.Deserialize<MesXslVehicle>(_jsonOpts);
return null;
}
catch
{
return null;
}
}
private static List<MesXslVehicle> ApplyFilters(
List<MesXslVehicle> source,
string? plateNumber,
string? vehicleBelong,
string? status)
{
IEnumerable<MesXslVehicle> query = source;
if (!string.IsNullOrWhiteSpace(plateNumber))
{
query = query.Where(v => (v.PlateNumber ?? string.Empty).Contains(plateNumber, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(vehicleBelong))
{
query = query.Where(v => string.Equals(v.VehicleBelong, vehicleBelong, StringComparison.OrdinalIgnoreCase));
}
if (!string.IsNullOrWhiteSpace(status))
{
query = query.Where(v => string.Equals(v.Status, status, StringComparison.OrdinalIgnoreCase));
}
return query.OrderByDescending(v => v.CreateTime ?? DateTime.MinValue).ToList();
}
private List<MesXslVehicle> ApplyPendingOpsSnapshotUnsafe(List<MesXslVehicle> source)
{
var map = source
.Where(v => !string.IsNullOrWhiteSpace(v.Id))
.ToDictionary(v => v.Id!, CloneVehicle, StringComparer.OrdinalIgnoreCase);
foreach (var op in _pendingOps.OrderBy(x => x.CreatedAt))
{
switch (op.OpType)
{
case VehicleOperationType.Add:
case VehicleOperationType.Edit:
if (op.Vehicle != null && !string.IsNullOrWhiteSpace(op.Vehicle.Id))
{
map[op.Vehicle.Id] = CloneVehicle(op.Vehicle);
}
break;
case VehicleOperationType.Delete:
if (!string.IsNullOrWhiteSpace(op.VehicleId))
{
map.Remove(op.VehicleId);
}
break;
case VehicleOperationType.UpdateStatus:
if (!string.IsNullOrWhiteSpace(op.VehicleId)
&& !string.IsNullOrWhiteSpace(op.Status)
&& map.TryGetValue(op.VehicleId, out var v))
{
v.Status = op.Status;
}
break;
}
}
return map.Values.ToList();
}
private void EnqueuePendingOperation(VehiclePendingOperation op)
{
lock (_cacheLock)
{
_pendingOps.Add(op);
SavePendingOpsToDiskUnsafe();
_logger.Information($"[车辆入队] op={op.OpType}, vehicleId={op.VehicleId}, pending={_pendingOps.Count}");
}
}
private void UpsertLocalCache(MesXslVehicle vehicle)
{
lock (_cacheLock)
{
var idx = _localCache.FindIndex(v => string.Equals(v.Id, vehicle.Id, StringComparison.OrdinalIgnoreCase));
if (idx >= 0)
{
_localCache[idx] = CloneVehicle(vehicle);
}
else
{
_localCache.Insert(0, CloneVehicle(vehicle));
}
SaveCacheToDiskUnsafe();
}
}
private void RemoveFromLocalCache(string id)
{
lock (_cacheLock)
{
_localCache.RemoveAll(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase));
SaveCacheToDiskUnsafe();
}
}
private void UpdateLocalStatus(string id, string status)
{
lock (_cacheLock)
{
var item = _localCache.FirstOrDefault(v => string.Equals(v.Id, id, StringComparison.OrdinalIgnoreCase));
if (item != null)
{
item.Status = status;
SaveCacheToDiskUnsafe();
}
}
}
private void LoadPendingOpsFromDisk()
{
try
{
if (!File.Exists(_pendingOpsFilePath)) return;
var json = File.ReadAllText(_pendingOpsFilePath);
var data = JsonSerializer.Deserialize<List<VehiclePendingOperation>>(json, _jsonOpts);
_pendingOps = data ?? new List<VehiclePendingOperation>();
_logger.Information($"[车辆本地] 载入待上传成功 count={_pendingOps.Count}");
}
catch (Exception ex)
{
_pendingOps = new List<VehiclePendingOperation>();
_logger.Warning($"[车辆本地] 载入待上传失败,已清空:{ex.Message}");
}
}
private void LoadCacheFromDisk()
{
try
{
if (!File.Exists(_cacheFilePath)) return;
var json = File.ReadAllText(_cacheFilePath);
var data = JsonSerializer.Deserialize<List<MesXslVehicle>>(json, _jsonOpts);
_localCache = data ?? new List<MesXslVehicle>();
_logger.Information($"[车辆本地] 载入缓存成功 count={_localCache.Count}");
}
catch (Exception ex)
{
_localCache = new List<MesXslVehicle>();
_logger.Warning($"[车辆本地] 载入缓存失败,已清空:{ex.Message}");
}
}
private void SavePendingOpsToDiskUnsafe()
{
var json = JsonSerializer.Serialize(_pendingOps, _jsonOpts);
File.WriteAllText(_pendingOpsFilePath, json);
}
private void SaveCacheToDiskUnsafe()
{
var json = JsonSerializer.Serialize(_localCache, _jsonOpts);
File.WriteAllText(_cacheFilePath, json);
}
private static MesXslVehicle CloneVehicle(MesXslVehicle input)
{
return new MesXslVehicle
{
Id = input.Id,
PlateNumber = input.PlateNumber,
VehicleBelong = input.VehicleBelong,
TareWeightKg = input.TareWeightKg,
LoadCapacity = input.LoadCapacity,
UnitId = input.UnitId,
LoadUnit = input.LoadUnit,
CustomerIds = input.CustomerIds,
CustomerShortName = input.CustomerShortName,
SupplierId = input.SupplierId,
SupplierName = input.SupplierName,
SupplierShortName = input.SupplierShortName,
VehicleLength = input.VehicleLength,
VehicleWidth = input.VehicleWidth,
VehicleHeight = input.VehicleHeight,
DriverName = input.DriverName,
DriverPhone = input.DriverPhone,
Status = input.Status,
TenantId = input.TenantId,
CreateBy = input.CreateBy,
CreateTime = input.CreateTime,
UpdateBy = input.UpdateBy,
UpdateTime = input.UpdateTime,
SysOrgCode = input.SysOrgCode
};
}
private static bool IsLocalTempId(string? id)
{
return !string.IsNullOrWhiteSpace(id)
&& id.StartsWith("local-", StringComparison.OrdinalIgnoreCase);
}
private sealed class VehiclePendingOperation
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public VehicleOperationType OpType { get; set; }
public string? VehicleId { get; set; }
public string? Status { get; set; }
public MesXslVehicle? Vehicle { get; set; }
// 冲突检测用的版本锚点:当本地首次针对该车辆产生修改时,记录当时的服务器 UpdateTime
public DateTime? AnchorUpdateTime { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public int RetryCount { get; set; } = 0;
}
private enum VehicleOperationType
{
Add = 1,
Edit = 2,
Delete = 3,
UpdateStatus = 4
}
/// <summary>
/// 兼容 Jeecg 常见时间字符串格式yyyy-MM-dd HH:mm:ss
/// </summary>
private sealed class NullableDateTimeJsonConverter : JsonConverter<DateTime?>
{
private static readonly string[] SupportedFormats =
[
"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, SupportedFormats, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeLocal, out var exact))
{
return exact;
}
if (DateTime.TryParse(raw, out var fallback))
{
return fallback;
}
}
throw new JsonException($"无法将 JSON 值转换为 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"));
return;
}
writer.WriteNullValue();
}
}
}