SyncMapPointsCommandHandler.cs
13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
using MassTransit;
using MassTransit.Internals;
using MassTransit.Mediator;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Rcs.Application.Common;
using Rcs.Application.MessageBus.Commands;
using Rcs.Application.Shared;
using Rcs.Cyaninetech.Services;
using Rcs.Domain.Entities;
using Rcs.Domain.Extensions;
using Rcs.Domain.Repositories;
using Rcs.Domain.Settings;
namespace Rcs.Infrastructure.MessageBus.Handlers.Commands;
/// <summary>
/// 同步地图资源命令处理器
/// @author zzy
/// </summary>
public class SyncMapPointsCommandHandler : IConsumer<SyncMapPointsCommand>
{
private readonly ILogger<SyncMapPointsCommandHandler> _logger;
private readonly IMapRepository _mapRepository;
private readonly IMapNodeRepository _mapNodeRepository;
private readonly IStorageAreaRepository _storageAreaRepository;
private readonly IStorageLocationRepository _storageLocationRepository;
private readonly IStorageLocationTypeRepository _locationTypeRepository;
private readonly ILanYinService _lanYinService;
public SyncMapPointsCommandHandler(
ILogger<SyncMapPointsCommandHandler> logger,
IMapRepository mapRepository,
IMapNodeRepository mapNodeRepository,
IStorageAreaRepository storageAreaRepository,
IStorageLocationRepository storageLocationRepository,
IStorageLocationTypeRepository locationTypeRepository,
ILanYinService lanYinService)
{
_logger = logger;
_mapRepository = mapRepository;
_mapNodeRepository = mapNodeRepository;
_storageAreaRepository = storageAreaRepository;
_storageLocationRepository = storageLocationRepository;
_locationTypeRepository = locationTypeRepository;
_lanYinService = lanYinService;
}
public async Task Consume(ConsumeContext<SyncMapPointsCommand> context)
{
var command = context.Message;
try
{
var map = await _mapRepository.GetByIdAsync(command.MapId, context.CancellationToken);
if (map == null)
{
await context.RespondAsync(ApiResponse.Failed($"未找到ID为 {command.MapId} 的地图"));
return;
}
if (string.IsNullOrWhiteSpace(map.PointsUrl))
throw new BusinessException("请先维护节点资源URL");
// 获取地图资源信息
var locations = await _lanYinService.GetLocationsAsync(map.PointsUrl);
var remoteNodeCodes = locations.Select(l => l.id).ToHashSet();
var remoteAreaCodes = locations.Select(l => l.area).Distinct().ToHashSet();
var remoteLocationCodes = locations.Select(l => l.id).ToHashSet();
// 获取当前地图所有节点,用于后续删除判断
var existingNodes = await _mapNodeRepository.GetByMapIdFullAsync(command.MapId, context.CancellationToken);
// 获取所有已存在的库区和货位,用于去重判断
var existingLocations = existingNodes.SelectMany(a => a.StorageLocations).ToList();
var existingAreas = existingLocations.Select(l => l.StorageArea).ToList();
// 获取默认库位类型
var defaultLocationType = await _locationTypeRepository.GetDefaultAsync();
// 用于跟踪本次循环中已处理的库区和货位,避免重复插入
var processedAreas = new Dictionary<string, Guid>(); // Key: AreaCode, Value: AreaId
var processedLocations = new HashSet<string>(); // LocationCode
// 循环处理locations,根据对应关系同步MapNode、StorageArea、StorageLocation
// 导航关系:MapNode (1) ←→ (N) StorageLocation (N) ←→ (1) StorageArea
// 对应关系:
// 1. location.id ↔ MapNode.NodeCode (一一对应)
// 2. location.id ↔ StorageLocation.LocationCode (一一对应)
// 3. location.id ↔ StorageLocation.MapNodeId (一一对应,建立MapNode与StorageLocation的关联)
// 4. location.area去重 ↔ StorageArea.AreaCode (一一对应)
foreach (var location in locations)
{
var useStatus = ParseUseStatus(location.use_status);
// ============ 1. 处理 MapNode(根据 NodeCode = location.id 判断是否存在)============
var existingNode = existingNodes.FirstOrDefault( en => en.NodeCode == location.id);
Guid nodeId;
if (existingNode != null)
{
// MapNode已存在,更新
nodeId = existingNode.NodeId;
existingNode.NodeName = location.alias;
existingNode.X = (location.position?.x != null ? (double)location.position.x : 0) * 1000;
existingNode.Y = (location.position?.y != null ? (double)location.position.y : 0) * 1000;
existingNode.StorageLocationTypeId = defaultLocationType?.TypeId;
await _mapNodeRepository.UpdateAsync(existingNode, context.CancellationToken);
}
else
{
// MapNode不存在,新增
nodeId = Guid.NewGuid();
var newNode = new MapNode
{
NodeId = nodeId,
MapId = command.MapId,
NodeCode = location.id,
NodeName = location.alias,
X = (location.position?.x != null ? (double)location.position.x : 0) * 1000,
Y = (location.position?.y != null ? (double)location.position.y : 0) * 1000,
Type = MapNodeTYPE.store,
StorageLocationTypeId = defaultLocationType?.TypeId,
Active = true,
CreatedAt = DateTime.Now
};
await _mapNodeRepository.AddAsync(newNode, context.CancellationToken);
}
// ============ 2. 处理 StorageArea(根据 AreaCode = location.area 判断是否存在)============
// 注意:StorageArea 作为逻辑分组,一个 area 对应多个 locations(多个 StorageLocation)
Guid areaId;
if (processedAreas.TryGetValue(location.area, out var existingAreaId))
{
// 本批次已处理过该库区,直接使用
areaId = existingAreaId;
}
else
{
// 从数据库查询是否已存在该库区(AreaCode 有全局唯一约束)
var currentArea = await _storageAreaRepository.GetByAreaCodeAsync(location.area, context.CancellationToken);
if (currentArea != null)
{
// StorageArea已存在,更新
areaId = currentArea.AreaId;
currentArea.AreaCode = location.area;
currentArea.AreaName = location.area;
currentArea.Description = $"自动同步库区(关联{locations.Count(l => l.area == location.area)}个点位)";
await _storageAreaRepository.UpdateAsync(currentArea, context.CancellationToken);
}
else
{
// StorageArea不存在,新增
areaId = Guid.NewGuid();
var newArea = new StorageArea()
{
AreaId = areaId,
AreaCode = location.area,
AreaName = location.area,
Description = $"自动同步库区(关联{locations.Count(l => l.area == location.area)}个点位)",
CreatedAt = DateTime.Now
};
await _storageAreaRepository.AddAsync(newArea, context.CancellationToken);
}
// 记录已处理的库区
processedAreas[location.area] = areaId;
}
// ============ 3. 处理 StorageLocation(根据 LocationCode = location.id 判断是否存在)============
if (!processedLocations.Contains(location.id))
{
// 从数据库查询是否已存在该货位
var currLocation = await _storageLocationRepository.GetByLocationCodeAsync(location.id, context.CancellationToken);
Guid locationId;
if (currLocation != null)
{
// StorageLocation已存在,更新
locationId = currLocation.LocationId;
currLocation.AreaId = areaId;
currLocation.MapNodeId = nodeId; // 建立与MapNode的关联
currLocation.LocationCode = location.id;
currLocation.LocationName = location.alias;
currLocation.Status = useStatus;
currLocation.IsActive = true;
currLocation.UpdatedAt = DateTime.Now;
await _storageLocationRepository.UpdateAsync(currLocation, context.CancellationToken);
}
else
{
// StorageLocation不存在,新增
locationId = Guid.NewGuid();
var newLocation = new StorageLocation()
{
LocationId = locationId,
AreaId = areaId,
MapNodeId = nodeId, // 建立与MapNode的关联
LocationCode = location.id,
LocationName = location.alias,
Status = useStatus,
IsActive = true,
CreatedAt = DateTime.Now
};
await _storageLocationRepository.AddAsync(newLocation, context.CancellationToken);
}
// 记录已处理的货位
processedLocations.Add(location.id);
}
}
// ============ 删除远程不存在的数据 ============
// 删除顺序:先删除子表(StorageLocation),再删除父表(StorageArea、MapNode),避免级联删除问题
// 1. 删除远程不存在的货位(StorageLocation)
foreach (var location in existingLocations)
{
if (!remoteLocationCodes.Contains(location.LocationCode))
{
_logger.LogInformation("删除远程不存在的货位: LocationCode={LocationCode}, LocationName={LocationName}", location.LocationCode, location.LocationName);
await _storageLocationRepository.DeleteAsync(location, context.CancellationToken);
}
}
// 2. 删除远程不存在的库区(StorageArea)
// 注意:需要重新获取,因为上面删除StorageLocation后,existingAreas可能已过时
var remainingAreas = await _storageAreaRepository.GetByMapIdAsync(command.MapId, context.CancellationToken);
foreach (var area in remainingAreas)
{
if (!remoteAreaCodes.Contains(area.AreaCode))
{
_logger.LogInformation("删除远程不存在的库区: AreaCode={AreaCode}, AreaName={AreaName}", area.AreaCode, area.AreaName);
await _storageAreaRepository.DeleteAsync(area, context.CancellationToken);
}
}
// 3. 删除远程不存在的节点(MapNode)
foreach (var node in existingNodes)
{
if (!remoteNodeCodes.Contains(node.NodeCode))
{
_logger.LogInformation("删除远程不存在的节点: NodeCode={NodeCode}, NodeName={NodeName}", node.NodeCode, node.NodeName);
await _mapNodeRepository.DeleteAsync(node, context.CancellationToken);
}
}
if (locations.Count > 0)
{
map.MapCode = locations[0].scene_id.ToString();
await _mapRepository.UpdateAsync(map, context.CancellationToken);
}
await context.RespondAsync(ApiResponse.Successful("同步地图资源成功"));
}
catch (Exception ex)
{
_logger.LogError(ex, "同步地图资源失败: MapId={MapId}", command.MapId);
await context.RespondAsync(ApiResponse.Failed($"同步失败: {ex.Message}"));
}
}
/// <summary>
/// 解析使用状态字符串为枚举
/// @author zzy
/// </summary>
private static StorageLocationStatus ParseUseStatus(string? status)
{
return status switch
{
"use" => StorageLocationStatus.Occupied,
"free" => StorageLocationStatus.Empty,
"pre_use" => StorageLocationStatus.Occupied,
_ => StorageLocationStatus.Empty
};
}
/// <summary>
/// 将节点使用状态转换为库位状态
/// @author zzy
/// </summary>
private static StorageLocationStatus ParseStorageLocationStatus(MapNodeUseStatus? useStatus)
{
return useStatus switch
{
MapNodeUseStatus.use => StorageLocationStatus.Occupied,
MapNodeUseStatus.pre_use => StorageLocationStatus.Reserved,
_ => StorageLocationStatus.Empty
};
}
}