diff --git a/src/main/java/com/ycwl/basic/device/DeviceFactory.java b/src/main/java/com/ycwl/basic/device/DeviceFactory.java
new file mode 100644
index 0000000..812cec5
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/DeviceFactory.java
@@ -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;
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/device/IDeviceCommon.java b/src/main/java/com/ycwl/basic/device/IDeviceCommon.java
new file mode 100644
index 0000000..58d1bd3
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/IDeviceCommon.java
@@ -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);
+}
diff --git a/src/main/java/com/ycwl/basic/device/checker/IDeviceStatusChecker.java b/src/main/java/com/ycwl/basic/device/checker/IDeviceStatusChecker.java
new file mode 100644
index 0000000..1235bac
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/checker/IDeviceStatusChecker.java
@@ -0,0 +1,7 @@
+package com.ycwl.basic.device.checker;
+
+import com.ycwl.basic.device.IDeviceCommon;
+
+public interface IDeviceStatusChecker extends IDeviceCommon {
+    boolean checkDeviceStatus();
+}
diff --git a/src/main/java/com/ycwl/basic/device/checker/helper/CommonDeviceChecker.java b/src/main/java/com/ycwl/basic/device/checker/helper/CommonDeviceChecker.java
new file mode 100644
index 0000000..b1a5658
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/checker/helper/CommonDeviceChecker.java
@@ -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];
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/device/checker/impl/AliOssDeviceChecker.java b/src/main/java/com/ycwl/basic/device/checker/impl/AliOssDeviceChecker.java
new file mode 100644
index 0000000..a80c349
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/checker/impl/AliOssDeviceChecker.java
@@ -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;
+        }
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/device/checker/impl/AlwaysOnDeviceChecker.java b/src/main/java/com/ycwl/basic/device/checker/impl/AlwaysOnDeviceChecker.java
new file mode 100644
index 0000000..05ed4b2
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/checker/impl/AlwaysOnDeviceChecker.java
@@ -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;
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/device/entity/alioss/DeviceAliOssConfig.java b/src/main/java/com/ycwl/basic/device/entity/alioss/DeviceAliOssConfig.java
new file mode 100644
index 0000000..0ae272a
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/entity/alioss/DeviceAliOssConfig.java
@@ -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;
+}
diff --git a/src/main/java/com/ycwl/basic/device/entity/common/FileObject.java b/src/main/java/com/ycwl/basic/device/entity/common/FileObject.java
new file mode 100644
index 0000000..27d51bd
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/entity/common/FileObject.java
@@ -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;
+}
diff --git a/src/main/java/com/ycwl/basic/device/entity/local/DeviceLocalConfig.java b/src/main/java/com/ycwl/basic/device/entity/local/DeviceLocalConfig.java
new file mode 100644
index 0000000..6293e1b
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/entity/local/DeviceLocalConfig.java
@@ -0,0 +1,7 @@
+package com.ycwl.basic.device.entity.local;
+
+import lombok.Data;
+
+@Data
+public class DeviceLocalConfig {
+}
diff --git a/src/main/java/com/ycwl/basic/device/enums/DeviceStoreTypeEnum.java b/src/main/java/com/ycwl/basic/device/enums/DeviceStoreTypeEnum.java
new file mode 100644
index 0000000..5d24254
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/enums/DeviceStoreTypeEnum.java
@@ -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;
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/device/operator/IDeviceStorageOperator.java b/src/main/java/com/ycwl/basic/device/operator/IDeviceStorageOperator.java
new file mode 100644
index 0000000..e8b28cd
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/operator/IDeviceStorageOperator.java
@@ -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);
+}
diff --git a/src/main/java/com/ycwl/basic/device/operator/helper/CommonPieceGetter.java b/src/main/java/com/ycwl/basic/device/operator/helper/CommonPieceGetter.java
new file mode 100644
index 0000000..9c73030
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/operator/helper/CommonPieceGetter.java
@@ -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];
+    }
+
+}
diff --git a/src/main/java/com/ycwl/basic/device/operator/impl/AliOssStorageOperator.java b/src/main/java/com/ycwl/basic/device/operator/impl/AliOssStorageOperator.java
new file mode 100644
index 0000000..5f0e5f5
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/operator/impl/AliOssStorageOperator.java
@@ -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);
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/device/operator/impl/LocalStorageOperator.java b/src/main/java/com/ycwl/basic/device/operator/impl/LocalStorageOperator.java
new file mode 100644
index 0000000..d321f85
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/device/operator/impl/LocalStorageOperator.java
@@ -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;
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java
index 86f81df..bb983cb 100644
--- a/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java
+++ b/src/main/java/com/ycwl/basic/mapper/pc/DeviceMapper.java
@@ -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);
 }
diff --git a/src/main/java/com/ycwl/basic/mapper/pc/FaceMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/FaceMapper.java
index d9e95cc..7ebc1ac 100644
--- a/src/main/java/com/ycwl/basic/mapper/pc/FaceMapper.java
+++ b/src/main/java/com/ycwl/basic/mapper/pc/FaceMapper.java
@@ -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);
 }
diff --git a/src/main/java/com/ycwl/basic/mapper/pc/FaceSampleMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/FaceSampleMapper.java
index a282bf0..cf41d0d 100644
--- a/src/main/java/com/ycwl/basic/mapper/pc/FaceSampleMapper.java
+++ b/src/main/java/com/ycwl/basic/mapper/pc/FaceSampleMapper.java
@@ -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);
 }
diff --git a/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java
index f80389f..f417196 100644
--- a/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java
+++ b/src/main/java/com/ycwl/basic/mapper/pc/ScenicMapper.java
@@ -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);
 
diff --git a/src/main/java/com/ycwl/basic/mapper/pc/TemplateMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/TemplateMapper.java
index 1305535..2f76cdb 100644
--- a/src/main/java/com/ycwl/basic/mapper/pc/TemplateMapper.java
+++ b/src/main/java/com/ycwl/basic/mapper/pc/TemplateMapper.java
@@ -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);
 }
diff --git a/src/main/java/com/ycwl/basic/mapper/pc/VideoMapper.java b/src/main/java/com/ycwl/basic/mapper/pc/VideoMapper.java
index e6ab007..124afa1 100644
--- a/src/main/java/com/ycwl/basic/mapper/pc/VideoMapper.java
+++ b/src/main/java/com/ycwl/basic/mapper/pc/VideoMapper.java
@@ -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);
 }
diff --git a/src/main/java/com/ycwl/basic/model/pc/device/entity/DeviceConfigEntity.java b/src/main/java/com/ycwl/basic/model/pc/device/entity/DeviceConfigEntity.java
new file mode 100644
index 0000000..e37d35e
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/model/pc/device/entity/DeviceConfigEntity.java
@@ -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;
+}
diff --git a/src/main/java/com/ycwl/basic/model/pc/face/entity/FaceEntity.java b/src/main/java/com/ycwl/basic/model/pc/face/entity/FaceEntity.java
index 9db8b04..8c74f5a 100644
--- a/src/main/java/com/ycwl/basic/model/pc/face/entity/FaceEntity.java
+++ b/src/main/java/com/ycwl/basic/model/pc/face/entity/FaceEntity.java
@@ -16,6 +16,7 @@ import java.util.Date;
 public class FaceEntity {
     @TableId
     private Long id;
+    private Long scenicId;
     /**
      * 人脸得分
      */
diff --git a/src/main/java/com/ycwl/basic/model/pc/face/req/FaceReqQuery.java b/src/main/java/com/ycwl/basic/model/pc/face/req/FaceReqQuery.java
index e6014e4..cba2db3 100644
--- a/src/main/java/com/ycwl/basic/model/pc/face/req/FaceReqQuery.java
+++ b/src/main/java/com/ycwl/basic/model/pc/face/req/FaceReqQuery.java
@@ -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("用户上传的人脸照片")
diff --git a/src/main/java/com/ycwl/basic/model/pc/order/req/OrderReqQuery.java b/src/main/java/com/ycwl/basic/model/pc/order/req/OrderReqQuery.java
index 55940f4..41c2b53 100644
--- a/src/main/java/com/ycwl/basic/model/pc/order/req/OrderReqQuery.java
+++ b/src/main/java/com/ycwl/basic/model/pc/order/req/OrderReqQuery.java
@@ -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;
diff --git a/src/main/java/com/ycwl/basic/model/pc/order/resp/OrderRespVO.java b/src/main/java/com/ycwl/basic/model/pc/order/resp/OrderRespVO.java
index b61d74d..bf6b9b9 100644
--- a/src/main/java/com/ycwl/basic/model/pc/order/resp/OrderRespVO.java
+++ b/src/main/java/com/ycwl/basic/model/pc/order/resp/OrderRespVO.java
@@ -102,4 +102,6 @@ public class OrderRespVO {
     private Date refundAt;
     @ApiModelProperty("订单明细")
     private List<OrderItemVO> orderItemList;
+    private Long scenicId;
+    private String scenicName;
 }
diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java
index da1e9c6..2e04211 100644
--- a/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java
+++ b/src/main/java/com/ycwl/basic/model/pc/scenic/entity/ScenicConfigEntity.java
@@ -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;
 }
diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicAddOrUpdateReq.java b/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicAddOrUpdateReq.java
index 1cfe0c9..2ff5333 100644
--- a/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicAddOrUpdateReq.java
+++ b/src/main/java/com/ycwl/basic/model/pc/scenic/req/ScenicAddOrUpdateReq.java
@@ -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;
 
diff --git a/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java b/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java
index 6c5eb34..47f1008 100644
--- a/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java
+++ b/src/main/java/com/ycwl/basic/model/pc/scenic/resp/ScenicRespVO.java
@@ -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("镜头数")
diff --git a/src/main/java/com/ycwl/basic/model/pc/source/entity/SourceEntity.java b/src/main/java/com/ycwl/basic/model/pc/source/entity/SourceEntity.java
index 97bf763..e3c9371 100644
--- a/src/main/java/com/ycwl/basic/model/pc/source/entity/SourceEntity.java
+++ b/src/main/java/com/ycwl/basic/model/pc/source/entity/SourceEntity.java
@@ -30,6 +30,10 @@ public class SourceEntity {
      * 所属用户
      */
     private Long memberId;
+    /**
+     * 人脸样本id
+     */
+    private Long faceSampleId;
     /**
      * 原素材类型:1视频,2图像
      */
diff --git a/src/main/java/com/ycwl/basic/model/pc/template/entity/TemplateConfigEntity.java b/src/main/java/com/ycwl/basic/model/pc/template/entity/TemplateConfigEntity.java
new file mode 100644
index 0000000..02d81ff
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/model/pc/template/entity/TemplateConfigEntity.java
@@ -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;
+}
diff --git a/src/main/java/com/ycwl/basic/service/impl/pc/MenuServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/pc/MenuServiceImpl.java
index 2a32b77..f383a33 100644
--- a/src/main/java/com/ycwl/basic/service/impl/pc/MenuServiceImpl.java
+++ b/src/main/java/com/ycwl/basic/service/impl/pc/MenuServiceImpl.java
@@ -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;
  * @Author:longbinbin
  * @Date:2024/12/3 10:16
  */
-
+@Service
 public class MenuServiceImpl implements MenuService {
 
     @Autowired
diff --git a/src/main/java/com/ycwl/basic/service/impl/pc/ScenicServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/pc/ScenicServiceImpl.java
index e67db49..4a2bfce 100644
--- a/src/main/java/com/ycwl/basic/service/impl/pc/ScenicServiceImpl.java
+++ b/src/main/java/com/ycwl/basic/service/impl/pc/ScenicServiceImpl.java
@@ -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 {
diff --git a/src/main/java/com/ycwl/basic/service/impl/task/TaskFaceServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/task/TaskFaceServiceImpl.java
index a0e0476..b38418b 100644
--- a/src/main/java/com/ycwl/basic/service/impl/task/TaskFaceServiceImpl.java
+++ b/src/main/java/com/ycwl/basic/service/impl/task/TaskFaceServiceImpl.java
@@ -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);
diff --git a/src/main/java/com/ycwl/basic/service/impl/task/TaskTaskServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/task/TaskTaskServiceImpl.java
index f72d384..c9fdbe3 100644
--- a/src/main/java/com/ycwl/basic/service/impl/task/TaskTaskServiceImpl.java
+++ b/src/main/java/com/ycwl/basic/service/impl/task/TaskTaskServiceImpl.java
@@ -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
diff --git a/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java b/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java
new file mode 100644
index 0000000..41932b5
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/task/DynamicTaskGenerator.java
@@ -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);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/task/FaceCleaner.java b/src/main/java/com/ycwl/basic/task/FaceCleaner.java
new file mode 100644
index 0000000..14c4b22
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/task/FaceCleaner.java
@@ -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());
+        });
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/task/GetSpaceChinaMobileLiveSteamJob.java b/src/main/java/com/ycwl/basic/task/GetSpaceChinaMobileLiveSteamJob.java
deleted file mode 100644
index 4740bff..0000000
--- a/src/main/java/com/ycwl/basic/task/GetSpaceChinaMobileLiveSteamJob.java
+++ /dev/null
@@ -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);
-//    }
-//
-//
-//
-//}
diff --git a/src/main/java/com/ycwl/basic/task/VideoPieceCleaner.java b/src/main/java/com/ycwl/basic/task/VideoPieceCleaner.java
new file mode 100644
index 0000000..aa44056
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/task/VideoPieceCleaner.java
@@ -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("删除视频文件完成");
+        }
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java
new file mode 100644
index 0000000..43fe52d
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/task/VideoPieceGetter.java
@@ -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 输入文件List,ffmpeg支持的协议均可
+     * @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;
+        }
+    }
+
+}
diff --git a/src/main/resources/mapper/pc/DeviceMapper.xml b/src/main/resources/mapper/pc/DeviceMapper.xml
index d805506..1f3a26b 100644
--- a/src/main/resources/mapper/pc/DeviceMapper.xml
+++ b/src/main/resources/mapper/pc/DeviceMapper.xml
@@ -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>
\ No newline at end of file
diff --git a/src/main/resources/mapper/pc/FaceMapper.xml b/src/main/resources/mapper/pc/FaceMapper.xml
index c74d2f1..b46b00d 100644
--- a/src/main/resources/mapper/pc/FaceMapper.xml
+++ b/src/main/resources/mapper/pc/FaceMapper.xml
@@ -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>
\ No newline at end of file
diff --git a/src/main/resources/mapper/pc/FaceSampleMapper.xml b/src/main/resources/mapper/pc/FaceSampleMapper.xml
index 649539c..0cba491 100644
--- a/src/main/resources/mapper/pc/FaceSampleMapper.xml
+++ b/src/main/resources/mapper/pc/FaceSampleMapper.xml
@@ -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>
\ No newline at end of file
diff --git a/src/main/resources/mapper/pc/OrderMapper.xml b/src/main/resources/mapper/pc/OrderMapper.xml
index 7429bbf..4c0695c 100644
--- a/src/main/resources/mapper/pc/OrderMapper.xml
+++ b/src/main/resources/mapper/pc/OrderMapper.xml
@@ -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
diff --git a/src/main/resources/mapper/pc/ScenicMapper.xml b/src/main/resources/mapper/pc/ScenicMapper.xml
index 306fc4e..153c08c 100644
--- a/src/main/resources/mapper/pc/ScenicMapper.xml
+++ b/src/main/resources/mapper/pc/ScenicMapper.xml
@@ -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>
\ No newline at end of file
diff --git a/src/main/resources/mapper/pc/TaskMapper.xml b/src/main/resources/mapper/pc/TaskMapper.xml
index 7f67abb..23d21dd 100644
--- a/src/main/resources/mapper/pc/TaskMapper.xml
+++ b/src/main/resources/mapper/pc/TaskMapper.xml
@@ -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">
diff --git a/src/main/resources/mapper/pc/TemplateMapper.xml b/src/main/resources/mapper/pc/TemplateMapper.xml
index 8cd6bb9..4d039ac 100644
--- a/src/main/resources/mapper/pc/TemplateMapper.xml
+++ b/src/main/resources/mapper/pc/TemplateMapper.xml
@@ -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>
\ No newline at end of file
diff --git a/src/main/resources/mapper/pc/VideoMapper.xml b/src/main/resources/mapper/pc/VideoMapper.xml
index 518e7ef..c25afa1 100644
--- a/src/main/resources/mapper/pc/VideoMapper.xml
+++ b/src/main/resources/mapper/pc/VideoMapper.xml
@@ -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>
\ No newline at end of file