获取人脸对应视频流程,自动删除源视频流程,自动创建任务渲染流程,自动删除人脸数据逻辑

This commit is contained in:
Jerry Yan 2024-12-11 15:38:18 +08:00
parent ba4c339660
commit 8c81a994c8
47 changed files with 1318 additions and 222 deletions

View File

@ -0,0 +1,45 @@
package com.ycwl.basic.device;
import com.ycwl.basic.device.checker.IDeviceStatusChecker;
import com.ycwl.basic.device.checker.impl.AliOssDeviceChecker;
import com.ycwl.basic.device.checker.impl.AlwaysOnDeviceChecker;
import com.ycwl.basic.device.enums.DeviceStoreTypeEnum;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.device.operator.impl.AliOssStorageOperator;
import com.ycwl.basic.device.operator.impl.LocalStorageOperator;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
public class DeviceFactory {
public static IDeviceStorageOperator getDeviceStorageOperator(DeviceEntity device, DeviceConfigEntity config) {
IDeviceStorageOperator operator = null;
if (config.getStoreType() == DeviceStoreTypeEnum.ALI_OSS.getType()) {
operator = new AliOssStorageOperator(config.getStoreConfigJson());
} else if (config.getStoreType() == DeviceStoreTypeEnum.LOCAL.getType()) {
operator = new LocalStorageOperator(config.getStoreConfigJson());
}
if (operator == null) {
return null;
}
operator.setDevice(device);
operator.setDeviceConfig(config);
return operator;
}
public static IDeviceStatusChecker getDeviceStatusChecker(DeviceEntity device, DeviceConfigEntity config) {
IDeviceStatusChecker checker = null;
if (config.getOnlineCheck() <= 0) {
checker = new AlwaysOnDeviceChecker();
} else {
if (config.getStoreType() == DeviceStoreTypeEnum.ALI_OSS.getType()) {
checker = new AliOssDeviceChecker(config.getStoreConfigJson());
}
}
if (checker == null) {
return null;
}
checker.setDevice(device);
checker.setDeviceConfig(config);
return checker;
}
}

View File

@ -0,0 +1,9 @@
package com.ycwl.basic.device;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
public interface IDeviceCommon {
void setDevice(DeviceEntity device);
void setDeviceConfig(DeviceConfigEntity config);
}

View File

@ -0,0 +1,7 @@
package com.ycwl.basic.device.checker;
import com.ycwl.basic.device.IDeviceCommon;
public interface IDeviceStatusChecker extends IDeviceCommon {
boolean checkDeviceStatus();
}

View File

@ -0,0 +1,23 @@
package com.ycwl.basic.device.checker.helper;
import com.alibaba.fastjson.JSON;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import lombok.Setter;
public abstract class CommonDeviceChecker<C> {
protected C config;
@Setter
protected DeviceEntity device;
@Setter
protected DeviceConfigEntity deviceConfig;
public CommonDeviceChecker(String configJson) {
config = JSON.parseObject(configJson, getConfigClass());
}
@SuppressWarnings("unchecked")
private Class<C> getConfigClass() {
return (Class<C>) ((java.lang.reflect.ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
}

View File

@ -0,0 +1,88 @@
package com.ycwl.basic.device.checker.impl;
import cn.hutool.core.date.DateUtil;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.ListObjectsV2Request;
import com.aliyun.oss.model.ListObjectsV2Result;
import com.aliyun.oss.model.OSSObjectSummary;
import com.ycwl.basic.device.checker.IDeviceStatusChecker;
import com.ycwl.basic.device.checker.helper.CommonDeviceChecker;
import com.ycwl.basic.device.entity.alioss.DeviceAliOssConfig;
import com.ycwl.basic.device.entity.common.FileObject;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class AliOssDeviceChecker extends CommonDeviceChecker<DeviceAliOssConfig> implements IDeviceStatusChecker {
public AliOssDeviceChecker(String configJson) {
super(configJson);
}
private OSS getClient() {
return new OSSClientBuilder().build(config.getEndpoint(), config.getAccessKeyId(), config.getAccessKeySecret());
}
private List<FileObject> getOssFileListByPrefix(String prefix) {
OSS ossClient = getClient();
ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request(config.getBucketName());
listObjectsV2Request.setPrefix(config.getPrefix() + prefix);
boolean isTruncated = true;
String continuationToken = null;
List<OSSObjectSummary> objectList = new ArrayList<>();
try {
while (isTruncated) {
if (continuationToken != null) {
listObjectsV2Request.setContinuationToken(continuationToken);
}
// 列举文件
ListObjectsV2Result result = ossClient.listObjectsV2(listObjectsV2Request);
objectList.addAll(result.getObjectSummaries());
isTruncated = result.isTruncated();
continuationToken = result.getNextContinuationToken();
}
return objectList.stream().filter(item -> item.getKey().endsWith(".ts")).map(item -> {
FileObject object = new FileObject();
object.setPath(item.getKey());
object.setNeedDownload(true);
String[] splitDir = item.getKey().split("/");
String splitDate = splitDir[splitDir.length - 2];
String splitName = splitDir[splitDir.length - 1];
String[] splitExt = splitName.split("\\.", 2);
String[] splitDt = splitExt[0].split("_", 2);
String createTime = splitDt[0];
String endTime = splitDt[1];
object.setName(splitName);
object.setEndTime(DateUtil.parse(splitDate+endTime, "yyyyMMddHHmmss"));
object.setCreateTime(DateUtil.parse(splitDate+createTime, "yyyyMMddHHmmss"));
return object;
}).collect(Collectors.toList());
} catch (OSSException e) {
log.error("获取OSS文件列表失败", e);
}
return null;
}
@Override
public boolean checkDeviceStatus() {
Date now = new Date();
if (deviceConfig.getOnlineMaxInterval() == null) {
// 没有合适配置
return true;
}
Date startTime = DateUtil.offsetMinute(now, -deviceConfig.getOnlineMaxInterval());
List<FileObject> fileList = getOssFileListByPrefix(DateUtil.format(startTime, "yyyyMMdd/HHmm"));
if (fileList != null) {
return !fileList.isEmpty();
} else {
return false;
}
}
}

View File

@ -0,0 +1,20 @@
package com.ycwl.basic.device.checker.impl;
import com.ycwl.basic.device.checker.IDeviceStatusChecker;
import com.ycwl.basic.device.checker.helper.CommonDeviceChecker;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import lombok.Setter;
@SuppressWarnings("LombokSetterMayBeUsed")
public class AlwaysOnDeviceChecker implements IDeviceStatusChecker {
@Setter
private DeviceEntity device;
@Setter
private DeviceConfigEntity deviceConfig;
@Override
public boolean checkDeviceStatus() {
return true;
}
}

View File

@ -0,0 +1,14 @@
package com.ycwl.basic.device.entity.alioss;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
@Data
public class DeviceAliOssConfig {
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String endpoint;
private String region;
private String prefix;
}

View File

@ -0,0 +1,15 @@
package com.ycwl.basic.device.entity.common;
import lombok.Data;
import java.util.Date;
@Data
public class FileObject {
private String path;
private String name;
private String url;
private boolean needDownload = false;
private Date createTime;
private Date endTime;
}

View File

@ -0,0 +1,7 @@
package com.ycwl.basic.device.entity.local;
import lombok.Data;
@Data
public class DeviceLocalConfig {
}

View File

@ -0,0 +1,23 @@
package com.ycwl.basic.device.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum DeviceStoreTypeEnum {
ALI_OSS(1, "阿里云OSS"),
LOCAL(2, "本地文件");
private final int type;
private final String remark;
public static DeviceStoreTypeEnum getByType(int type) {
for (DeviceStoreTypeEnum e : values()) {
if (e.type == type) {
return e;
}
}
return null;
}
}

View File

@ -0,0 +1,26 @@
package com.ycwl.basic.device.operator;
import com.ycwl.basic.device.IDeviceCommon;
import com.ycwl.basic.device.entity.common.FileObject;
import java.util.Date;
import java.util.List;
public interface IDeviceStorageOperator extends IDeviceCommon {
/**
* 获取指定时间范围内的文件列表
*
* @param startDate 开始时间
* @param endDate 结束时间
* @return
*/
List<FileObject> getFileListByDtRange(Date startDate, Date endDate);
/**
* 删除指定日期之前的文件不包含指定的日期当天
*
* @param date 指定日期不包含指定日期当天
* @return
*/
boolean removeFilesBeforeDate(Date date);
}

View File

@ -0,0 +1,24 @@
package com.ycwl.basic.device.operator.helper;
import com.alibaba.fastjson.JSON;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import lombok.Setter;
public abstract class CommonPieceGetter<C> {
protected C config;
@Setter
private DeviceEntity device;
@Setter
private DeviceConfigEntity deviceConfig;
public CommonPieceGetter(String configJson) {
config = JSON.parseObject(configJson, getConfigClass());
}
@SuppressWarnings("unchecked")
private Class<C> getConfigClass() {
return (Class<C>) ((java.lang.reflect.ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
}

View File

@ -0,0 +1,151 @@
package com.ycwl.basic.device.operator.impl;
import cn.hutool.core.date.DateUtil;
import com.aliyun.oss.HttpMethod;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.DeleteObjectsRequest;
import com.aliyun.oss.model.ListObjectsV2Request;
import com.aliyun.oss.model.ListObjectsV2Result;
import com.aliyun.oss.model.OSSObjectSummary;
import com.ycwl.basic.device.entity.alioss.DeviceAliOssConfig;
import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.device.operator.helper.CommonPieceGetter;
import lombok.extern.slf4j.Slf4j;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class AliOssStorageOperator extends CommonPieceGetter<DeviceAliOssConfig> implements IDeviceStorageOperator {
public AliOssStorageOperator(String configJson) {
super(configJson);
}
private OSS getClient() {
return new OSSClientBuilder().build(config.getEndpoint(), config.getAccessKeyId(), config.getAccessKeySecret());
}
private List<OSSObjectSummary> getOssObjectListByPrefix(String prefix) {
OSS ossClient = getClient();
ListObjectsV2Request listObjectsV2Request = new ListObjectsV2Request(config.getBucketName());
listObjectsV2Request.setPrefix(config.getPrefix() + prefix);
boolean isTruncated = true;
String continuationToken = null;
List<OSSObjectSummary> objectList = new ArrayList<>();
try {
while (isTruncated) {
if (continuationToken != null) {
listObjectsV2Request.setContinuationToken(continuationToken);
}
// 列举文件
ListObjectsV2Result result = ossClient.listObjectsV2(listObjectsV2Request);
objectList.addAll(result.getObjectSummaries());
isTruncated = result.isTruncated();
continuationToken = result.getNextContinuationToken();
}
return objectList;
} catch (OSSException e) {
log.error("获取OSS文件列表失败", e);
}
return null;
}
private List<FileObject> getOssFileListByPrefix(String prefix) {
OSS ossClient = getClient();
List<OSSObjectSummary> objectList = getOssObjectListByPrefix(prefix);
if (objectList == null) {
return null;
}
return objectList.stream().filter(item -> item.getKey().endsWith(".ts")).map(item -> {
FileObject object = new FileObject();
object.setPath(item.getKey());
URL url = ossClient.generatePresignedUrl(item.getBucketName(), item.getKey(), DateUtil.offsetHour(new Date(), 8), HttpMethod.GET);
object.setUrl(url.toString());
object.setNeedDownload(true);
String[] splitDir = item.getKey().split("/");
String splitDate = splitDir[splitDir.length - 2];
String splitName = splitDir[splitDir.length - 1];
String[] splitExt = splitName.split("\\.", 2);
String[] splitDt = splitExt[0].split("_", 2);
String createTime = splitDt[0];
String endTime = splitDt[1];
object.setName(splitName);
object.setEndTime(DateUtil.parse(splitDate+endTime, "yyyyMMddHHmmss"));
object.setCreateTime(DateUtil.parse(splitDate+createTime, "yyyyMMddHHmmss"));
return object;
}).collect(Collectors.toList());
}
private boolean removeFilesByPrefix(String prefix) {
OSS ossClient = getClient();
List<OSSObjectSummary> objectList = getOssObjectListByPrefix(prefix);
if (objectList == null) {
return false;
}
DeleteObjectsRequest request = new DeleteObjectsRequest(config.getBucketName());
request.setKeys(objectList.stream().map(OSSObjectSummary::getKey).collect(Collectors.toList()));
try {
ossClient.deleteObjects(request);
return true;
} catch (OSSException e) {
return false;
} finally {
ossClient.shutdown();
}
}
@Override
public List<FileObject> getFileListByDtRange(Date startDate, Date endDate) {
if (startDate == null || endDate == null) {
return null;
}
List<FileObject> fileList = new ArrayList<>();
if (startDate.after(endDate)) {
return fileList;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(startDate);
calendar.set(Calendar.SECOND, 0);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd/HHmm");
while (calendar.getTime().before(endDate)) {
String prefix = dateFormat.format(calendar.getTime());
List<FileObject> fileListByPrefix = getOssFileListByPrefix(prefix);
if (fileListByPrefix == null) {
return null;
}
fileList.addAll(fileListByPrefix);
calendar.add(Calendar.MINUTE, 1);
}
calendar.clear();
return fileList.stream()
.sorted(Comparator.comparing(FileObject::getCreateTime))
.filter(item -> item.getCreateTime().after(startDate))
.filter(item -> item.getCreateTime().before(endDate))
.collect(Collectors.toList());
}
@Override
public boolean removeFilesBeforeDate(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.HOUR_OF_DAY, 0);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd/");
calendar.add(Calendar.DAY_OF_MONTH, -1);
String prefix = dateFormat.format(calendar.getTime());
return removeFilesByPrefix(prefix);
}
}

View File

@ -0,0 +1,26 @@
package com.ycwl.basic.device.operator.impl;
import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.entity.local.DeviceLocalConfig;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.device.operator.helper.CommonPieceGetter;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public class LocalStorageOperator extends CommonPieceGetter<DeviceLocalConfig> implements IDeviceStorageOperator {
public LocalStorageOperator(String configJson) {
super(configJson);
}
@Override
public List<FileObject> getFileListByDtRange(Date startDate, Date endDate) {
return Collections.emptyList();
}
@Override
public boolean removeFilesBeforeDate(Date date) {
return false;
}
}

View File

@ -1,6 +1,7 @@
package com.ycwl.basic.mapper.pc;
import com.ycwl.basic.model.mobile.scenic.ScenicDeviceCountVO;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.device.req.DeviceAddOrUpdateReq;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
@ -24,7 +25,10 @@ public interface DeviceMapper {
int update(DeviceAddOrUpdateReq deviceReqQuery);
int updateStatus(Long id);
DeviceEntity getByDeviceId(Long deviceId);
List<DeviceRespVO> listByScenicId(Long scenicId);
ScenicDeviceCountVO deviceCountByScenicId(@Param("scenicId") Long scenicId,@Param("userId") Long userId);
DeviceConfigEntity getConfigByDeviceId(Long deviceId);
}

View File

@ -16,6 +16,7 @@ import java.util.List;
@Mapper
public interface FaceMapper {
List<FaceRespVO> list(FaceReqQuery faceReqQuery);
List<FaceRespVO> listByScenicIdAndNotFinished(Long scenicId);
FaceRespVO getById(Long id);
int add(FaceEntity face);
int deleteById(Long id);
@ -23,4 +24,6 @@ public interface FaceMapper {
int update(FaceEntity face);
FaceRespVO getByMemberId(Long userId);
int finishedJourney(Long faceId);
}

View File

@ -21,4 +21,6 @@ public interface FaceSampleMapper {
int deleteById(Long id);
int deleteByIds(@Param("list") List<Long> ids);
int update(FaceSampleEntity faceSample);
List<FaceSampleRespVO> listByIds(List<Long> list);
}

View File

@ -30,6 +30,7 @@ public interface ScenicMapper {
int updateStatus(Long id);
ScenicConfigEntity getConfig(Long scenicId);
/**
* 添加景区配置
*
@ -51,7 +52,7 @@ public interface ScenicMapper {
*
* @param scenicId
*/
void deleteConfigByscenicId(Long scenicId);
void deleteConfigByScenicId(Long scenicId);
List<ScenicAppVO> appList(ScenicReqQuery scenicReqQuery);

View File

@ -3,6 +3,7 @@ package com.ycwl.basic.mapper.pc;
import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.task.req.TaskReqQuery;
import com.ycwl.basic.model.pc.task.resp.TaskRespVO;
import com.ycwl.basic.model.pc.template.entity.TemplateConfigEntity;
import com.ycwl.basic.model.pc.template.entity.TemplateEntity;
import com.ycwl.basic.model.pc.template.req.TemplateReqQuery;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
@ -26,4 +27,9 @@ public interface TemplateMapper {
int deleteByPid(Long pid);
int deleteByScenicId(Long scenicId);
List<TemplateRespVO> getByPid(Long id);
TemplateConfigEntity getConfig(Long templateId);
int addConfig(TemplateConfigEntity templateConfig);
int updateConfigById(TemplateConfigEntity templateConfigEntity);
int deleteConfigByTemplateId(Long templateId);
int deleteConfigById(Long id);
}

View File

@ -6,6 +6,7 @@ import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.video.req.VideoReqQuery;
import com.ycwl.basic.model.pc.video.resp.VideoRespVO;
import lombok.NonNull;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@ -22,4 +23,6 @@ public interface VideoMapper {
int add(VideoEntity task);
int deleteById(Long id);
int update(VideoEntity task);
VideoEntity findByTaskId(@NonNull Long taskId);
}

View File

@ -0,0 +1,59 @@
package com.ycwl.basic.model.pc.device.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("device_config")
public class DeviceConfigEntity {
private Long id;
/**
* 设备id
*/
private Long deviceId;
/**
* 启用时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 存储类型
*/
private Integer storeType;
/**
* 存储配置
*/
private String storeConfigJson;
/**
* 存储过期天数
*/
private Integer storeExpireDay;
/**
* 检测设备是否在线
*/
private Integer onlineCheck;
/**
* 检测设备是否在线最大间隔
*/
private Integer onlineMaxInterval;
/**
* 切割时取人脸前多少秒的视频
*/
private BigDecimal cutPre;
/**
* 切割时取人脸后多少秒的视频
*/
private BigDecimal cutPost;
}

View File

@ -16,6 +16,7 @@ import java.util.Date;
public class FaceEntity {
@TableId
private Long id;
private Long scenicId;
/**
* 人脸得分
*/

View File

@ -16,6 +16,8 @@ import java.util.Date;
@Data
@ApiModel("人脸查询参数")
public class FaceReqQuery extends BaseQueryParameterReq {
@ApiModelProperty("景区id")
private Long scenicId;
@ApiModelProperty("会员id")
private Long memberId;
@ApiModelProperty("用户上传的人脸照片")

View File

@ -18,6 +18,7 @@ import java.util.Date;
@ApiModel(value = "订单查询对象")
public class OrderReqQuery extends BaseQueryParameterReq {
private Long id;
private Long scenicId;
private Long memberId;
@ApiModelProperty("用户昵称")
private String memberNickname;

View File

@ -102,4 +102,6 @@ public class OrderRespVO {
private Date refundAt;
@ApiModelProperty("订单明细")
private List<OrderItemVO> orderItemList;
private Long scenicId;
private String scenicName;
}

View File

@ -38,4 +38,21 @@ public class ScenicConfigEntity {
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 预约流程1-预约2-在线3-全部
*/
private Integer bookRoutine;
/**
* 样本保存时间
*/
private Integer sampleStoreDay;
/**
* 视频保存时间
*/
private Integer videoStoreDay;
/**
* 最大行程时长
*/
private Integer maxJourneyHour;
}

View File

@ -1,7 +1,6 @@
package com.ycwl.basic.model.pc.scenic.req;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -78,8 +77,6 @@ public class ScenicAddOrUpdateReq {
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
@ApiModelProperty("景区配置")
private ScenicConfigEntity scenicConfig;
@ApiModelProperty("景区源素材价格,元")
private BigDecimal price;

View File

@ -79,13 +79,11 @@ public class ScenicRespVO {
* 状态 1启用0关闭
*/
@ApiModelProperty("状态 1启用0关闭")
private String status;
private Integer status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
@ApiModelProperty("景区配置")
private ScenicConfigEntity scenicConfig;
@ApiModelProperty("景区源素材价格,元")
private BigDecimal price;
@ApiModelProperty("镜头数")

View File

@ -30,6 +30,10 @@ public class SourceEntity {
* 所属用户
*/
private Long memberId;
/**
* 人脸样本id
*/
private Long faceSampleId;
/**
* 原素材类型1视频2图像
*/

View File

@ -0,0 +1,16 @@
package com.ycwl.basic.model.pc.template.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
@Data
@TableName("template_config")
public class TemplateConfigEntity {
private Long id;
private Long templateId;
private Integer isDefault;
private Date createDate;
private Integer minimalPlaceholderFill;
}

View File

@ -7,6 +7,7 @@ import com.ycwl.basic.service.pc.MenuService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.SnowFlakeUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@ -15,7 +16,7 @@ import java.util.List;
* @Authorlongbinbin
* @Date2024/12/3 10:16
*/
@Service
public class MenuServiceImpl implements MenuService {
@Autowired

View File

@ -96,7 +96,7 @@ public class ScenicServiceImpl implements ScenicService {
public ApiResponse<Boolean> deleteById(Long id) {
int i = scenicMapper.deleteById(id);
if (i > 0) {
scenicMapper.deleteConfigByscenicId(id);
scenicMapper.deleteConfigByScenicId(id);
scenicAccountMapper.deleteByScenicId(id);
return ApiResponse.success(true);
}else {

View File

@ -21,6 +21,7 @@ import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.entity.FaceSampleEntity;
import com.ycwl.basic.model.pc.faceSample.req.FaceSampleReqQuery;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.task.resp.AddFaceRespVo;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.task.TaskFaceService;
@ -84,13 +85,12 @@ public class TaskFaceServiceImpl implements TaskFaceService {
.map(SearchFaceResponse.Data.MatchListItem.FaceItemsItem::getExtraData)
.map(Long::parseLong)
.collect(Collectors.toList());
faceEntity.setMatchSampleIds(StringUtils.joinWith(",", faceSampleIds));
faceEntity.setMatchSampleIds(StringUtils.join(faceSampleIds, ","));
faceMapper.update(faceEntity);
respVo.setSampleListIds(faceSampleIds);
respVo.setScore(matchList.get(0).getQualitieScore());
return respVo;
} catch (Exception e) {
e.printStackTrace();
log.error("人脸搜索失败:{}", e.getMessage());
throw new BaseException(e.getMessage());
}
@ -136,7 +136,15 @@ public class TaskFaceServiceImpl implements TaskFaceService {
FaceSampleReqQuery query = new FaceSampleReqQuery();
query.setDeviceId(scenicId);
faceSampleMapper.list(query);
Date thatDay = DateUtil.offsetDay(new Date(), -3);
ScenicConfigEntity scenicConfig = scenicMapper.getConfig(scenicId);
if (scenicConfig == null) {
return;
}
Integer sampleStoreDay = scenicConfig.getSampleStoreDay();
if (sampleStoreDay == null) {
sampleStoreDay = 3;
}
Date thatDay = DateUtil.offsetDay(new Date(), -sampleStoreDay);
Date dayStart = DateUtil.beginOfDay(thatDay);
Date dayEnd = DateUtil.endOfDay(thatDay);
query.setStartTime(dayStart);

View File

@ -8,6 +8,7 @@ import com.ycwl.basic.mapper.pc.RenderWorkerMapper;
import com.ycwl.basic.mapper.pc.SourceMapper;
import com.ycwl.basic.mapper.pc.TaskMapper;
import com.ycwl.basic.mapper.pc.TemplateMapper;
import com.ycwl.basic.mapper.pc.VideoMapper;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.renderWorker.entity.RenderWorkerEntity;
@ -16,6 +17,7 @@ import com.ycwl.basic.model.pc.task.entity.TaskEntity;
import com.ycwl.basic.model.pc.task.resp.TaskRespVO;
import com.ycwl.basic.model.pc.template.req.TemplateReqQuery;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
import com.ycwl.basic.model.task.req.TaskReqVo;
import com.ycwl.basic.model.task.req.WorkerAuthReqVo;
@ -57,6 +59,8 @@ public class TaskTaskServiceImpl implements TaskService {
private SourceMapper sourceMapper;
@Autowired
private OssUtil ossUtil;
@Autowired
private VideoMapper videoMapper;
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
String accessKey = req.getAccessKey();
@ -112,9 +116,7 @@ public class TaskTaskServiceImpl implements TaskService {
}
List<TaskRespVO> taskList = taskMapper.selectNotRunning();
resp.setTasks(taskList);
taskList.forEach(task -> {
taskMapper.assignToWorker(task.getId(), worker.getId());
});
taskList.forEach(task -> taskMapper.assignToWorker(task.getId(), worker.getId()));
// return Task
return resp;
}
@ -131,9 +133,7 @@ public class TaskTaskServiceImpl implements TaskService {
}
Map<String, List<SourceRespVO>> sourcesMap = Arrays.stream(faceRespVO.getMatchSampleIds().split(","))
.map(Long::valueOf)
.map(sampleId -> {
return faceSampleMapper.getById(sampleId);
})
.map(sampleId -> faceSampleMapper.getById(sampleId))
.filter(Objects::nonNull)
.map(FaceSampleRespVO::getSourceId)
.map(sourceId -> sourceMapper.getById(sourceId))
@ -178,6 +178,22 @@ public class TaskTaskServiceImpl implements TaskService {
taskUpdate.setStatus(1);
taskUpdate.setWorkerId(worker.getId());
taskMapper.update(taskUpdate);
VideoEntity video = videoMapper.findByTaskId(taskId);
if (video != null) {
video.setVideoUrl(task.getVideoUrl());
videoMapper.update(video);
} else {
video = new VideoEntity();
video.setId(SnowFlakeUtil.getLongId());
video.setScenicId(task.getScenicId());
video.setMemberId(task.getMemberId());
video.setTemplateId(task.getTemplateId());
video.setTaskId(taskId);
video.setWorkerId(worker.getId());
video.setVideoUrl(task.getVideoUrl());
video.setCreateTime(new Date());
videoMapper.add(video);
}
}
@Override

View File

@ -0,0 +1,142 @@
package com.ycwl.basic.task;
import com.ycwl.basic.mapper.pc.DeviceMapper;
import com.ycwl.basic.mapper.pc.FaceMapper;
import com.ycwl.basic.mapper.pc.FaceSampleMapper;
import com.ycwl.basic.mapper.pc.ScenicMapper;
import com.ycwl.basic.mapper.pc.TemplateMapper;
import com.ycwl.basic.model.pc.face.resp.FaceRespVO;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.model.pc.template.entity.TemplateConfigEntity;
import com.ycwl.basic.model.pc.template.req.TemplateReqQuery;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.task.resp.SearchFaceRespVo;
import com.ycwl.basic.service.task.TaskFaceService;
import com.ycwl.basic.service.task.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@Component
@EnableScheduling
@Slf4j
public class DynamicTaskGenerator {
@Autowired
private ScenicMapper scenicMapper;
@Autowired
private TemplateMapper templateMapper;
@Autowired
private FaceMapper faceMapper;
@Autowired
private TaskFaceService faceService;
@Autowired
private FaceSampleMapper faceSampleMapper;
@Autowired
private TaskService taskService;
@Autowired
private DeviceMapper deviceMapper;
@Scheduled(cron = "0 0 * * * ?")
public void dynamicTask() {
List<ScenicRespVO> scenicList = scenicMapper.list(new ScenicReqQuery());
for (ScenicRespVO scenic : scenicList) {
log.info("定时任务执行,当前景区:{}", scenic.getName());
ScenicConfigEntity scenicConfig = scenicMapper.getConfig(scenic.getId());
if (scenicConfig == null || scenicConfig.getBookRoutine() == 2) {
log.info("当前景区{},未启用提前预约流程", scenic.getName());
continue;
}
log.info("当前景区{},启用了提前预约流程", scenic.getName());
TemplateReqQuery templateQuery = new TemplateReqQuery();
templateQuery.setScenicId(scenic.getId());
List<TemplateRespVO> templateList = templateMapper.list(templateQuery);
for (TemplateRespVO template : templateList) {
log.info("当前景区{},启用了提前预约流程,模板:{}", scenic.getName(), template.getName());
if (template.getStatus() == 0) {
log.info("当前模板{}未启用,跳过", template.getName());
continue;
}
TemplateConfigEntity templateConfig = templateMapper.getConfig(template.getId());
if (templateConfig == null) {
log.info("当前模板{}未配置,跳过", template.getName());
continue;
}
if (templateConfig.getIsDefault() == 0) {
if (scenicConfig.getBookRoutine() == 1) {
log.info("当前模板{}未启用默认,且景区启用预约流程,跳过", template.getName());
continue;
}
}
Integer minimalPlaceholderFill = templateConfig.getMinimalPlaceholderFill();
List<String> placeholderList = new ArrayList<>();
if (minimalPlaceholderFill == null) {
minimalPlaceholderFill = 0;
}
List<TemplateRespVO> subTemplateList = templateMapper.getByPid(template.getId());
for (TemplateRespVO subTemplate : subTemplateList) {
if (subTemplate.getIsPlaceholder() == 1) {
placeholderList.add(subTemplate.getSourceUrl());
}
}
if (minimalPlaceholderFill == 0) {
for (TemplateRespVO subTemplate : subTemplateList) {
if (subTemplate.getIsPlaceholder() == 1) {
minimalPlaceholderFill += 1;
}
}
}
log.info("当前模板{}启用默认,最小占位素材:{}", template.getName(), minimalPlaceholderFill);
// 查找人脸样本
List<FaceRespVO> list = faceMapper.listByScenicIdAndNotFinished(scenic.getId());
for (FaceRespVO face : list) {
log.info("当前模板{}启用默认,人脸样本:{}", template.getName(), face.getFaceUrl());
if (((new Date()).getTime() - face.getCreateAt().getTime()) > scenicConfig.getMaxJourneyHour() * 3600 * 1000) {
log.info("当前人脸样本{}已超过最大游玩{}小时,自动检测人脸", face.getFaceUrl(), scenicConfig.getMaxJourneyHour());
List<String> oldMatchedSampleListIds = new ArrayList<>();
if (face.getMatchSampleIds() != null) {
oldMatchedSampleListIds = Arrays.asList(face.getMatchSampleIds().split(","));
}
SearchFaceRespVo searchFace = faceService.searchFace(scenic.getId(), face.getId());
if (oldMatchedSampleListIds.size() == searchFace.getSampleListIds().size()) {
boolean isEqual = true;
for (Long sampleId : searchFace.getSampleListIds()) {
if (!oldMatchedSampleListIds.contains(sampleId.toString())) {
isEqual = false;
break;
}
}
if (isEqual) {
log.info("当前人脸样本{}已超过最大游玩{}小时,但人脸检测结果与上次相同,跳过", face.getFaceUrl(), scenicConfig.getMaxJourneyHour());
continue;
}
}
List<FaceSampleRespVO> faceSampleList = faceSampleMapper.listByIds(searchFace.getSampleListIds());
int matchedPlaceholder = 0;
for (FaceSampleRespVO faceSample : faceSampleList) {
if (placeholderList.contains(faceSample.getDeviceId().toString())) {
matchedPlaceholder += 1;
}
}
if (matchedPlaceholder >= minimalPlaceholderFill) {
log.info("当前人脸样本{}已超过最小占位素材{},自动创建任务", face.getFaceUrl(), minimalPlaceholderFill);
taskService.createRenderTask(scenic.getId(), template.getId(), face.getId());
faceMapper.finishedJourney(face.getId());
} else {
log.info("当前人脸样本{}未超过最小占位素材{},未达到自动生成条件", face.getFaceUrl(), minimalPlaceholderFill);
}
}
}
}
}
}
}

View File

@ -0,0 +1,35 @@
package com.ycwl.basic.task;
import com.ycwl.basic.mapper.pc.FaceSampleMapper;
import com.ycwl.basic.mapper.pc.ScenicMapper;
import com.ycwl.basic.model.pc.scenic.req.ScenicReqQuery;
import com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO;
import com.ycwl.basic.service.task.TaskFaceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@EnableScheduling
@Slf4j
public class FaceCleaner {
@Autowired
private ScenicMapper scenicMapper;
@Autowired
private TaskFaceService faceService;
@Scheduled(cron = "0 0 4 * * ?")
public void clean(){
ScenicReqQuery scenicQuery = new ScenicReqQuery();
List<ScenicRespVO> scenicList = scenicMapper.list(scenicQuery);
scenicList.forEach(scenic -> {
log.info("当前景区{},开始删除人脸样本", scenic.getName());
faceService.batchDeleteFace(scenic.getId());
});
}
}

View File

@ -1,176 +0,0 @@
//package com.ycwl.basic.task;
//
//
//import cn.hutool.core.util.StrUtil;
//import com.alibaba.fastjson.JSONArray;
//import com.alibaba.fastjson.JSONObject;
//import com.tu.common.redis.RedisUtils;
//import com.tu.common.utils.ChinaMobileUtil;
//import com.tu.common.utils.DateUtils;
//import com.tu.dao.TuSpaceManageDao;
//import com.tu.dto.GetLiveSteamUrlDTO;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.beans.factory.annotation.Value;
//import org.springframework.core.annotation.Order;
//import org.springframework.scheduling.annotation.EnableScheduling;
//import org.springframework.scheduling.annotation.Scheduled;
//import org.springframework.stereotype.Component;
//
//import javax.annotation.PostConstruct;
//import java.util.*;
//
//import static com.tu.common.utils.ChinaMobileUtil.getTokenBySend;
//import static com.tu.common.utils.ChinaMobileUtil.sendPost;
//
//@Slf4j
//@Component
//@EnableScheduling
//public class GetSpaceChinaMobileLiveSteamJob {
//
// @Autowired
// TuSpaceManageDao tuSpaceManageDao;
// @Autowired
// RedisUtils redisUtils;
// @Autowired
// ChinaMobileUtil chinaMobileUtil;
// @Value("${spring.profiles.active}")
// private String springProfile;
//
// //public static String token = null;
// private static Queue failMsgSubQueue = new LinkedList(); // 保存订阅失败的数据
//
// /**
// * 订阅消息
// */
// @PostConstruct
// public void msgSubscription() {
// new Thread(() -> {
// if (springProfile.equals("prod")) {
// List<GetLiveSteamUrlDTO> liveSteamUrl = tuSpaceManageDao.getLiveSteamUrl(null, 2);
// List<String> deviceIdList = new ArrayList<>();
// for (GetLiveSteamUrlDTO item : liveSteamUrl) {
// String deviceSn = item.getDeviceSn();
// if (deviceSn != null && !deviceSn.equals("")) {
// deviceIdList.add(deviceSn);
//
// if (deviceIdList.size() == 29) {
// // 30条数据为一组分开订阅
// String msg = doMsgSubscription(deviceIdList);
// if (StrUtil.isBlank(msg)) {
// // 消息订阅失败,过一会儿再试试
// List<String> failDeviceIdList = new ArrayList<>();
// Collections.copy(failDeviceIdList, deviceIdList);
// failMsgSubQueue.offer(failDeviceIdList);
// }
// deviceIdList = new ArrayList<>();
// }
//
// }
// }
// doMsgSubscription(deviceIdList);
// }
// }).start();
//
// }
//
//
// /**
// * 每小时扫描订阅失败的消息
// */
// @PostConstruct
// @Scheduled(cron = "0 0 * * * *")
// public void scanQueue() {
// if (failMsgSubQueue.size() > 0) {
// Object poll = failMsgSubQueue.poll();
// if (poll != null) {
// List<String> pollList = (List<String>) poll;
// String msg = doMsgSubscription(pollList);
// if (StrUtil.isBlank(msg)) {
// // 消息订阅失败,过一会儿再试试
// List<String> failDeviceIdList = new ArrayList<>();
// Collections.copy(failDeviceIdList, pollList);
// failMsgSubQueue.offer(failDeviceIdList);
// }
// }
// }
// }
//
//
// @PostConstruct
// @Scheduled(fixedRate = 1000 * 60 * 60 * 2)
// @Order(1)
// public void getToken() {
// if (springProfile.equals("prod")) {
// String tokenBySend = getTokenBySend();
// redisUtils.set("CHINA_MOBILE_TOKEN_KEY_NEW", tokenBySend, RedisUtils.HOUR_TOW_EXPIRE);
// }
// }
//
// @Scheduled(cron = "0 */19 * * * *")
// @PostConstruct
// @Order(2)
// public void setLiveSteam() {
// new Thread(() -> {
// if (springProfile.equals("prod")) {
// List<GetLiveSteamUrlDTO> liveSteamUrl = tuSpaceManageDao.getLiveSteamUrl(null, 2);
// for (GetLiveSteamUrlDTO item : liveSteamUrl) {
// String url = getLiveSteam(item.getDeviceSn());
// if (url != null && !url.equals("")) {
// tuSpaceManageDao.updateLiveSteamUrl(item.getId(), url);
// }
// }
// }
// }).start();
// }
//
// public String getLiveSteam(String deviceId) {
// log.info("【移动】开始获取视频流" + deviceId);
// String url_hls = "https://open.andmu.cn/v3/open/api/device/hls";
// JSONObject bodyParam = new JSONObject();
// bodyParam.put("deviceId", deviceId);
// bodyParam.put("endTime", DateUtils.addDateDays(new Date(), 2).getTime());
// log.info("【移动】bodyParam->{}", bodyParam);
// String res = sendPost(url_hls, bodyParam, chinaMobileUtil.getTempToken());
// log.info("【移动】开始获取视频流结果:-》{}" + res);
// JSONObject jsonObject = JSONObject.parseObject(res);
// String m3u8Url = null;
// if (jsonObject.get("resultCode").toString().equals("000000")) {
// String data = jsonObject.get("data").toString();
// JSONObject jsonData = JSONObject.parseObject(data);
// m3u8Url = jsonData.get("m3u8Url").toString();
// }
// // token已过期
// if (jsonObject.get("resultCode").toString().equals("11504")) {
// // 删除redis的token会重新获取
// redisUtils.delete("CHINA_MOBILE_TOKEN_KEY_NEW");
// getLiveSteam(deviceId);
// }
// return m3u8Url;
// }
//
//
// public static void main(String[] args) {
//
// //List<String> deviceIdList =new ArrayList<>(30);
// //deviceIdList.add("2");
// //deviceIdList.add("1");
// //
// //List<String> failDeviceIdList = new ArrayList<>(deviceIdList);
// //failMsgSubQueue.offer(failDeviceIdList);
// //deviceIdList = new ArrayList<>();
// //List<String> pollList = ( List<String>) failMsgSubQueue.poll();
// //for (String s : pollList) {
// // System.out.println(s);
// //}
// //System.out.println(failMsgSubQueue.poll());
// //System.out.println(failMsgSubQueue.size());
//
//
// //String s = doMsgSubscription(Arrays.asList("040312e7fc9f", "743fc2dae410"));
// //System.out.println(s);
// }
//
//
//
//}

View File

@ -0,0 +1,49 @@
package com.ycwl.basic.task;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.mapper.pc.DeviceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.req.DeviceReqQuery;
import com.ycwl.basic.model.pc.device.resp.DeviceRespVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
@EnableScheduling
@Slf4j
public class VideoPieceCleaner {
@Autowired
private DeviceMapper deviceMapper;
@Scheduled(cron = "0 0 0 * * ?")
public void clean() {
log.info("开始删除视频文件");
List<DeviceRespVO> deviceList = deviceMapper.list(new DeviceReqQuery());
for (DeviceRespVO device : deviceList) {
DeviceConfigEntity config = deviceMapper.getConfigByDeviceId(device.getId());
if (config == null) {
continue;
}
if (config.getStoreExpireDay() == null) {
continue;
}
if (config.getStoreExpireDay() <= 0) {
continue;
}
IDeviceStorageOperator storageOperator = DeviceFactory.getDeviceStorageOperator(null, config);
if (storageOperator == null) {
continue;
}
storageOperator.removeFilesBeforeDate(new Date(System.currentTimeMillis() - config.getStoreExpireDay() * 24 * 60 * 60 * 1000));
log.info("删除视频文件完成");
}
}
}

View File

@ -0,0 +1,370 @@
package com.ycwl.basic.task;
import cn.hutool.core.date.DateUtil;
import com.ycwl.basic.device.DeviceFactory;
import com.ycwl.basic.device.entity.common.FileObject;
import com.ycwl.basic.device.operator.IDeviceStorageOperator;
import com.ycwl.basic.mapper.pc.DeviceMapper;
import com.ycwl.basic.mapper.pc.FaceSampleMapper;
import com.ycwl.basic.mapper.pc.SourceMapper;
import com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity;
import com.ycwl.basic.model.pc.device.entity.DeviceEntity;
import com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO;
import com.ycwl.basic.model.pc.source.entity.SourceEntity;
import com.ycwl.basic.utils.OssUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Component
@EnableScheduling
@Slf4j
public class VideoPieceGetter {
@Autowired
private FaceSampleMapper faceSampleMapper;
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private OssUtil ossUtil;
@Autowired
private SourceMapper sourceMapper;
@Data
public static class Task {
public Long deviceId;
public Long faceSampleId;
public Date createTime;
}
@Data
public static class FfmpegTask {
List<FileObject> fileList;
BigDecimal duration;
BigDecimal offsetStart;
String outputFile;
}
public static LinkedBlockingQueue<Task> queue = new LinkedBlockingQueue<>();
public static void addTask(Task task) {
queue.add(task);
}
@Scheduled(fixedDelay = 5000L)
public void doTask() {
Task task = queue.poll();
if (task == null) {
return;
}
log.info("poll task: {}", task);
FaceSampleRespVO faceSample = faceSampleMapper.getById(task.getFaceSampleId());
DeviceEntity device = deviceMapper.getByDeviceId(task.getDeviceId());
DeviceConfigEntity config = deviceMapper.getConfigByDeviceId(task.getDeviceId());
BigDecimal cutPre = BigDecimal.valueOf(5L);
BigDecimal cutPost = BigDecimal.valueOf(4L);
if (config == null) {
return;
}
// 有配置
if (config.getCutPre() != null) {
cutPre = config.getCutPre();
}
if (config.getCutPost() != null) {
cutPost = config.getCutPost();
}
IDeviceStorageOperator pieceGetter = DeviceFactory.getDeviceStorageOperator(device, config);
if (pieceGetter == null) {
return;
}
BigDecimal duration = cutPre.add(cutPost);
List<FileObject> listByDtRange = pieceGetter.getFileListByDtRange(
new Date(task.getCreateTime().getTime() - cutPre.multiply(BigDecimal.valueOf(1000)).longValue()),
new Date(task.getCreateTime().getTime() + cutPost.multiply(BigDecimal.valueOf(1000)).longValue())
);
if (listByDtRange.isEmpty()) {
queue.add(task);
return;
}
long offset = task.getCreateTime().getTime() - listByDtRange.get(0).getCreateTime().getTime();
FfmpegTask ffmpegTask = new FfmpegTask();
ffmpegTask.setFileList(listByDtRange);
ffmpegTask.setDuration(duration);
ffmpegTask.setOffsetStart(BigDecimal.valueOf(offset, 3));
File outFile = new File(faceSample.getDeviceId().toString() + "_" + faceSample.getId() + ".mp4");
ffmpegTask.setOutputFile(outFile.getAbsolutePath());
boolean result = startFfmpegTask(ffmpegTask);
if (!result) {
log.warn("视频裁切失败");
return;
}
log.info("视频裁切成功");
try {
InputStream inputStream = new FileInputStream(outFile);
String url = ossUtil.uploadFile(inputStream, "user-video-source", outFile.getName());
SourceEntity sourceEntity = new SourceEntity();
sourceEntity.setVideoUrl(url);
sourceEntity.setFaceSampleId(faceSample.getId());
sourceEntity.setScenicId(faceSample.getScenicId());
sourceEntity.setDeviceId(faceSample.getDeviceId());
sourceEntity.setType(1);
sourceMapper.add(sourceEntity);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
public boolean startFfmpegTask(FfmpegTask task) {
boolean result;
if (task.getFileList().size() == 1) {
// 单个文件切割用简单方法
result = runFfmpegForSingleFile(task);
} else {
// 多个文件切割用速度快的
result = runFfmpegForMultipleFile1(task);
}
// 先尝试方法1
if (result) {
return true;
}
log.warn("FFMPEG简易方法失败尝试复杂方法转码");
// 不行再尝试方法二
return runFfmpegForMultipleFile2(task);
}
private boolean runFfmpegForMultipleFile1(FfmpegTask task) {
// 多文件方法一先转换成ts然后合并切割
// 步骤一先转换成ts并行转换
boolean notOk = task.getFileList().stream().map(file -> {
try {
if (file.isNeedDownload() || (!file.getName().endsWith(".ts"))) {
String tmpFile = file.getName() + ".ts";
boolean result = convertMp4ToTs(file, tmpFile);
// 因为是并行转换没法保证顺序就直接存里面
if (result) {
file.setUrl(tmpFile);
} else {
// 失败了务必删除临时文件
(new File(tmpFile)).delete();
}
return result;
} else {
return true;
}
} catch (IOException e) {
log.warn("转码出错");
return false;
}
}).anyMatch(b -> !b);
// 转码进程中出现问题
if (notOk) {
return false;
}
// 步骤二使用concat协议拼接裁切
boolean result;
try {
result = quickVideoCut(
"concat:" + task.getFileList().stream().map(FileObject::getUrl).collect(Collectors.joining("|")),
task.getOffsetStart(), task.getDuration(), task.getOutputFile()
);
} catch (IOException e) {
return false;
}
// 步骤三删除临时文件
task.getFileList().stream().map(FileObject::getUrl).forEach(tmpFile -> {
File f = new File(tmpFile);
if (f.exists() && f.isFile()) {
f.delete();
}
});
return result;
}
private boolean runFfmpegForMultipleFile2(FfmpegTask task) {
// 多文件方法二使用计算资源编码
try {
return slowVideoCut(task.getFileList(), task.getOffsetStart(), task.getDuration(), task.getOutputFile());
} catch (IOException e) {
return false;
}
}
private boolean runFfmpegForSingleFile(FfmpegTask task) {
try {
return quickVideoCut(task.getFileList().get(0).getUrl(), task.getOffsetStart(), task.getDuration(), task.getOutputFile());
} catch (IOException e) {
return false;
}
}
/**
* 把MP4转换成可以拼接的TS文件
*
* @param file MP4文件或ffmpeg支持的输入
* @param outFileName 输出文件路径
* @return 是否成功
* @throws IOException 奇奇怪怪的报错
*/
private boolean convertMp4ToTs(FileObject file, String outFileName) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-i");
ffmpegCmd.add(file.getUrl());
ffmpegCmd.add("-c");
ffmpegCmd.add("copy");
ffmpegCmd.add("-bsf:v");
ffmpegCmd.add("h264_mp4toannexb");
ffmpegCmd.add("-f");
ffmpegCmd.add("mpegts");
ffmpegCmd.add(outFileName);
return handleFfmpegProcess(ffmpegCmd);
}
private boolean convertHevcToTs(FileObject file, String outFileName) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-i");
ffmpegCmd.add(file.getUrl());
ffmpegCmd.add("-c");
ffmpegCmd.add("copy");
ffmpegCmd.add("-bsf:v");
ffmpegCmd.add("hevc_mp4toannexb");
ffmpegCmd.add("-f");
ffmpegCmd.add("mpegts");
ffmpegCmd.add(outFileName);
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 快速切割不产生转码速度快但可能会出现第一帧数据不是I帧导致前面的数据无法使用
*
* @param inputFile 输入文件ffmpeg支持的协议均可
* @param offset 离输入文件开始的偏移
* @param length 输出文件时长
* @param outputFile 输出文件名称
* @return 是否成功
* @throws IOException 奇奇怪怪的报错
*/
private boolean quickVideoCut(String inputFile, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
ffmpegCmd.add("-i");
ffmpegCmd.add(inputFile);
ffmpegCmd.add("-c:v");
ffmpegCmd.add("copy");
ffmpegCmd.add("-an");
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-t");
ffmpegCmd.add(length.toPlainString());
ffmpegCmd.add("-f");
ffmpegCmd.add("mp4");
ffmpegCmd.add(outputFile);
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 转码切割兜底逻辑速度慢但优势成功后转码视频绝对可用
*
* @param inputFiles 输入文件Listffmpeg支持的协议均可
* @param offset 离输入文件开始的偏移
* @param length 输出文件时长
* @param outputFile 输出文件名称
* @return 是否成功
* @throws IOException 奇奇怪怪的报错
*/
private boolean slowVideoCut(List<FileObject> inputFiles, BigDecimal offset, BigDecimal length, String outputFile) throws IOException {
List<String> ffmpegCmd = new ArrayList<>();
ffmpegCmd.add("ffmpeg");
ffmpegCmd.add("-hide_banner");
ffmpegCmd.add("-y");
for (FileObject file : inputFiles) {
ffmpegCmd.add("-i");
ffmpegCmd.add(file.getUrl());
}
// 使用filter_complex做拼接
ffmpegCmd.add("-filter_complex");
ffmpegCmd.add(
IntStream.range(0, inputFiles.size()).mapToObj(i -> "[" + i + ":v]").collect(Collectors.joining("")) +
"concat=n=2:v=1[v]"
);
ffmpegCmd.add("-map");
ffmpegCmd.add("[v]");
ffmpegCmd.add("-preset:v");
ffmpegCmd.add("fast");
ffmpegCmd.add("-an");
// 没有使用copy因为使用了filter_complex
ffmpegCmd.add("-ss");
ffmpegCmd.add(offset.toPlainString());
ffmpegCmd.add("-t");
ffmpegCmd.add(length.toPlainString());
ffmpegCmd.add("-f");
ffmpegCmd.add("mp4");
ffmpegCmd.add(outputFile);
return handleFfmpegProcess(ffmpegCmd);
}
/**
* 运行ffmpeg并确认ffmpeg是否正常退出
*
* @param ffmpegCmd ffmpeg命令
* @return 是否正常退出
*/
private static boolean handleFfmpegProcess(List<String> ffmpegCmd) throws IOException {
Date _startDt = new Date();
log.info("FFMPEG执行命令【{}】", String.join(" ", ffmpegCmd));
ProcessBuilder pb = new ProcessBuilder(ffmpegCmd);
Process ffmpegProcess = pb.start();
// 如果需要额外分析输出之类
if (log.isTraceEnabled()) {
InputStream stderr = ffmpegProcess.getErrorStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stderr));
String line;
while ((line = reader.readLine()) != null) {
log.trace(line);
}
}
try {
// 最长1分钟
boolean exited = ffmpegProcess.waitFor(1, TimeUnit.MINUTES);
if (exited) {
int code = ffmpegProcess.exitValue();
Date _endDt = new Date();
log.info("FFMPEG执行命令结束Code【{}】,耗费时间:【{}ms】命令【{}】", code, _endDt.getTime() - _startDt.getTime(), String.join(" ", ffmpegCmd));
return 0 == code;
} else {
log.error("FFMPEG执行命令没有在1分钟内退出命令【{}】", String.join(" ", ffmpegCmd));
ffmpegProcess.destroy();
return false;
}
} catch (InterruptedException e) {
// TODO: 被中断了
log.warn("FFMPEG执行命令【{}】,被中断了", String.join(" ", ffmpegCmd));
return false;
}
}
}

View File

@ -72,4 +72,14 @@
group by device_id
)b on a.scenic_id = b.scenic_id
</select>
<select id="getConfigByDeviceId" resultType="com.ycwl.basic.model.pc.device.entity.DeviceConfigEntity">
select *
from device_config
where device_id = #{deviceId}
</select>
<select id="getByDeviceId" resultType="com.ycwl.basic.model.pc.device.entity.DeviceEntity">
select *
from device
where id = #{deviceId}
</select>
</mapper>

View File

@ -2,12 +2,15 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.pc.FaceMapper">
<insert id="add">
insert into face(id, score, member_id, face_url, match_sample_ids, first_match_rate, match_result)
values (#{id}, #{score}, #{memberId}, #{faceUrl}, #{matchSampleIds}, #{firstMatchRate}, #{matchResult})
insert into face(id, scenic_id, score, member_id, face_url, match_sample_ids, first_match_rate, match_result)
values (#{id}, #{scenicId}, #{score}, #{memberId}, #{faceUrl}, #{matchSampleIds}, #{firstMatchRate}, #{matchResult})
</insert>
<update id="update">
update face
<set>
<if test="scenicId!= null ">
scenic_id = #{scenicId},
</if>
<if test="memberId!= null ">
member_id = #{memberId},
</if>
@ -29,6 +32,9 @@
</set>
where id = #{id}
</update>
<update id="finishedJourney">
update face set finished_journey = 1 where id = #{id}
</update>
<delete id="deleteById">
delete from face where id = #{id}
</delete>
@ -42,7 +48,7 @@
</if>
</delete>
<select id="list" resultType="com.ycwl.basic.model.pc.face.resp.FaceRespVO">
select id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result
select id, scenic_id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result
from face
<where>
<if test="memberId!= null and memberId!= ''">
@ -66,13 +72,18 @@
</where>
</select>
<select id="getById" resultType="com.ycwl.basic.model.pc.face.resp.FaceRespVO">
select id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result
select id, scenic_id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result, create_at, update_at
from face
where id = #{id}
</select>
<select id="getByMemberId" resultType="com.ycwl.basic.model.pc.face.resp.FaceRespVO">
select id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result
select id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result, create_at, update_at
from face
where member_id = #{memberId}
</select>
<select id="listByScenicIdAndNotFinished" resultType="com.ycwl.basic.model.pc.face.resp.FaceRespVO">
select id, scenic_id, member_id, face_url,score, match_sample_ids, first_match_rate, match_result, create_at, update_at
from face
where scenic_id = #{scenicId} and finished_journey != 1
</select>
</mapper>

View File

@ -85,4 +85,13 @@
from face_sample
where id = #{id}
</select>
<select id="listByIds" resultType="com.ycwl.basic.model.pc.faceSample.resp.FaceSampleRespVO">
select id, scenic_id, device_id, face_url, match_sample_ids, first_match_rate, source_id, match_result,`status`
from face_sample
where id in (
<foreach collection="list" item="id" separator=",">
#{id}
</foreach>
)
</select>
</mapper>

View File

@ -3,6 +3,8 @@
<mapper namespace="com.ycwl.basic.mapper.pc.OrderMapper">
<resultMap id="PCBaseResultMap" type="com.ycwl.basic.model.pc.order.resp.OrderRespVO">
<id column="id" property="id"/>
<result column="scenic_id" property="scenicId"/>
<result column="scenic_name" property="scenicName"/>
<result column="member_id" property="memberId"/>
<result column="nickname" property="memberNickname"/>
<result column="real_name" property="memberRealName"/>
@ -120,10 +122,11 @@
delete from `order` where id = #{id}
</delete>
<select id="list" resultMap="PCBaseResultMap">
select distinct o.id, o.member_id,m.nickname ,m.real_name , o.openid, o.price, pay_price, remark, o.broker_id, o.promo_code,
refund_reason, refund_status, o.`status`, refund_at, pay_at, cancel_at, o.goods_type
select distinct o.id, o.scenic_id, s.name as scenic_name, o.member_id,m.nickname ,m.real_name , o.openid, o.price, pay_price, remark, o.broker_id, o.promo_code,
refund_reason, refund_status, o.`status`, refund_at, pay_at, cancel_at,oi.id oiId, o.goods_type, oi.goods_id
from `order` AS o
left join member m on o.member_id = m.id
left join scenic s on o.scenic_id = s.id
left join order_item oi on o.id = oi.order_id
left join source sr on o.goods_type='2' and oi.goods_id = sr.id
left join video vd on o.goods_type='1' and oi.goods_id = vd.id
@ -131,6 +134,9 @@
<if test="id!= null ">
and o.id = #{id}
</if>
<if test="scenicId != null">
and o.scenic_id = #{scenicId}
</if>
<if test="memberNickname!= null and memberNickname!=''">
and m.nickname like concat('%',#{memberNickname},'%')
</if>
@ -189,11 +195,12 @@
order by o.create_at desc
</select>
<select id="getById" resultMap="PCBaseResultMap">
select o.id, o.member_id, o.openid, o.price, o.pay_price, o.remark, o.broker_id, o.promo_code, o.refund_reason,
select o.id, o.scenic_id, s.name as scenic_name, o.member_id, o.openid, o.price, o.pay_price, o.remark, o.broker_id, o.promo_code, o.refund_reason,
o.refund_status, o.status, o.create_at, o.update_at, o.pay_at, o.cancel_at, o.refund_at,
m.nickname , m.real_name
from `order` o
left join member m on m.id = o.member_id
left join scenic s on o.scenic_id = s.id
left join order_item oi on o.id = oi.order_id
left join template t on o.goods_type='3' and oi.goods_id = t.id
left join source sr on o.goods_type='2' and oi.goods_id = sr.id

View File

@ -2,8 +2,8 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.pc.ScenicMapper">
<insert id="add">
insert into scenic(id, `name`, introduction,cover_url, longitude, latitude, radius, province, city, area, address)
values (#{id}, #{name}, #{introduction}, #{coverUrl},#{longitude}, #{latitude}, #{radius}, #{province}, #{city}, #{area}, #{address})
insert into scenic(id, `name`, introduction, phone, cover_url, longitude, latitude, radius, province, city, area, address)
values (#{id}, #{name}, #{introduction}, #{phone}, #{coverUrl},#{longitude}, #{latitude}, #{radius}, #{province}, #{city}, #{area}, #{address})
</insert>
<insert id="addConfig">
insert into scenic_config(id, scenic_id, start_time, end_time, is_default)
@ -79,16 +79,14 @@
<delete id="deleteById">
delete from scenic where id = #{id}
</delete>
<delete id="deleteConfigByscenicId">
<delete id="deleteConfigByScenicId">
delete from scenic_config where scenic_id = #{scenicId}
</delete>
<select id="list" resultMap="scenicAndConfig">
<select id="list" resultMap="scenic">
select s.id, `name`, `phone`, introduction,cover_url, longitude, latitude, radius, province, city, area, address, `status`, s.create_time, update_time,
c.start_time, c.end_time,
(select scenic_account.account from scenic_account where scenic_account.scenic_id = s.id and scenic_account.is_super = 1 limit 1) as account,
c.is_default, c.create_time createTime2,s.price
s.price
from scenic s
left join scenic_config c on s.id = c.id
<where>
<if test="name!=null and name!=''">
and locate(#{name},`name`) > 0
@ -113,11 +111,10 @@
</if>
</where>
</select>
<select id="getById" resultMap="scenicAndConfig">
<select id="getById" resultMap="scenic">
select s.id, `name`, `phone`, introduction,cover_url, longitude, latitude, radius, province, city, area, address, `status`, s.create_time, update_time,
c.start_time, c.end_time, c.is_default, c.create_time createTime2,s.price
s.price
from scenic s
left join scenic_config c on s.id = c.id
where s.id = #{id}
</select>
<select id="appList" resultType="com.ycwl.basic.model.mobile.scenic.ScenicAppVO">
@ -175,8 +172,13 @@
where `status` = 1
ORDER BY distance ASC
</select>
<select id="getConfig" resultType="com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity">
select *
from scenic_config
where scenic_id = #{scenicId}
</select>
<resultMap id="scenicAndConfig" type="com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO">
<resultMap id="scenic" type="com.ycwl.basic.model.pc.scenic.resp.ScenicRespVO">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="phone" column="phone"/>
@ -194,13 +196,5 @@
<result property="price" column="price"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<association property="scenicConfig" javaType="com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity">
<id property="id" column="c.id"/>
<result property="scenicId" column="s.id"/>
<result property="startTime" column="c.start_time"/>
<result property="endTime" column="c.end_time"/>
<result property="isDefault" column="c.is_default"/>
<result property="createTime" column="createTime2"/>
</association>
</resultMap>
</mapper>

View File

@ -10,6 +10,7 @@
<set>
<if test="workerId!= null">worker_id = #{workerId}, </if>
<if test="memberId!= null">member_id = #{memberId}, </if>
<if test="faceId!= null">face_id = #{faceId}, </if>
<if test="templateId!= null">template_id = #{templateId}, </if>
<if test="scenicId!= null">scenic_id = #{scenicId}, </if>
<if test="taskParams!= null">task_params = #{taskParams}, </if>
@ -26,7 +27,7 @@
</update>
<update id="assignToWorker">
update task
set worker_id = #{workerId}
set worker_id = #{workerId}, status = 2
where id = #{taskId}
</update>
<update id="deassign">

View File

@ -5,6 +5,10 @@
insert into template(id, scenic_id, `name`, pid, is_placeholder, source_url, luts, overlays, audios, cover_url, frame_rate, speed, price)
values (#{id}, #{scenicId}, #{name}, #{pid}, #{isPlaceholder}, #{sourceUrl}, #{luts}, #{overlays}, #{audios}, #{coverUrl}, #{frameRate}, #{speed}, #{price})
</insert>
<insert id="addConfig">
insert into template_config(template_id, is_default, minimal_placeholder_fill)
values (#{templateId}, #{isDefault}, #{minimalPlaceholderFill})
</insert>
<update id="update">
update template
<set>
@ -35,6 +39,14 @@
END)
where id = #{id}
</update>
<update id="updateConfigById">
update template_config
<set>
<if test="isDefault!= null">is_default = #{isDefault}, </if>
<if test="minimalPlaceholderFill!= null">minimal_placeholder_fill = #{minimalPlaceholderFill}, </if>
</set>
where id = #{id}
</update>
<delete id="deleteById">
delete from template where id = #{id}
</delete>
@ -44,6 +56,12 @@
<delete id="deleteByScenicId">
delete from template where scenic_id = #{id}
</delete>
<delete id="deleteConfigByTemplateId">
delete from template_config where template_id = #{id}
</delete>
<delete id="deleteConfigById">
delete from template_config where id = #{id}
</delete>
<select id="list" resultType="com.ycwl.basic.model.pc.template.resp.TemplateRespVO">
select t.id, t.scenic_id, s.name as scenic_name, t.`name`, t.cover_url, t.status, t.create_time, t.update_time
from template t left join scenic s on s.id = t.scenic_id
@ -55,6 +73,7 @@
and scenic_id = #{scenicId}
</if>
<if test="name!= null">and locate(#{name},t.`name`) > 0 </if>
<if test="scenicId!= null">and t.scenic_id = #{scenicId} </if>
<if test="isPlaceholder!= null">and is_placeholder = #{isPlaceholder} </if>
<if test="status!= null">and t.`status` = #{status} </if>
<if test="startTime!= null">and t.create_time &gt;= #{startTime} </if>
@ -71,4 +90,7 @@
from template t left join scenic s on s.id = t.scenic_id
where pid = #{id}
</select>
<select id="getConfig" resultType="com.ycwl.basic.model.pc.template.entity.TemplateConfigEntity">
select * from template_config where template_id = #{templateId}
</select>
</mapper>

View File

@ -49,4 +49,7 @@
left join template t on v.template_id = t.id
where v.id = #{id}
</select>
<select id="findByTaskId" resultType="com.ycwl.basic.model.pc.video.entity.VideoEntity">
select * from video where task_id = #{taskId} limit 1
</select>
</mapper>