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 da4a654..e6f8595 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
@@ -70,4 +70,8 @@ public class ScenicConfigEntity {
     private String storeConfigJson;
     private BigDecimal brokerDirectRate;
     private Integer faceDetectHelperThreshold;
+
+    private String watermarkType;
+    private String watermarkScenicText;
+    private String watermarkDtFormat;
 }
diff --git a/src/main/java/com/ycwl/basic/task/ImageWatermarkTask.java b/src/main/java/com/ycwl/basic/task/ImageWatermarkTask.java
new file mode 100644
index 0000000..1e12c33
--- /dev/null
+++ b/src/main/java/com/ycwl/basic/task/ImageWatermarkTask.java
@@ -0,0 +1,168 @@
+package com.ycwl.basic.task;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.extra.qrcode.QrCodeUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.ycwl.basic.image.watermark.ImageWatermarkFactory;
+import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
+import com.ycwl.basic.image.watermark.exception.ImageWatermarkException;
+import com.ycwl.basic.image.watermark.operator.IOperator;
+import com.ycwl.basic.mapper.SourceMapper;
+import com.ycwl.basic.model.pc.face.entity.FaceEntity;
+import com.ycwl.basic.model.pc.mp.MpConfigEntity;
+import com.ycwl.basic.model.pc.scenic.entity.ScenicConfigEntity;
+import com.ycwl.basic.model.pc.scenic.entity.ScenicEntity;
+import com.ycwl.basic.model.pc.source.entity.MemberSourceEntity;
+import com.ycwl.basic.model.pc.source.entity.SourceEntity;
+import com.ycwl.basic.notify.entity.WxMpSrvConfig;
+import com.ycwl.basic.repository.FaceRepository;
+import com.ycwl.basic.repository.ScenicRepository;
+import com.ycwl.basic.storage.StorageFactory;
+import com.ycwl.basic.storage.adapters.IStorageAdapter;
+import com.ycwl.basic.storage.enums.StorageAcl;
+import com.ycwl.basic.utils.WxMpUtil;
+import lombok.AllArgsConstructor;
+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.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+
+@Component
+@EnableScheduling
+@Slf4j
+public class ImageWatermarkTask {
+    @Autowired
+    private FaceRepository faceRepository;
+    @Autowired
+    private ScenicRepository scenicRepository;
+    @Autowired
+    private SourceMapper sourceMapper;
+
+    @Data
+    @AllArgsConstructor
+    public static class Task {
+        public Long memberId;
+        public Long faceId;
+    }
+
+    public static ConcurrentLinkedQueue<Task> queue = new ConcurrentLinkedQueue<>();
+
+    public static void addTask(Long memberId, Long faceId) {
+        queue.add(new Task(memberId, faceId));
+    }
+
+    @Scheduled(fixedRate = 200L)
+    public void doTask() {
+        Task task = queue.poll();
+        if (task == null) {
+            return;
+        }
+        log.info("poll task: {}/{}", task, queue.size());
+        new Thread(() -> {
+            try {
+                runTask(task);
+            } catch (Exception e) {
+                log.error("run task error", e);
+            }
+        }).start();
+    }
+
+    public void runTask(Task task) {
+        // 生成二维码
+        FaceEntity face = faceRepository.getFace(task.faceId);
+        if (face == null) {
+            return;
+        }
+        ScenicEntity scenic = scenicRepository.getScenic(face.getScenicId());
+        if (scenic == null) {
+            return;
+        }
+        ScenicConfigEntity scenicConfig = scenicRepository.getScenicConfig(face.getScenicId());
+        MpConfigEntity scenicMpConfig = scenicRepository.getScenicMpConfig(face.getScenicId());
+        if (scenicMpConfig == null) {
+            return;
+        }
+        List<SourceEntity> sourceEntities = sourceMapper.listImageByFaceRelation(task.memberId, task.faceId);
+        if (sourceEntities == null || sourceEntities.isEmpty()) {
+            return;
+        }
+        File qrcode = new File("qrcode_"+face.getMemberId()+".jpg");
+        try {
+            String urlLink = WxMpUtil.generateUrlLink(scenicMpConfig.getAppId(), scenicMpConfig.getAppSecret(), "pages/videoSynthesis/index", "scenicId=" + face.getScenicId() + "&faceId=" + face.getId());
+            QrCodeUtil.generate(urlLink + "?cq=", 300, 300, qrcode);
+        } catch (Exception e) {
+            log.error("generateWXQRCode error", e);
+            return;
+        }
+        IStorageAdapter adapter = StorageFactory.get(scenicConfig.getStoreType());
+        adapter.loadConfig(JSONObject.parseObject(scenicConfig.getStoreConfigJson(), Map.class));
+        // TODO
+        WatermarkInfo info = new WatermarkInfo();
+        info.setQrcodeFile(qrcode);
+        final ThreadPoolExecutor executor = new ThreadPoolExecutor(16, 128, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(128));
+        for (SourceEntity sourceEntity : sourceEntities) {
+            executor.execute(() -> {
+                String url;
+                try {
+                    IOperator operator = ImageWatermarkFactory.get(scenicConfig.getWatermarkType());
+                    File dstFile = new File(sourceEntity.getId() + ".jpg");
+                    File watermarkedFile = new File(sourceEntity.getId() + "_w.png");
+                    try {
+                        HttpUtil.downloadFile(sourceEntity.getUrl(), dstFile);
+                    } catch (Exception e) {
+                        log.error("downloadFile error", e);
+                        return;
+                    }
+                    info.setOriginalFile(dstFile);
+                    info.setScenicLine(scenicConfig.getWatermarkScenicText());
+                    info.setDatetime(sourceEntity.getCreateTime());
+                    info.setDtFormat(scenicConfig.getWatermarkDtFormat());
+                    info.setWatermarkedFile(watermarkedFile);
+                    try {
+                        operator.process(info);
+                    } catch (ImageWatermarkException e) {
+                        log.error("process error", e);
+                        return;
+                    }
+                    url = adapter.uploadFile(watermarkedFile, "photo_w", watermarkedFile.getName());
+                    adapter.setAcl(StorageAcl.PUBLIC_READ, "photo_w", watermarkedFile.getName());
+                } catch (ImageWatermarkException e) {
+                    // 不支持
+                    url = sourceEntity.getUrl();
+                }
+
+                MemberSourceEntity memberSource = new MemberSourceEntity();
+                memberSource.setMemberId(task.memberId);
+                memberSource.setScenicId(face.getScenicId());
+                memberSource.setFaceId(task.faceId);
+                memberSource.setType(sourceEntity.getType());
+                memberSource.setSourceId(sourceEntity.getId());
+                memberSource.setWaterUrl(url);
+                sourceMapper.updateWaterUrl(memberSource);
+            });
+        }
+        try {
+            Thread.sleep(2000L);
+            log.info("executor等待被结束![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
+            executor.shutdown();
+            executor.awaitTermination(30, TimeUnit.SECONDS);
+            log.info("executor已结束![A:{}/T:{}/F:{}]", executor.getActiveCount(), executor.getTaskCount(), executor.getCompletedTaskCount());
+        } catch (InterruptedException e) {
+            return;
+        }
+    }
+}
diff --git a/src/main/java/com/ycwl/basic/utils/WxMpUtil.java b/src/main/java/com/ycwl/basic/utils/WxMpUtil.java
index 6d7138b..69f34b6 100644
--- a/src/main/java/com/ycwl/basic/utils/WxMpUtil.java
+++ b/src/main/java/com/ycwl/basic/utils/WxMpUtil.java
@@ -14,7 +14,8 @@ import java.io.FileOutputStream;
 import java.util.Date;
 
 public class WxMpUtil {
-    private static final String GET_WXA_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacode?access_token=%s";
+    private static final String GET_WXA_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s";
+    private static final String GET_URL_LICK_URL = "https://api.weixin.qq.com/wxa/generate_urllink?access_token=%s";
 
     private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
     private static String ACCESS_TOKEN = "";
@@ -61,6 +62,33 @@ public class WxMpUtil {
         }
     }
 
+    public static String generateUrlLink(String appId, String appSecret, String path, String query) throws Exception {
+        String url = String.format(GET_URL_LICK_URL, getAccessToken(appId, appSecret));
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        HttpPost httpPost = new HttpPost(url);
+        JSONObject json = new JSONObject();
+        json.put("path", path);
+        json.put("query", query);
+        StringEntity entity = new StringEntity(json.toJSONString(), "utf-8");
+        httpPost.setEntity(entity);
+        httpPost.setHeader("Content-Type", "application/json");
+        try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+            if (response.getStatusLine().getStatusCode() != 200) {
+                expireTime = new Date();
+                throw new Exception("获取小程序码失败");
+            }
+            HttpEntity responseEntity = response.getEntity();
+            if (responseEntity != null) {
+                String responseStr = EntityUtils.toString(responseEntity);
+                JSONObject jsonObject = JSONObject.parseObject(responseStr);
+                return jsonObject.getString("url_link");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
     public static void main(String[] args) throws Exception {
         generateWXAQRCode("wxe7ff26af70bfc37c", "5252fbbc68513bc77b7cc0052b9f9695", "trial", "pages/home/index?scenicId=3955650120997015552", "sxlj_t.jpg");
     }
diff --git a/src/main/resources/mapper/ScenicMapper.xml b/src/main/resources/mapper/ScenicMapper.xml
index b13c589..ba5cc15 100644
--- a/src/main/resources/mapper/ScenicMapper.xml
+++ b/src/main/resources/mapper/ScenicMapper.xml
@@ -107,7 +107,10 @@
             face_detect_helper_threshold=#{faceDetectHelperThreshold},
             store_type=#{storeType},
             store_config_json=#{storeConfigJson},
-            broker_direct_rate=#{brokerDirectRate}
+            broker_direct_rate=#{brokerDirectRate},
+            watermark_type=#{watermarkType},
+            watermark_scenic_text=#{watermarkScenicText},
+            watermark_dt_format=#{watermarkDtFormat}
         </set>
         where id = #{id}
     </update>
diff --git a/src/main/resources/mapper/SourceMapper.xml b/src/main/resources/mapper/SourceMapper.xml
index 6906906..19ba769 100644
--- a/src/main/resources/mapper/SourceMapper.xml
+++ b/src/main/resources/mapper/SourceMapper.xml
@@ -38,6 +38,11 @@
         </set>
         where member_id = #{memberId} and face_id = #{faceId} and `type` = #{type}
     </update>
+    <update id="updateWaterUrl">
+        update member_source
+        set water_url = #{waterUrl}
+        where member_id = #{memberId} and source_id = #{sourceId} and `type` = #{type}
+    </update>
     <delete id="deleteById">
         delete from source where id = #{id}
     </delete>