|
|
@@ -1,12 +1,30 @@
|
|
1
|
1
|
package com.water.iot.service;
|
|
2
|
2
|
|
|
|
3
|
+import com.fasterxml.jackson.core.JsonProcessingException;
|
|
|
4
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
5
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
6
|
+import com.water.iot.entity.OtaFirmware;
|
|
|
7
|
+import com.water.iot.entity.OtaTask;
|
|
|
8
|
+import com.water.iot.entity.OtaUpgradeRecord;
|
|
3
|
9
|
import lombok.RequiredArgsConstructor;
|
|
4
|
10
|
import lombok.extern.slf4j.Slf4j;
|
|
|
11
|
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
|
|
5
|
12
|
import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
13
|
+import org.springframework.jdbc.support.GeneratedKeyHolder;
|
|
|
14
|
+import org.springframework.jdbc.support.KeyHolder;
|
|
6
|
15
|
import org.springframework.stereotype.Service;
|
|
|
16
|
+import org.springframework.transaction.annotation.Transactional;
|
|
7
|
17
|
|
|
8
|
|
-import java.util.Map;
|
|
|
18
|
+import java.sql.PreparedStatement;
|
|
|
19
|
+import java.sql.Statement;
|
|
|
20
|
+import java.time.LocalDateTime;
|
|
|
21
|
+import java.util.*;
|
|
|
22
|
+import java.util.stream.Collectors;
|
|
9
|
23
|
|
|
|
24
|
+/**
|
|
|
25
|
+ * OTA固件升级服务
|
|
|
26
|
+ * 支持固件上传/版本管理/升级任务创建(按批次)/进度追踪/结果统计/设备查询可用固件
|
|
|
27
|
+ */
|
|
10
|
28
|
@Slf4j
|
|
11
|
29
|
@Service
|
|
12
|
30
|
@RequiredArgsConstructor
|
|
|
@@ -14,21 +32,475 @@ public class OtaService {
|
|
14
|
32
|
|
|
15
|
33
|
private final JdbcTemplate jdbcTemplate;
|
|
16
|
34
|
private final DeviceShadowService shadowService;
|
|
|
35
|
+ private final ObjectMapper objectMapper;
|
|
17
|
36
|
|
|
18
|
|
- /** 创建 OTA 升级任务 */
|
|
19
|
|
- public void createUpgrade(Long modelId, String firmwareVersion, String firmwareUrl, String checkMd5) {
|
|
|
37
|
+ // ========== 固件管理 ==========
|
|
|
38
|
+
|
|
|
39
|
+ /**
|
|
|
40
|
+ * 创建固件版本
|
|
|
41
|
+ */
|
|
|
42
|
+ @Transactional
|
|
|
43
|
+ public OtaFirmware createFirmware(OtaFirmware firmware) {
|
|
|
44
|
+ firmware.setStatus("draft");
|
|
|
45
|
+ firmware.setCreatedAt(LocalDateTime.now());
|
|
|
46
|
+ firmware.setUpdatedAt(LocalDateTime.now());
|
|
|
47
|
+
|
|
|
48
|
+ KeyHolder keyHolder = new GeneratedKeyHolder();
|
|
|
49
|
+ jdbcTemplate.update(connection -> {
|
|
|
50
|
+ var ps = connection.prepareStatement(
|
|
|
51
|
+ "INSERT INTO iot_ota_firmware (model_id, firmware_version, file_url, description, status, md5, file_size, created_at, updated_at) " +
|
|
|
52
|
+ "VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())",
|
|
|
53
|
+ Statement.RETURN_GENERATED_KEYS
|
|
|
54
|
+ );
|
|
|
55
|
+ ps.setLong(1, firmware.getModelId());
|
|
|
56
|
+ ps.setString(2, firmware.getFirmwareVersion());
|
|
|
57
|
+ ps.setString(3, firmware.getFileUrl());
|
|
|
58
|
+ ps.setString(4, firmware.getDescription());
|
|
|
59
|
+ ps.setString(5, firmware.getStatus());
|
|
|
60
|
+ ps.setString(6, firmware.getMd5());
|
|
|
61
|
+ ps.setLong(7, firmware.getFileSize() != null ? firmware.getFileSize() : 0);
|
|
|
62
|
+ return ps;
|
|
|
63
|
+ }, keyHolder);
|
|
|
64
|
+
|
|
|
65
|
+ firmware.setId(keyHolder.getKey().longValue());
|
|
|
66
|
+ log.info("Firmware created: id={}, version={}", firmware.getId(), firmware.getFirmwareVersion());
|
|
|
67
|
+ return firmware;
|
|
|
68
|
+ }
|
|
|
69
|
+
|
|
|
70
|
+ /**
|
|
|
71
|
+ * 发布固件
|
|
|
72
|
+ */
|
|
|
73
|
+ @Transactional
|
|
|
74
|
+ public void publishFirmware(Long firmwareId, String publishedBy) {
|
|
|
75
|
+ int updated = jdbcTemplate.update(
|
|
|
76
|
+ "UPDATE iot_ota_firmware SET status = 'published', published_by = ?, published_at = NOW(), updated_at = NOW() WHERE id = ?",
|
|
|
77
|
+ publishedBy, firmwareId
|
|
|
78
|
+ );
|
|
|
79
|
+ if (updated == 0) throw new RuntimeException("固件不存在: " + firmwareId);
|
|
|
80
|
+ log.info("Firmware {} published by {}", firmwareId, publishedBy);
|
|
|
81
|
+ }
|
|
|
82
|
+
|
|
|
83
|
+ /**
|
|
|
84
|
+ * 废弃固件
|
|
|
85
|
+ */
|
|
|
86
|
+ @Transactional
|
|
|
87
|
+ public void deprecateFirmware(Long firmwareId) {
|
|
|
88
|
+ jdbcTemplate.update(
|
|
|
89
|
+ "UPDATE iot_ota_firmware SET status = 'deprecated', updated_at = NOW() WHERE id = ?",
|
|
|
90
|
+ firmwareId
|
|
|
91
|
+ );
|
|
|
92
|
+ }
|
|
|
93
|
+
|
|
|
94
|
+ /**
|
|
|
95
|
+ * 查询固件详情
|
|
|
96
|
+ */
|
|
|
97
|
+ public OtaFirmware getFirmware(Long firmwareId) {
|
|
|
98
|
+ List<OtaFirmware> list = jdbcTemplate.query(
|
|
|
99
|
+ "SELECT * FROM iot_ota_firmware WHERE id = ?",
|
|
|
100
|
+ new BeanPropertyRowMapper<>(OtaFirmware.class),
|
|
|
101
|
+ firmwareId
|
|
|
102
|
+ );
|
|
|
103
|
+ return list.isEmpty() ? null : list.get(0);
|
|
|
104
|
+ }
|
|
|
105
|
+
|
|
|
106
|
+ /**
|
|
|
107
|
+ * 按模型查询固件版本列表
|
|
|
108
|
+ */
|
|
|
109
|
+ public List<OtaFirmware> listFirmwareByModel(Long modelId) {
|
|
|
110
|
+ return jdbcTemplate.query(
|
|
|
111
|
+ "SELECT * FROM iot_ota_firmware WHERE model_id = ? ORDER BY created_at DESC",
|
|
|
112
|
+ new BeanPropertyRowMapper<>(OtaFirmware.class),
|
|
|
113
|
+ modelId
|
|
|
114
|
+ );
|
|
|
115
|
+ }
|
|
|
116
|
+
|
|
|
117
|
+ /**
|
|
|
118
|
+ * 查询所有固件(分页)
|
|
|
119
|
+ */
|
|
|
120
|
+ public List<OtaFirmware> listAllFirmware(int page, int size) {
|
|
|
121
|
+ int offset = (page - 1) * size;
|
|
|
122
|
+ return jdbcTemplate.query(
|
|
|
123
|
+ "SELECT * FROM iot_ota_firmware ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
|
|
124
|
+ new BeanPropertyRowMapper<>(OtaFirmware.class),
|
|
|
125
|
+ size, offset
|
|
|
126
|
+ );
|
|
|
127
|
+ }
|
|
|
128
|
+
|
|
|
129
|
+ /**
|
|
|
130
|
+ * 设备查询可用固件(基于设备SN查找最新发布的固件)
|
|
|
131
|
+ */
|
|
|
132
|
+ public OtaFirmware getAvailableFirmware(String deviceSn) {
|
|
|
133
|
+ // 查找设备对应的模型
|
|
|
134
|
+ List<Long> modelIds = jdbcTemplate.queryForList(
|
|
|
135
|
+ "SELECT model_id FROM iot_device WHERE device_sn = ? AND model_id IS NOT NULL",
|
|
|
136
|
+ Long.class, deviceSn
|
|
|
137
|
+ );
|
|
|
138
|
+ if (modelIds.isEmpty()) return null;
|
|
|
139
|
+
|
|
|
140
|
+ Long modelId = modelIds.get(0);
|
|
|
141
|
+ List<OtaFirmware> list = jdbcTemplate.query(
|
|
|
142
|
+ "SELECT * FROM iot_ota_firmware WHERE model_id = ? AND status = 'published' ORDER BY created_at DESC LIMIT 1",
|
|
|
143
|
+ new BeanPropertyRowMapper<>(OtaFirmware.class),
|
|
|
144
|
+ modelId
|
|
|
145
|
+ );
|
|
|
146
|
+ return list.isEmpty() ? null : list.get(0);
|
|
|
147
|
+ }
|
|
|
148
|
+
|
|
|
149
|
+ // ========== 升级任务管理 ==========
|
|
|
150
|
+
|
|
|
151
|
+ /**
|
|
|
152
|
+ * 创建OTA升级任务(按批次)
|
|
|
153
|
+ * @param firmwareId 固件ID
|
|
|
154
|
+ * @param deviceIds 目标设备ID列表
|
|
|
155
|
+ * @param batchSize 每批大小
|
|
|
156
|
+ * @param createdBy 创建人
|
|
|
157
|
+ */
|
|
|
158
|
+ @Transactional
|
|
|
159
|
+ public OtaTask createUpgradeTask(Long firmwareId, List<Long> deviceIds, int batchSize, String createdBy) {
|
|
|
160
|
+ OtaFirmware firmware = getFirmware(firmwareId);
|
|
|
161
|
+ if (firmware == null) throw new RuntimeException("固件不存在: " + firmwareId);
|
|
|
162
|
+ if (!"published".equals(firmware.getStatus())) throw new RuntimeException("固件未发布: " + firmwareId);
|
|
|
163
|
+
|
|
|
164
|
+ // 序列化设备ID列表
|
|
|
165
|
+ String deviceIdsJson;
|
|
|
166
|
+ try {
|
|
|
167
|
+ deviceIdsJson = objectMapper.writeValueAsString(deviceIds);
|
|
|
168
|
+ } catch (JsonProcessingException e) {
|
|
|
169
|
+ throw new RuntimeException("设备ID序列化失败", e);
|
|
|
170
|
+ }
|
|
|
171
|
+
|
|
|
172
|
+ OtaTask task = new OtaTask();
|
|
|
173
|
+ task.setFirmwareId(firmwareId);
|
|
|
174
|
+ task.setFirmwareVersion(firmware.getFirmwareVersion());
|
|
|
175
|
+ task.setTargetDeviceIds(deviceIdsJson);
|
|
|
176
|
+ task.setTaskStatus("pending");
|
|
|
177
|
+ task.setBatchSize(batchSize > 0 ? batchSize : 10);
|
|
|
178
|
+ task.setTotalDevices(deviceIds.size());
|
|
|
179
|
+ task.setSuccessCount(0);
|
|
|
180
|
+ task.setFailedCount(0);
|
|
|
181
|
+ task.setExecutingCount(0);
|
|
|
182
|
+ task.setCreatedBy(createdBy);
|
|
|
183
|
+ task.setCreatedAt(LocalDateTime.now());
|
|
|
184
|
+ task.setUpdatedAt(LocalDateTime.now());
|
|
|
185
|
+
|
|
|
186
|
+ KeyHolder keyHolder = new GeneratedKeyHolder();
|
|
|
187
|
+ jdbcTemplate.update(connection -> {
|
|
|
188
|
+ var ps = connection.prepareStatement(
|
|
|
189
|
+ "INSERT INTO iot_ota_task (firmware_id, firmware_version, target_device_ids, task_status, " +
|
|
|
190
|
+ "batch_size, total_devices, success_count, failed_count, executing_count, created_by, created_at, updated_at) " +
|
|
|
191
|
+ "VALUES (?, ?, ?::jsonb, ?, ?, ?, 0, 0, 0, ?, NOW(), NOW())",
|
|
|
192
|
+ Statement.RETURN_GENERATED_KEYS
|
|
|
193
|
+ );
|
|
|
194
|
+ ps.setLong(1, firmwareId);
|
|
|
195
|
+ ps.setString(2, firmware.getFirmwareVersion());
|
|
|
196
|
+ ps.setString(3, deviceIdsJson);
|
|
|
197
|
+ ps.setString(4, "pending");
|
|
|
198
|
+ ps.setInt(5, task.getBatchSize());
|
|
|
199
|
+ ps.setInt(6, deviceIds.size());
|
|
|
200
|
+ ps.setString(7, createdBy);
|
|
|
201
|
+ return ps;
|
|
|
202
|
+ }, keyHolder);
|
|
|
203
|
+
|
|
|
204
|
+ task.setId(keyHolder.getKey().longValue());
|
|
|
205
|
+
|
|
|
206
|
+ // 创建升级记录(每台设备一条)
|
|
|
207
|
+ createUpgradeRecords(task.getId(), deviceIds, firmware.getFirmwareVersion());
|
|
|
208
|
+
|
|
|
209
|
+ log.info("Upgrade task created: id={}, firmware={}, devices={}, batchSize={}",
|
|
|
210
|
+ task.getId(), firmware.getFirmwareVersion(), deviceIds.size(), task.getBatchSize());
|
|
|
211
|
+
|
|
|
212
|
+ return task;
|
|
|
213
|
+ }
|
|
|
214
|
+
|
|
|
215
|
+ /**
|
|
|
216
|
+ * 按设备类型/区域创建升级任务
|
|
|
217
|
+ */
|
|
|
218
|
+ @Transactional
|
|
|
219
|
+ public OtaTask createUpgradeTaskByFilter(Long firmwareId, String deviceType, String area,
|
|
|
220
|
+ int batchSize, String createdBy) {
|
|
|
221
|
+ // 查询符合条件的在线设备
|
|
|
222
|
+ String sql = "SELECT id FROM iot_device WHERE model_id = (SELECT model_id FROM iot_ota_firmware WHERE id = ?) " +
|
|
|
223
|
+ "AND status = 'online'";
|
|
|
224
|
+ List<Object> params = new ArrayList<>();
|
|
|
225
|
+ params.add(firmwareId);
|
|
|
226
|
+
|
|
|
227
|
+ if (deviceType != null && !deviceType.isEmpty()) {
|
|
|
228
|
+ sql += " AND device_type = ?";
|
|
|
229
|
+ params.add(deviceType);
|
|
|
230
|
+ }
|
|
|
231
|
+ if (area != null && !area.isEmpty()) {
|
|
|
232
|
+ sql += " AND area = ?";
|
|
|
233
|
+ params.add(area);
|
|
|
234
|
+ }
|
|
|
235
|
+
|
|
|
236
|
+ List<Long> deviceIds = jdbcTemplate.queryForList(sql, Long.class, params.toArray());
|
|
|
237
|
+ if (deviceIds.isEmpty()) throw new RuntimeException("没有符合条件的在线设备");
|
|
|
238
|
+
|
|
|
239
|
+ OtaTask task = createUpgradeTask(firmwareId, deviceIds, batchSize, createdBy);
|
|
|
240
|
+ task.setTargetType(deviceType);
|
|
|
241
|
+ task.setTargetArea(area);
|
|
|
242
|
+
|
|
|
243
|
+ jdbcTemplate.update(
|
|
|
244
|
+ "UPDATE iot_ota_task SET target_type = ?, target_area = ? WHERE id = ?",
|
|
|
245
|
+ deviceType, area, task.getId()
|
|
|
246
|
+ );
|
|
|
247
|
+
|
|
|
248
|
+ return task;
|
|
|
249
|
+ }
|
|
|
250
|
+
|
|
|
251
|
+ /**
|
|
|
252
|
+ * 启动升级任务(开始执行)
|
|
|
253
|
+ */
|
|
|
254
|
+ @Transactional
|
|
|
255
|
+ public void startTask(Long taskId) {
|
|
|
256
|
+ OtaTask task = getTask(taskId);
|
|
|
257
|
+ if (task == null) throw new RuntimeException("任务不存在: " + taskId);
|
|
|
258
|
+ if (!"pending".equals(task.getTaskStatus())) throw new RuntimeException("任务状态不允许启动: " + task.getTaskStatus());
|
|
|
259
|
+
|
|
|
260
|
+ jdbcTemplate.update(
|
|
|
261
|
+ "UPDATE iot_ota_task SET task_status = 'executing', executed_at = NOW(), updated_at = NOW() WHERE id = ?",
|
|
|
262
|
+ taskId
|
|
|
263
|
+ );
|
|
|
264
|
+
|
|
|
265
|
+ // 将第一批次的记录设为executing
|
|
|
266
|
+ int batchSize = task.getBatchSize();
|
|
20
|
267
|
jdbcTemplate.update(
|
|
21
|
|
- "INSERT INTO iot_device_event (device_id, device_sn, event_type, event_data) " +
|
|
22
|
|
- "SELECT id, device_sn, 'ota', json_build_object('version',?, 'url',?, 'md5',?) " +
|
|
23
|
|
- "FROM iot_device WHERE model_id = ? AND status = 'online'",
|
|
24
|
|
- firmwareVersion, firmwareUrl, checkMd5, modelId);
|
|
25
|
|
- log.info("OTA task created for model {}: version={}", modelId, firmwareVersion);
|
|
|
268
|
+ "UPDATE iot_ota_upgrade_record SET status = 'executing', started_at = NOW() " +
|
|
|
269
|
+ "WHERE task_id = ? AND id IN (SELECT id FROM iot_ota_upgrade_record WHERE task_id = ? AND status = 'pending' LIMIT ?)",
|
|
|
270
|
+ taskId, taskId, batchSize
|
|
|
271
|
+ );
|
|
|
272
|
+
|
|
|
273
|
+ log.info("Task {} started, first batch size: {}", taskId, batchSize);
|
|
26
|
274
|
}
|
|
27
|
275
|
|
|
28
|
|
- /** 设备查询是否有待升级固件 */
|
|
29
|
|
- public Map<String, Object> checkUpgrade(String deviceSn, String currentVersion) {
|
|
30
|
|
- return jdbcTemplate.queryForMap(
|
|
31
|
|
- "SELECT * FROM iot_device_event WHERE device_sn = ? AND event_type = 'ota' ORDER BY created_at DESC LIMIT 1",
|
|
32
|
|
- deviceSn);
|
|
|
276
|
+ /**
|
|
|
277
|
+ * 取消升级任务
|
|
|
278
|
+ */
|
|
|
279
|
+ @Transactional
|
|
|
280
|
+ public void cancelTask(Long taskId) {
|
|
|
281
|
+ jdbcTemplate.update(
|
|
|
282
|
+ "UPDATE iot_ota_task SET task_status = 'cancelled', updated_at = NOW() WHERE id = ? AND task_status IN ('pending', 'executing')",
|
|
|
283
|
+ taskId
|
|
|
284
|
+ );
|
|
|
285
|
+ // 取消未完成的记录
|
|
|
286
|
+ jdbcTemplate.update(
|
|
|
287
|
+ "UPDATE iot_ota_upgrade_record SET status = 'failed', fail_reason = '任务已取消' " +
|
|
|
288
|
+ "WHERE task_id = ? AND status IN ('pending', 'executing')",
|
|
|
289
|
+ taskId
|
|
|
290
|
+ );
|
|
|
291
|
+ }
|
|
|
292
|
+
|
|
|
293
|
+ /**
|
|
|
294
|
+ * 查询升级任务详情
|
|
|
295
|
+ */
|
|
|
296
|
+ public OtaTask getTask(Long taskId) {
|
|
|
297
|
+ List<OtaTask> list = jdbcTemplate.query(
|
|
|
298
|
+ "SELECT * FROM iot_ota_task WHERE id = ?",
|
|
|
299
|
+ new BeanPropertyRowMapper<>(OtaTask.class),
|
|
|
300
|
+ taskId
|
|
|
301
|
+ );
|
|
|
302
|
+ return list.isEmpty() ? null : list.get(0);
|
|
|
303
|
+ }
|
|
|
304
|
+
|
|
|
305
|
+ /**
|
|
|
306
|
+ * 查询升级任务列表
|
|
|
307
|
+ */
|
|
|
308
|
+ public List<OtaTask> listTasks(int page, int size) {
|
|
|
309
|
+ int offset = (page - 1) * size;
|
|
|
310
|
+ return jdbcTemplate.query(
|
|
|
311
|
+ "SELECT * FROM iot_ota_task ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
|
|
312
|
+ new BeanPropertyRowMapper<>(OtaTask.class),
|
|
|
313
|
+ size, offset
|
|
|
314
|
+ );
|
|
|
315
|
+ }
|
|
|
316
|
+
|
|
|
317
|
+ // ========== 进度追踪 ==========
|
|
|
318
|
+
|
|
|
319
|
+ /**
|
|
|
320
|
+ * 更新单台设备升级进度
|
|
|
321
|
+ */
|
|
|
322
|
+ @Transactional
|
|
|
323
|
+ public void updateProgress(Long recordId, int progress) {
|
|
|
324
|
+ jdbcTemplate.update(
|
|
|
325
|
+ "UPDATE iot_ota_upgrade_record SET progress = ? WHERE id = ?",
|
|
|
326
|
+ progress, recordId
|
|
|
327
|
+ );
|
|
|
328
|
+ }
|
|
|
329
|
+
|
|
|
330
|
+ /**
|
|
|
331
|
+ * 标记设备升级成功
|
|
|
332
|
+ */
|
|
|
333
|
+ @Transactional
|
|
|
334
|
+ public void markSuccess(Long recordId, Long deviceId, String toVersion) {
|
|
|
335
|
+ jdbcTemplate.update(
|
|
|
336
|
+ "UPDATE iot_ota_upgrade_record SET status = 'success', progress = 100, completed_at = NOW() WHERE id = ?",
|
|
|
337
|
+ recordId
|
|
|
338
|
+ );
|
|
|
339
|
+ jdbcTemplate.update(
|
|
|
340
|
+ "UPDATE iot_ota_task SET success_count = success_count + 1, executing_count = executing_count - 1, updated_at = NOW() WHERE id = (SELECT task_id FROM iot_ota_upgrade_record WHERE id = ?)",
|
|
|
341
|
+ recordId
|
|
|
342
|
+ );
|
|
|
343
|
+ // 更新设备固件版本
|
|
|
344
|
+ jdbcTemplate.update(
|
|
|
345
|
+ "UPDATE iot_device SET firmware_version = ? WHERE id = ?",
|
|
|
346
|
+ toVersion, deviceId
|
|
|
347
|
+ );
|
|
|
348
|
+
|
|
|
349
|
+ // 检查是否触发下一批次
|
|
|
350
|
+ triggerNextBatch(recordId);
|
|
|
351
|
+ }
|
|
|
352
|
+
|
|
|
353
|
+ /**
|
|
|
354
|
+ * 标记设备升级失败
|
|
|
355
|
+ */
|
|
|
356
|
+ @Transactional
|
|
|
357
|
+ public void markFailed(Long recordId, String failReason) {
|
|
|
358
|
+ jdbcTemplate.update(
|
|
|
359
|
+ "UPDATE iot_ota_upgrade_record SET status = 'failed', fail_reason = ?, completed_at = NOW() WHERE id = ?",
|
|
|
360
|
+ failReason, recordId
|
|
|
361
|
+ );
|
|
|
362
|
+ jdbcTemplate.update(
|
|
|
363
|
+ "UPDATE iot_ota_task SET failed_count = failed_count + 1, executing_count = executing_count - 1, updated_at = NOW() WHERE id = (SELECT task_id FROM iot_ota_upgrade_record WHERE id = ?)",
|
|
|
364
|
+ recordId
|
|
|
365
|
+ );
|
|
|
366
|
+
|
|
|
367
|
+ triggerNextBatch(recordId);
|
|
|
368
|
+ }
|
|
|
369
|
+
|
|
|
370
|
+ /**
|
|
|
371
|
+ * 查询升级任务统计
|
|
|
372
|
+ */
|
|
|
373
|
+ public Map<String, Object> getTaskStatistics(Long taskId) {
|
|
|
374
|
+ OtaTask task = getTask(taskId);
|
|
|
375
|
+ if (task == null) return Collections.emptyMap();
|
|
|
376
|
+
|
|
|
377
|
+ Map<String, Object> stats = new LinkedHashMap<>();
|
|
|
378
|
+ stats.put("taskId", taskId);
|
|
|
379
|
+ stats.put("taskStatus", task.getTaskStatus());
|
|
|
380
|
+ stats.put("totalDevices", task.getTotalDevices());
|
|
|
381
|
+ stats.put("successCount", task.getSuccessCount());
|
|
|
382
|
+ stats.put("failedCount", task.getFailedCount());
|
|
|
383
|
+ stats.put("executingCount", task.getExecutingCount());
|
|
|
384
|
+ stats.put("pendingCount", task.getTotalDevices() - task.getSuccessCount() - task.getFailedCount() - task.getExecutingCount());
|
|
|
385
|
+
|
|
|
386
|
+ double progress = task.getTotalDevices() > 0 ?
|
|
|
387
|
+ ((task.getSuccessCount() + task.getFailedCount()) * 100.0 / task.getTotalDevices()) : 0;
|
|
|
388
|
+ stats.put("progressPercent", Math.round(progress * 100) / 100.0);
|
|
|
389
|
+
|
|
|
390
|
+ // 按状态分组统计
|
|
|
391
|
+ List<Map<String, Object>> byStatus = jdbcTemplate.queryForList(
|
|
|
392
|
+ "SELECT status, COUNT(*) as count FROM iot_ota_upgrade_record WHERE task_id = ? GROUP BY status",
|
|
|
393
|
+ taskId
|
|
|
394
|
+ );
|
|
|
395
|
+ stats.put("statusBreakdown", byStatus);
|
|
|
396
|
+
|
|
|
397
|
+ return stats;
|
|
|
398
|
+ }
|
|
|
399
|
+
|
|
|
400
|
+ /**
|
|
|
401
|
+ * 查询升级记录列表
|
|
|
402
|
+ */
|
|
|
403
|
+ public List<OtaUpgradeRecord> getTaskRecords(Long taskId) {
|
|
|
404
|
+ return jdbcTemplate.query(
|
|
|
405
|
+ "SELECT * FROM iot_ota_upgrade_record WHERE task_id = ? ORDER BY id",
|
|
|
406
|
+ new BeanPropertyRowMapper<>(OtaUpgradeRecord.class),
|
|
|
407
|
+ taskId
|
|
|
408
|
+ );
|
|
|
409
|
+ }
|
|
|
410
|
+
|
|
|
411
|
+ /**
|
|
|
412
|
+ * 查询单台设备的升级历史
|
|
|
413
|
+ */
|
|
|
414
|
+ public List<OtaUpgradeRecord> getDeviceUpgradeHistory(String deviceSn) {
|
|
|
415
|
+ return jdbcTemplate.query(
|
|
|
416
|
+ "SELECT r.* FROM iot_ota_upgrade_record r JOIN iot_device d ON r.device_id = d.id " +
|
|
|
417
|
+ "WHERE d.device_sn = ? ORDER BY r.created_at DESC",
|
|
|
418
|
+ new BeanPropertyRowMapper<>(OtaUpgradeRecord.class),
|
|
|
419
|
+ deviceSn
|
|
|
420
|
+ );
|
|
|
421
|
+ }
|
|
|
422
|
+
|
|
|
423
|
+ // ===== 私有辅助方法 =====
|
|
|
424
|
+
|
|
|
425
|
+ /**
|
|
|
426
|
+ * 创建升级记录
|
|
|
427
|
+ */
|
|
|
428
|
+ private void createUpgradeRecords(Long taskId, List<Long> deviceIds, String toVersion) {
|
|
|
429
|
+ String sql = "INSERT INTO iot_ota_upgrade_record (task_id, device_id, device_sn, from_version, to_version, status, progress, created_at) " +
|
|
|
430
|
+ "VALUES (?, ?, ?, ?, ?, 'pending', 0, NOW())";
|
|
|
431
|
+
|
|
|
432
|
+ List<Object[]> batchArgs = new ArrayList<>();
|
|
|
433
|
+ for (Long deviceId : deviceIds) {
|
|
|
434
|
+ // 查询设备SN和当前版本
|
|
|
435
|
+ List<Map<String, Object>> deviceInfo = jdbcTemplate.queryForList(
|
|
|
436
|
+ "SELECT device_sn, firmware_version FROM iot_device WHERE id = ?",
|
|
|
437
|
+ deviceId
|
|
|
438
|
+ );
|
|
|
439
|
+ if (!deviceInfo.isEmpty()) {
|
|
|
440
|
+ Map<String, Object> info = deviceInfo.get(0);
|
|
|
441
|
+ String deviceSn = (String) info.get("device_sn");
|
|
|
442
|
+ String fromVersion = info.get("firmware_version") != null ?
|
|
|
443
|
+ (String) info.get("firmware_version") : "unknown";
|
|
|
444
|
+ batchArgs.add(new Object[]{taskId, deviceId, deviceSn, fromVersion, toVersion});
|
|
|
445
|
+ }
|
|
|
446
|
+ }
|
|
|
447
|
+
|
|
|
448
|
+ if (!batchArgs.isEmpty()) {
|
|
|
449
|
+ jdbcTemplate.batchUpdate(sql, batchArgs);
|
|
|
450
|
+ }
|
|
|
451
|
+ }
|
|
|
452
|
+
|
|
|
453
|
+ /**
|
|
|
454
|
+ * 触发下一批次
|
|
|
455
|
+ * 当一个批次的设备完成(成功或失败)后,自动启动下一批
|
|
|
456
|
+ */
|
|
|
457
|
+ private void triggerNextBatch(Long recordId) {
|
|
|
458
|
+ Long taskId = jdbcTemplate.queryForObject(
|
|
|
459
|
+ "SELECT task_id FROM iot_ota_upgrade_record WHERE id = ?", Long.class, recordId
|
|
|
460
|
+ );
|
|
|
461
|
+
|
|
|
462
|
+ OtaTask task = getTask(taskId);
|
|
|
463
|
+ if (task == null || !"executing".equals(task.getTaskStatus())) return;
|
|
|
464
|
+
|
|
|
465
|
+ // 检查是否还有执行中的记录
|
|
|
466
|
+ int executing = jdbcTemplate.queryForObject(
|
|
|
467
|
+ "SELECT COUNT(*) FROM iot_ota_upgrade_record WHERE task_id = ? AND status = 'executing'",
|
|
|
468
|
+ Integer.class, taskId
|
|
|
469
|
+ );
|
|
|
470
|
+
|
|
|
471
|
+ if (executing == 0) {
|
|
|
472
|
+ // 启动下一批
|
|
|
473
|
+ int pending = jdbcTemplate.queryForObject(
|
|
|
474
|
+ "SELECT COUNT(*) FROM iot_ota_upgrade_record WHERE task_id = ? AND status = 'pending'",
|
|
|
475
|
+ Integer.class, taskId
|
|
|
476
|
+ );
|
|
|
477
|
+
|
|
|
478
|
+ if (pending > 0) {
|
|
|
479
|
+ int nextBatch = Math.min(pending, task.getBatchSize());
|
|
|
480
|
+ jdbcTemplate.update(
|
|
|
481
|
+ "UPDATE iot_ota_upgrade_record SET status = 'executing', started_at = NOW() " +
|
|
|
482
|
+ "WHERE task_id = ? AND id IN (SELECT id FROM iot_ota_upgrade_record WHERE task_id = ? AND status = 'pending' LIMIT ?)",
|
|
|
483
|
+ taskId, taskId, nextBatch
|
|
|
484
|
+ );
|
|
|
485
|
+ jdbcTemplate.update(
|
|
|
486
|
+ "UPDATE iot_ota_task SET executing_count = executing_count + ? WHERE id = ?",
|
|
|
487
|
+ nextBatch, taskId
|
|
|
488
|
+ );
|
|
|
489
|
+ log.info("Task {} next batch started: {} devices", taskId, nextBatch);
|
|
|
490
|
+ } else {
|
|
|
491
|
+ // 所有设备处理完毕,标记任务完成
|
|
|
492
|
+ int failedCount = jdbcTemplate.queryForObject(
|
|
|
493
|
+ "SELECT COUNT(*) FROM iot_ota_upgrade_record WHERE task_id = ? AND status = 'failed'",
|
|
|
494
|
+ Integer.class, taskId
|
|
|
495
|
+ );
|
|
|
496
|
+ String finalStatus = failedCount > 0 ? "completed" : "completed";
|
|
|
497
|
+ jdbcTemplate.update(
|
|
|
498
|
+ "UPDATE iot_ota_task SET task_status = ?, completed_at = NOW(), updated_at = NOW() WHERE id = ?",
|
|
|
499
|
+ finalStatus, taskId
|
|
|
500
|
+ );
|
|
|
501
|
+ log.info("Task {} completed: total={}, success={}, failed={}",
|
|
|
502
|
+ taskId, task.getTotalDevices(), task.getSuccessCount(), failedCount);
|
|
|
503
|
+ }
|
|
|
504
|
+ }
|
|
33
|
505
|
}
|
|
34
|
506
|
}
|