package com.doumee.service.business.impl.collection;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.doumee.biz.system.SystemDictDataBiz;
|
import com.doumee.core.constants.ResponseStatus;
|
import com.doumee.core.exception.BusinessException;
|
import com.doumee.core.utils.Constants;
|
import com.doumee.core.utils.DateUtil;
|
import com.doumee.core.utils.FtpUtil;
|
import com.doumee.service.business.collection.CollectionMediaConstants;
|
import com.doumee.service.business.collection.MediaFrameUtil;
|
import com.doumee.dao.admin.request.DeliverySnapshotManualRequest;
|
import com.doumee.dao.business.CollectionMediaMapper;
|
import com.doumee.dao.business.DeliveryMediaSnapshotFeedbackMapper;
|
import com.doumee.dao.business.DeliveryMediaSnapshotMapper;
|
import com.doumee.dao.business.model.CollectionMedia;
|
import com.doumee.dao.business.model.DeliveryMediaSnapshot;
|
import com.doumee.dao.business.model.DeliveryMediaSnapshotFeedback;
|
import com.doumee.service.business.DeliverySnapshotService;
|
import com.doumee.service.business.snapshot.SnapshotAnalyzeRequest;
|
import com.doumee.service.business.snapshot.SnapshotAnalyzeResponse;
|
import com.doumee.service.business.snapshot.SnapshotInferClient;
|
import com.doumee.service.business.snapshot.SnapshotInferProperties;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.lang3.StringUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
|
import javax.annotation.Resource;
|
import java.io.File;
|
import java.io.FileInputStream;
|
import java.io.IOException;
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.util.Date;
|
import java.util.List;
|
import java.util.UUID;
|
import java.util.concurrent.Executor;
|
|
@Slf4j
|
@Service
|
public class DeliverySnapshotServiceImpl implements DeliverySnapshotService {
|
|
@Autowired
|
private CollectionMediaMapper collectionMediaMapper;
|
@Autowired
|
private DeliveryMediaSnapshotMapper deliveryMediaSnapshotMapper;
|
@Autowired
|
private DeliveryMediaSnapshotFeedbackMapper deliveryMediaSnapshotFeedbackMapper;
|
@Autowired
|
private SystemDictDataBiz systemDictDataBiz;
|
@Autowired
|
private SnapshotInferClient snapshotInferClient;
|
@Autowired
|
private SnapshotInferProperties snapshotInferProperties;
|
@Resource(name = "asyncExecutor")
|
private Executor asyncExecutor;
|
|
@Override
|
public String submitAnalyze(Integer mediaId) {
|
CollectionMedia media = requireDownloadedMedia(mediaId);
|
if (Constants.equalsInteger(media.getSnapshotStatus(), CollectionMediaConstants.SNAPSHOT_STATUS_PROCESSING)) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "快照分析进行中,请稍后刷新");
|
}
|
markSnapshotProcessing(mediaId);
|
asyncExecutor.execute(() -> executeAnalyze(mediaId));
|
return "已提交快照分析任务,请稍后刷新查看";
|
}
|
|
@Override
|
public void submitAnalyzeAsync(Integer mediaId) {
|
if (!snapshotInferProperties.isAutoOnDownload()) {
|
return;
|
}
|
try {
|
CollectionMedia media = collectionMediaMapper.selectById(mediaId);
|
if (media == null || Constants.equalsInteger(media.getIsdeleted(), Constants.ONE)) {
|
return;
|
}
|
if (!Constants.equalsInteger(media.getDownloadStatus(), Constants.ONE)
|
|| StringUtils.isBlank(media.getFilePathLocal())) {
|
return;
|
}
|
if (Constants.equalsInteger(media.getSnapshotStatus(), CollectionMediaConstants.SNAPSHOT_STATUS_PROCESSING)
|
|| Constants.equalsInteger(media.getSnapshotStatus(), CollectionMediaConstants.SNAPSHOT_STATUS_DONE)) {
|
return;
|
}
|
if (media.getMediaType() != null && media.getMediaType() != 0) {
|
return;
|
}
|
markSnapshotProcessing(mediaId);
|
asyncExecutor.execute(() -> executeAnalyze(mediaId));
|
} catch (Exception e) {
|
log.warn("自动提交快照分析失败 mediaId={}: {}", mediaId, e.getMessage());
|
}
|
}
|
|
@Override
|
public List<DeliveryMediaSnapshot> listByMediaId(Integer mediaId) {
|
List<DeliveryMediaSnapshot> list = deliveryMediaSnapshotMapper.selectList(new QueryWrapper<DeliveryMediaSnapshot>().lambda()
|
.eq(DeliveryMediaSnapshot::getMediaId, mediaId)
|
.eq(DeliveryMediaSnapshot::getIsdeleted, Constants.ZERO)
|
.orderByAsc(DeliveryMediaSnapshot::getSnapshotType));
|
list.forEach(this::fillSnapshotUrl);
|
return list;
|
}
|
|
@Override
|
public String saveManual(DeliverySnapshotManualRequest request) {
|
if (request == null || request.getMediaId() == null || request.getSnapshotType() == null
|
|| request.getTimestampSec() == null) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "参数不完整");
|
}
|
if (!Constants.equalsInteger(request.getSnapshotType(), CollectionMediaConstants.SNAPSHOT_TYPE_STOREFRONT)
|
&& !Constants.equalsInteger(request.getSnapshotType(), CollectionMediaConstants.SNAPSHOT_TYPE_HANDOVER)) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "快照类型无效");
|
}
|
CollectionMedia media = requireDownloadedMedia(request.getMediaId());
|
File videoFile = null;
|
File frameFile = null;
|
try {
|
videoFile = downloadMediaToTemp(media);
|
frameFile = File.createTempFile("hk_snapshot_", ".jpg");
|
if (!MediaFrameUtil.extractFrame(getFfmpegPath(), videoFile, frameFile, request.getTimestampSec().doubleValue())) {
|
throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "截帧失败");
|
}
|
String relativePath = uploadSnapshot(frameFile, media.getId(), request.getSnapshotType());
|
saveFeedback(media.getId(), request.getSnapshotType(), request.getTimestampSec());
|
upsertSnapshot(media.getId(), request.getSnapshotType(), request.getTimestampSec(),
|
relativePath, null, "manual", null);
|
CollectionMedia update = new CollectionMedia();
|
update.setId(media.getId());
|
update.setSnapshotStatus(CollectionMediaConstants.SNAPSHOT_STATUS_DONE);
|
update.setSnapshotTime(new Date());
|
update.setSnapshotMessage(null);
|
collectionMediaMapper.updateById(update);
|
return "保存成功";
|
} catch (BusinessException e) {
|
throw e;
|
} catch (Exception e) {
|
log.error("手动保存快照失败 mediaId={}: {}", request.getMediaId(), e.getMessage(), e);
|
throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "保存快照失败");
|
} finally {
|
deleteQuietly(videoFile);
|
deleteQuietly(frameFile);
|
}
|
}
|
|
private void executeAnalyze(Integer mediaId) {
|
CollectionMedia media = collectionMediaMapper.selectById(mediaId);
|
if (media == null || Constants.equalsInteger(media.getIsdeleted(), Constants.ONE)) {
|
return;
|
}
|
File videoFile = null;
|
File storefrontFrame = null;
|
File handoverFrame = null;
|
try {
|
videoFile = downloadMediaToTemp(media);
|
double duration = MediaFrameUtil.probeDurationSec(getFfmpegPath(), videoFile);
|
if (duration <= 0) {
|
duration = estimateDuration(media);
|
}
|
SnapshotAnalyzeRequest analyzeRequest = new SnapshotAnalyzeRequest();
|
analyzeRequest.setMediaId(mediaId);
|
analyzeRequest.setVideoUrl(buildVideoUrl(media));
|
analyzeRequest.setSampleFps(snapshotInferProperties.getSampleFps());
|
analyzeRequest.setEnableAsr(snapshotInferProperties.isEnableAsr());
|
analyzeRequest.setDurationSec(duration);
|
SnapshotAnalyzeResponse analyzeResponse = snapshotInferClient.analyze(analyzeRequest);
|
if (Boolean.FALSE.equals(analyzeResponse.getSuccess())) {
|
throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(),
|
StringUtils.defaultIfBlank(analyzeResponse.getMessage(), "快照推理失败"));
|
}
|
|
double storefrontSec = analyzeResponse.getStorefront().getTimeSec();
|
double handoverSec = analyzeResponse.getHandover().getTimeSec();
|
if (handoverSec <= storefrontSec) {
|
handoverSec = Math.min(duration > 0 ? duration - 1 : storefrontSec + 60, storefrontSec + 60);
|
}
|
|
storefrontFrame = File.createTempFile("hk_storefront_", ".jpg");
|
handoverFrame = File.createTempFile("hk_handover_", ".jpg");
|
if (!MediaFrameUtil.extractFrame(getFfmpegPath(), videoFile, storefrontFrame, storefrontSec)) {
|
throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "门头图截帧失败");
|
}
|
if (!MediaFrameUtil.extractFrame(getFfmpegPath(), videoFile, handoverFrame, handoverSec)) {
|
throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "交付图截帧失败");
|
}
|
|
String storefrontPath = uploadSnapshot(storefrontFrame, mediaId, CollectionMediaConstants.SNAPSHOT_TYPE_STOREFRONT);
|
String handoverPath = uploadSnapshot(handoverFrame, mediaId, CollectionMediaConstants.SNAPSHOT_TYPE_HANDOVER);
|
|
upsertSnapshot(mediaId, CollectionMediaConstants.SNAPSHOT_TYPE_STOREFRONT,
|
BigDecimal.valueOf(storefrontSec).setScale(2, RoundingMode.HALF_UP),
|
storefrontPath,
|
analyzeResponse.getStorefront().getConfidence(),
|
StringUtils.defaultIfBlank(analyzeResponse.getStorefront().getSource(), "ai"),
|
analyzeResponse.getModelVersion());
|
upsertSnapshot(mediaId, CollectionMediaConstants.SNAPSHOT_TYPE_HANDOVER,
|
BigDecimal.valueOf(handoverSec).setScale(2, RoundingMode.HALF_UP),
|
handoverPath,
|
analyzeResponse.getHandover().getConfidence(),
|
StringUtils.defaultIfBlank(analyzeResponse.getHandover().getSource(), "ai"),
|
analyzeResponse.getModelVersion());
|
|
CollectionMedia done = new CollectionMedia();
|
done.setId(mediaId);
|
done.setSnapshotStatus(CollectionMediaConstants.SNAPSHOT_STATUS_DONE);
|
done.setSnapshotTime(new Date());
|
done.setSnapshotMessage(null);
|
collectionMediaMapper.updateById(done);
|
log.info("快照分析完成 mediaId={} storefrontSec={} handoverSec={}", mediaId, storefrontSec, handoverSec);
|
} catch (Exception e) {
|
log.error("快照分析失败 mediaId={}: {}", mediaId, e.getMessage(), e);
|
CollectionMedia fail = new CollectionMedia();
|
fail.setId(mediaId);
|
fail.setSnapshotStatus(CollectionMediaConstants.SNAPSHOT_STATUS_FAILED);
|
fail.setSnapshotMessage(StringUtils.left(e.getMessage(), 500));
|
collectionMediaMapper.updateById(fail);
|
} finally {
|
deleteQuietly(videoFile);
|
deleteQuietly(storefrontFrame);
|
deleteQuietly(handoverFrame);
|
}
|
}
|
|
private void markSnapshotProcessing(Integer mediaId) {
|
CollectionMedia processing = new CollectionMedia();
|
processing.setId(mediaId);
|
processing.setSnapshotStatus(CollectionMediaConstants.SNAPSHOT_STATUS_PROCESSING);
|
processing.setSnapshotMessage(null);
|
collectionMediaMapper.updateById(processing);
|
}
|
|
private CollectionMedia requireDownloadedMedia(Integer mediaId) {
|
CollectionMedia media = collectionMediaMapper.selectById(mediaId);
|
if (media == null || Constants.equalsInteger(media.getIsdeleted(), Constants.ONE)) {
|
throw new BusinessException(ResponseStatus.DATA_EMPTY);
|
}
|
if (!Constants.equalsInteger(media.getDownloadStatus(), Constants.ONE)
|
|| StringUtils.isBlank(media.getFilePathLocal())) {
|
throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请先下载媒体文件");
|
}
|
return media;
|
}
|
|
private File downloadMediaToTemp(CollectionMedia media) throws IOException {
|
FtpUtil ftp = createFtpClient();
|
File temp = File.createTempFile("hk_media_snap_", resolveSuffix(media));
|
String remote = getMediaFolder() + media.getFilePathLocal();
|
String local = ftp.download(remote, temp.getAbsolutePath());
|
if (StringUtils.isBlank(local)) {
|
ftp.disconnect();
|
throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "下载媒体文件失败");
|
}
|
ftp.disconnect();
|
return temp;
|
}
|
|
private String uploadSnapshot(File frameFile, Integer mediaId, int snapshotType) throws IOException {
|
FtpUtil ftp = createFtpClient();
|
try {
|
String folder = getSnapshotFolder();
|
String suffix = snapshotType == CollectionMediaConstants.SNAPSHOT_TYPE_STOREFRONT ? "_storefront.jpg" : "_handover.jpg";
|
String relative = DateUtil.getNowShortDate() + "/" + mediaId + suffix;
|
String remote = folder + relative;
|
try (FileInputStream in = new FileInputStream(frameFile)) {
|
if (!ftp.uploadInputstream(in, remote)) {
|
throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(), "上传快照失败");
|
}
|
}
|
return relative;
|
} finally {
|
ftp.disconnect();
|
}
|
}
|
|
private void upsertSnapshot(Integer mediaId, int snapshotType, BigDecimal timestampSec, String filePath,
|
Double confidence, String source, String modelVersion) {
|
deliveryMediaSnapshotMapper.update(null, new UpdateWrapper<DeliveryMediaSnapshot>().lambda()
|
.eq(DeliveryMediaSnapshot::getMediaId, mediaId)
|
.eq(DeliveryMediaSnapshot::getSnapshotType, snapshotType)
|
.set(DeliveryMediaSnapshot::getIsdeleted, Constants.ONE));
|
DeliveryMediaSnapshot row = new DeliveryMediaSnapshot();
|
row.setMediaId(mediaId);
|
row.setSnapshotType(snapshotType);
|
row.setTimestampSec(timestampSec);
|
row.setFilePath(filePath);
|
if (confidence != null) {
|
row.setConfidence(BigDecimal.valueOf(confidence).setScale(4, RoundingMode.HALF_UP));
|
}
|
row.setSource(source);
|
row.setModelVersion(modelVersion);
|
row.setCreateDate(new Date());
|
row.setIsdeleted(Constants.ZERO);
|
deliveryMediaSnapshotMapper.insert(row);
|
}
|
|
private void fillSnapshotUrl(DeliveryMediaSnapshot snapshot) {
|
if (StringUtils.isBlank(snapshot.getFilePath())) {
|
return;
|
}
|
try {
|
String prefix = systemDictDataBiz.queryByCode(Constants.FTP, Constants.FTP_RESOURCE_PATH).getCode();
|
snapshot.setFileUrlFull(prefix + getSnapshotFolder() + snapshot.getFilePath());
|
} catch (Exception e) {
|
log.warn("构建快照URL失败 id={}: {}", snapshot.getId(), e.getMessage());
|
}
|
}
|
|
private String buildVideoUrl(CollectionMedia media) {
|
try {
|
String prefix = systemDictDataBiz.queryByCode(Constants.FTP, Constants.FTP_RESOURCE_PATH).getCode();
|
return prefix + getMediaFolder() + media.getFilePathLocal();
|
} catch (Exception e) {
|
return null;
|
}
|
}
|
|
private double estimateDuration(CollectionMedia media) {
|
if (media.getStartTime() != null && media.getEndTime() != null) {
|
long ms = media.getEndTime().getTime() - media.getStartTime().getTime();
|
if (ms > 0) {
|
return ms / 1000.0;
|
}
|
}
|
return 1200.0;
|
}
|
|
private FtpUtil createFtpClient() throws IOException {
|
return new FtpUtil(
|
systemDictDataBiz.queryByCode(Constants.FTP, Constants.FTP_HOST).getCode(),
|
Integer.parseInt(systemDictDataBiz.queryByCode(Constants.FTP, Constants.FTP_PORT).getCode()),
|
systemDictDataBiz.queryByCode(Constants.FTP, Constants.FTP_USERNAME).getCode(),
|
systemDictDataBiz.queryByCode(Constants.FTP, Constants.FTP_PWD).getCode());
|
}
|
|
private String getMediaFolder() {
|
try {
|
return systemDictDataBiz.queryByCode(Constants.FTP, Constants.COLLECTION_MEDIA_FOLDER).getCode();
|
} catch (Exception e) {
|
return "/collection_media/";
|
}
|
}
|
|
private String getSnapshotFolder() {
|
try {
|
return systemDictDataBiz.queryByCode(Constants.FTP, CollectionMediaConstants.COLLECTION_SNAPSHOT_FOLDER).getCode();
|
} catch (Exception e) {
|
return "/collection_snapshot/";
|
}
|
}
|
|
private String getFfmpegPath() {
|
try {
|
return systemDictDataBiz.queryByCode(Constants.CS_PARAM, Constants.CS_FFMPEG_PATH).getCode();
|
} catch (Exception e) {
|
return "ffmpeg";
|
}
|
}
|
|
private String resolveSuffix(CollectionMedia media) {
|
if (StringUtils.isNotBlank(media.getFileName()) && media.getFileName().contains(".")) {
|
return media.getFileName().substring(media.getFileName().lastIndexOf('.')).toLowerCase();
|
}
|
return ".mp4";
|
}
|
|
private void saveFeedback(Integer mediaId, int snapshotType, BigDecimal manualTimeSec) {
|
DeliveryMediaSnapshot existing = deliveryMediaSnapshotMapper.selectOne(new QueryWrapper<DeliveryMediaSnapshot>().lambda()
|
.eq(DeliveryMediaSnapshot::getMediaId, mediaId)
|
.eq(DeliveryMediaSnapshot::getSnapshotType, snapshotType)
|
.eq(DeliveryMediaSnapshot::getIsdeleted, Constants.ZERO)
|
.orderByDesc(DeliveryMediaSnapshot::getId)
|
.last("LIMIT 1"));
|
DeliveryMediaSnapshotFeedback feedback = new DeliveryMediaSnapshotFeedback();
|
feedback.setMediaId(mediaId);
|
feedback.setSnapshotType(snapshotType);
|
if (existing != null && existing.getTimestampSec() != null) {
|
feedback.setAiTimeSec(existing.getTimestampSec());
|
feedback.setModelVersion(existing.getModelVersion());
|
}
|
feedback.setManualTimeSec(manualTimeSec);
|
feedback.setCreateDate(new Date());
|
feedback.setIsdeleted(Constants.ZERO);
|
deliveryMediaSnapshotFeedbackMapper.insert(feedback);
|
}
|
|
private void deleteQuietly(File file) {
|
if (file != null && file.exists() && !file.delete()) {
|
log.warn("临时文件删除失败: {}", file.getAbsolutePath());
|
}
|
}
|
|
}
|