zhaowenyu
2026-02-11 6f92004036dd02a18275626e6312eee7228f0afe
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
package com.ard.sample.media.service.impl;
 
import com.ard.sample.common.error.CommonErrorEnum;
import com.ard.sample.common.model.ResponseResult;
import com.ard.sample.component.mqtt.model.*;
import com.ard.sample.component.mqtt.service.IMessageSenderService;
import com.ard.common.core.redis.RedisConst;
import com.ard.common.core.redis.RedisOpsUtils;
import com.ard.sample.component.websocket.model.BizCodeEnum;
import com.ard.sample.component.websocket.model.CustomWebSocketMessage;
import com.ard.sample.component.websocket.service.ISendMessageService;
import com.ard.sample.component.websocket.service.IWebSocketManageService;
import com.ard.sample.manage.model.dto.DeviceDTO;
import com.ard.sample.manage.model.enums.UserTypeEnum;
import com.ard.sample.manage.service.IDeviceService;
import com.ard.sample.media.model.FileUploadCallback;
import com.ard.sample.media.model.FileUploadDTO;
import com.ard.sample.media.model.MediaFileCountDTO;
import com.ard.sample.media.model.MediaFileDTO;
import com.ard.sample.media.service.IFileService;
import com.ard.sample.media.service.IMediaService;
import com.ard.sample.wayline.mapper.IWaylineJobHistoryMapper;
import com.ard.sample.wayline.model.dto.WaylineJobDTO;
import com.ard.sample.wayline.service.IWaylineJobHistoryService;
import com.ard.sample.wayline.service.IWaylineJobService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Service;
 
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
 
/**
 * @author sean
 * @version 0.2
 * @date 2021/12/9
 */
@Slf4j
@Service
public class MediaServiceImpl implements IMediaService {
 
    @Autowired
    private IFileService fileService;
 
    @Autowired
    private IWaylineJobService waylineJobService;
 
    @Autowired
    private ObjectMapper objectMapper;
 
    @Autowired
    private IMessageSenderService messageSenderService;
 
    @Autowired
    private IDeviceService deviceService;
 
    @Autowired
    private ISendMessageService sendMessageService;
 
    @Autowired
    private IWebSocketManageService webSocketManageService;
 
    @Override
    public Boolean fastUpload(String fingerprint) {
        return fileService.checkExist(fingerprint);
    }
 
    @Override
    public Integer saveMediaFile(FileUploadDTO file) {
        return fileService.saveFile(file);
    }
 
    @Override
    public List<String> getAllTinyFingerprints() {
        return fileService.getAllFiles()
                .stream()
                .map(MediaFileDTO::getTinnyFingerprint)
                .collect(Collectors.toList());
    }
 
    @Override
    public List<String> getExistTinyFingerprints(List<String> tinyFingerprints) {
        List<String> tinyFingerprintList = this.getAllTinyFingerprints();
        return tinyFingerprints
                .stream()
                .filter(tinyFingerprintList::contains)
                .collect(Collectors.toList());
 
    }
 
    /**
     * 处理文件上传回调事件
     *
     * 该方法接收文件上传回调消息,进行业务处理并返回响应。主要逻辑包括:
     * 1. 解析回调数据并构建响应对象
     * 2. 检查回调结果,非成功状态直接返回
     * 3. 验证Redis中存储的媒体文件计数信息,防止重复处理
     * 4. 查询设备信息和作业信息,解析媒体文件
     * 5. 发布处理结果到指定主题
     * 6. 通知上传计数更新
     *
     * @param receiver 包含文件上传回调数据的通用主题接收器对象
     *                  包含以下关键字段:
     *                  - data: 回调数据(JSON格式)
     *                  - gateway: 设备网关标识
     *                  - tid: 事务ID
     *                  - bid: 业务ID
     */
    @Override
    @ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_FILE_UPLOAD_CALLBACK, outputChannel = ChannelName.OUTBOUND)
    public void handleFileUploadCallBack(CommonTopicReceiver receiver) {
        FileUploadCallback callback = objectMapper.convertValue(receiver.getData(), FileUploadCallback.class);
        log.info("媒体库回调-原始报文内容:{}",receiver);
        log.info("媒体库调试-收到文件上传回调:{}",callback);
        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + receiver.getGateway()
                + TopicConst.EVENTS_SUF + TopicConst._REPLY_SUF;
        CommonTopicResponse<RequestsReply> data = CommonTopicResponse.<RequestsReply>builder()
                .timestamp(System.currentTimeMillis())
                .method(EventsMethodEnum.FILE_UPLOAD_CALLBACK.getMethod())
                .data(RequestsReply.success())
                .tid(receiver.getTid())
                .bid(receiver.getBid())
                .build();
 
        if (callback.getResult() != ResponseResult.CODE_SUCCESS) {
            messageSenderService.publish(topic, data);
            log.info("媒体库调试-回调失败:{}",callback);
            return;
        }
 
        String jobId = callback.getFile().getExt().getFlightId();
 
        MediaFileCountDTO mediaFileCount = (MediaFileCountDTO) RedisOpsUtils.hashGet(RedisConst.MEDIA_FILE_PREFIX + receiver.getGateway(), jobId);
        // duplicate data
        if (receiver.getBid().equals(mediaFileCount.getBid()) && receiver.getTid().equals(mediaFileCount.getTid())) {
            messageSenderService.publish(topic, data);
            return;
        }
 
        DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + receiver.getGateway());
        Optional<WaylineJobDTO> jobOpt = waylineJobService.getJobByJobId(jobId);
        if (jobOpt.isPresent()) {
            boolean isSave = parseMediaFile(callback, jobOpt.get());
            log.info("媒体库收到数据:{}",callback);
            if (!isSave) {
                data.setData(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT));
                log.info("媒体库保存失败:{}",callback);
            }
 
        }
 
        messageSenderService.publish(topic, data);
 
        notifyUploadedCount(mediaFileCount, receiver, jobId);
    }
 
    /**
     * 更新上传的计数并通知网页端
     * @param mediaFileCount
     * @param receiver
     * @param jobId
     */
    private void notifyUploadedCount(MediaFileCountDTO mediaFileCount, CommonTopicReceiver receiver, String jobId) {
        mediaFileCount.setBid(receiver.getBid());
        mediaFileCount.setTid(receiver.getTid());
        mediaFileCount.setUploadedCount(mediaFileCount.getUploadedCount() + 1);
 
        String key = RedisConst.MEDIA_FILE_PREFIX + receiver.getGateway();
        // After all the files of the job are uploaded, delete the media file key.
        if (mediaFileCount.getUploadedCount() >= mediaFileCount.getMediaCount()) {
            RedisOpsUtils.hashDel(key, new String[]{jobId});
 
            // After uploading, delete the key with the highest priority.
            String highestKey = RedisConst.MEDIA_HIGHEST_PRIORITY_PREFIX + receiver.getGateway();
            if (RedisOpsUtils.checkExist(highestKey) &&
                    jobId.equals(((MediaFileCountDTO) RedisOpsUtils.get(highestKey)).getJobId())) {
                RedisOpsUtils.del(highestKey);
            }
 
            if (RedisOpsUtils.hashLen(key) == 0) {
                RedisOpsUtils.del(key);
            }
        } else {
            RedisOpsUtils.hashSet(key, jobId, mediaFileCount);
        }
 
        DeviceDTO dock = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + receiver.getGateway());
        sendMessageService.sendBatch(webSocketManageService.getValueAndUserType(UserTypeEnum.WEB.getVal()),
                CustomWebSocketMessage.builder()
                        .bizCode(BizCodeEnum.FILE_UPLOAD_CALLBACK.getCode())
                        .timestamp(System.currentTimeMillis())
                        .data(mediaFileCount)
                        .build());
    }
 
    /**
     * 解析媒体文件并设置相关属性
     *
     * <p>该函数主要完成两个操作:
     * 1. 根据任务中的机库序列号设置拍摄媒体的无人机序列号
     * 2. 从文件对象键中提取路径并设置到文件对象中
     * 最后尝试保存文件并返回是否保存成功
     *
     * @param callback 文件上传回调对象,包含待处理的文件信息
     * @param job 航线任务数据传输对象,包含机库序列号等信息
     * @return 返回true表示文件保存成功,false表示保存失败
     */
    private Boolean parseMediaFile(FileUploadCallback callback, WaylineJobDTO job) {
        // 设置拍摄媒体的无人机
        Optional<DeviceDTO> dockDTO = deviceService.getDeviceBySn(job.getDockSn());
        dockDTO.ifPresent(dock -> callback.getFile().getExt().setSn(dock.getChildDeviceSn()));
 
        // 设定路径
        String objectKey = callback.getFile().getObjectKey();
        callback.getFile().setPath(objectKey.substring(objectKey.indexOf("/") + 1, objectKey.lastIndexOf("/")));
        log.info("保存媒体库数据:{}",callback);
        return fileService.saveFile(callback.getFile()) > 0;
    }
 
    /**
     * 处理最高优先级上传飞行任务媒体事件
     *
     * 该方法处理来自MQTT通道的最高优先级上传飞行任务媒体请求,主要功能包括:
     * 1. 验证请求数据有效性
     * 2. 发送成功响应
     * 3. 更新Redis中媒体文件计数信息
     * 4. 通过WebSocket通知前端
     *
     * @param receiver 包含请求数据的接收器对象,包含方法、bid、tid等信息
     * @param headers 消息头信息,包含接收的MQTT主题等元数据
     */
    @Override
    @ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_HIGHEST_PRIORITY_UPLOAD_FLIGHT_TASK_MEDIA, outputChannel = ChannelName.OUTBOUND)
    public void handleHighestPriorityUploadFlightTaskMedia(CommonTopicReceiver receiver, MessageHeaders headers) {
        Map map = objectMapper.convertValue(receiver.getData(), Map.class);
        if (map.isEmpty() || !map.containsKey(MapKeyConst.FLIGHT_ID)) {
            return;
        }
 
        messageSenderService.publish(headers.get(MqttHeaders.RECEIVED_TOPIC) + TopicConst._REPLY_SUF,
                CommonTopicResponse.builder()
                        .data(RequestsReply.success())
                        .method(receiver.getMethod())
                        .timestamp(System.currentTimeMillis())
                        .bid(receiver.getBid())
                        .tid(receiver.getTid())
                        .build());
 
        String dockSn = receiver.getGateway();
        String jobId = map.get(MapKeyConst.FLIGHT_ID).toString();
        String key = RedisConst.MEDIA_HIGHEST_PRIORITY_PREFIX + dockSn;
        MediaFileCountDTO countDTO = new MediaFileCountDTO();
        if (RedisOpsUtils.checkExist(key)) {
            countDTO = (MediaFileCountDTO) RedisOpsUtils.get(key);
            if (jobId.equals(countDTO.getJobId())) {
                RedisOpsUtils.setWithExpire(key, countDTO, RedisConst.DEVICE_ALIVE_SECOND * 5);
                return;
            }
 
            countDTO.setPreJobId(countDTO.getJobId());
        }
        countDTO.setJobId(jobId);
 
        RedisOpsUtils.setWithExpire(key, countDTO, RedisConst.DEVICE_ALIVE_SECOND * 5);
 
        DeviceDTO dock = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + dockSn);
        sendMessageService.sendBatch(webSocketManageService.getValueAndUserType(UserTypeEnum.WEB.getVal()),
                CustomWebSocketMessage.builder()
                        .timestamp(System.currentTimeMillis())
                        .bizCode(BizCodeEnum.HIGHEST_PRIORITY_UPLOAD_FLIGHT_TASK_MEDIA.getCode())
                        .data(countDTO)
                        .build());
 
    }
}