yuhongqi 2 недель назад
Родитель
Сommit
50dbe6a9fe
24 измененных файлов с 609 добавлено и 52 удалено
  1. 73 0
      fs-admin/src/main/java/com/fs/task/MiniProgramSubTask.java
  2. 5 2
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java
  3. 3 0
      fs-service/src/main/java/com/fs/his/constant/IntegralTaskConstants.java
  4. 1 1
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java
  5. 4 1
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralBatchMapper.java
  6. 2 1
      fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java
  7. 30 0
      fs-service/src/main/java/com/fs/his/param/SignNotifyParam.java
  8. 1 1
      fs-service/src/main/java/com/fs/his/service/IIntegralTaskService.java
  9. 15 0
      fs-service/src/main/java/com/fs/his/service/ISignSubscribeNotifyService.java
  10. 112 42
      fs-service/src/main/java/com/fs/his/service/impl/IntegralTaskServiceImpl.java
  11. 103 0
      fs-service/src/main/java/com/fs/his/service/impl/SignSubscribeNotifyServiceImpl.java
  12. 3 0
      fs-service/src/main/java/com/fs/his/utils/IntegralConfigHelper.java
  13. 4 0
      fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsListVO.java
  14. 3 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsIntegralGoodsScrmMapper.java
  15. 4 0
      fs-service/src/main/java/com/fs/hisStore/param/FsIntegralGoodsListUParam.java
  16. 66 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  17. 5 0
      fs-service/src/main/java/com/fs/live/domain/LiveMiniprogramSubNotifyTask.java
  18. 16 2
      fs-service/src/main/java/com/fs/live/mapper/LiveMiniprogramSubNotifyTaskMapper.java
  19. 2 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  20. 130 0
      fs-service/src/main/java/com/fs/qw/utils/QwExternalContactRemarkMarkHelper.java
  21. 7 0
      fs-service/src/main/resources/db/changelog/changes/20260613-live-user-add-is-del.sql
  22. 4 2
      fs-service/src/main/resources/mapper/his/FsUserIntegralBatchMapper.xml
  23. 3 0
      fs-service/src/main/resources/mapper/live/LiveMiniprogramSubNotifyTaskMapper.xml
  24. 13 0
      fs-user-app/src/main/java/com/fs/app/controller/UserController.java

+ 73 - 0
fs-admin/src/main/java/com/fs/task/MiniProgramSubTask.java

@@ -3,6 +3,7 @@ package com.fs.task;
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.TypeReference;
+import com.fs.his.mapper.FsUserSignMapper;
 import com.fs.live.domain.LiveMiniprogramSubNotifyTask;
 import com.fs.live.mapper.LiveMiniprogramSubNotifyTaskMapper;
 import com.fs.store.enums.MiniAppNotifyTaskStatusEnum;
@@ -34,6 +35,8 @@ public class MiniProgramSubTask {
 
     private final LiveMiniprogramSubNotifyTaskMapper notifyTaskMapper;
 
+    private final FsUserSignMapper fsUserSignMapper;
+
     private WxMaProperties.Config config = null;
 
     @Autowired
@@ -108,4 +111,74 @@ public class MiniProgramSubTask {
         }
 
     }
+
+    /**
+     * 签到提醒订阅消息:发送计划时间已到且今日未签到的用户
+     */
+    public void notifySignSubscribe() {
+        log.info("小程序签到提醒订阅通知定时任务开始");
+        List<LiveMiniprogramSubNotifyTask> pendingData = notifyTaskMapper.selectSignPendingData();
+        if (CollectionUtils.isEmpty(pendingData)) {
+            log.info("小程序签到提醒订阅通知定时任务, 无待处理数据");
+            return;
+        }
+        LocalDateTime now = LocalDateTime.now();
+        for (LiveMiniprogramSubNotifyTask pendingDatum : pendingData) {
+            if (pendingDatum.getUpdateTime() != null && pendingDatum.getUpdateTime().isAfter(now)) {
+                continue;
+            }
+            Long userId = pendingDatum.getFsUserId();
+            if (userId == null) {
+                pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.FAILED.getValue());
+                pendingDatum.setErrorMessage("缺少fsUserId,无法校验签到状态");
+                pendingDatum.setRetryCount(pendingDatum.getRetryCount() + 1);
+                pendingDatum.setUpdateTime(LocalDateTime.now());
+                continue;
+            }
+            if (fsUserSignMapper.getToDayIsSign(userId) > 0) {
+                pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.CANCELED.getValue());
+                pendingDatum.setErrorMessage("今日已签到,跳过发送");
+                pendingDatum.setUpdateTime(LocalDateTime.now());
+                continue;
+            }
+            sendSubscribeMessage(pendingDatum);
+        }
+        notifyTaskMapper.updateBatchById(pendingData);
+        log.info("小程序签到提醒订阅通知定时任务结束, 处理数量={}", pendingData.size());
+    }
+
+    private void sendSubscribeMessage(LiveMiniprogramSubNotifyTask pendingDatum) {
+        pendingDatum.setUpdateTime(LocalDateTime.now());
+        ClientCredGrantReqDTO clientCredGrantReqDTO = new ClientCredGrantReqDTO();
+        clientCredGrantReqDTO.setAppid(config.getAppid());
+        clientCredGrantReqDTO.setSecret(config.getSecret());
+        clientCredGrantReqDTO.setGrant_type("client_credential");
+        try {
+            WeXinAccessTokenDTO stableToken = wechatMiniProgrService.getStableToken(clientCredGrantReqDTO);
+            String accessToken = stableToken.getAccessToken();
+
+            TemplateMessageSendRequestDTO sendRequestDTO = new TemplateMessageSendRequestDTO();
+            sendRequestDTO.setTemplate_id(pendingDatum.getTemplateId());
+            sendRequestDTO.setTouser(pendingDatum.getTouser());
+            sendRequestDTO.setPage(pendingDatum.getPage());
+            TypeReference<Map<String, TemplateMessageSendRequestDTO.TemplateDataValue>> typeReference =
+                    new TypeReference<Map<String, TemplateMessageSendRequestDTO.TemplateDataValue>>() {};
+            sendRequestDTO.setData(JSON.parseObject(pendingDatum.getData(), typeReference));
+            MiniGramSubsMsgResultDTO result = wechatMiniProgrService.sendSubscribeMsg(accessToken, sendRequestDTO);
+            pendingDatum.setRequestBody(JSON.toJSONString(sendRequestDTO));
+            pendingDatum.setResponseBody(JSON.toJSONString(result));
+            if (result.getErrcode() == 0) {
+                pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.SUCCESS.getValue());
+            } else {
+                pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.FAILED.getValue());
+                pendingDatum.setErrorMessage(JSON.toJSONString(result));
+                pendingDatum.setRetryCount(pendingDatum.getRetryCount() + 1);
+            }
+        } catch (Throwable e) {
+            pendingDatum.setStatus(MiniAppNotifyTaskStatusEnum.FAILED.getValue());
+            pendingDatum.setErrorMessage(ExceptionUtils.getStackTrace(e));
+            pendingDatum.setRetryCount(pendingDatum.getRetryCount() + 1);
+            log.error("小程序签到提醒订阅通知异常, taskId={}", pendingDatum.getId(), e);
+        }
+    }
 }

+ 5 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java

@@ -152,8 +152,11 @@ public class FsCourseWatchCommentServiceImpl extends ServiceImpl<FsCourseWatchCo
         fsCourseWatchComment.setCreateTime(DateUtils.getNowDate());
         int i = baseMapper.insertFsCourseWatchComment(fsCourseWatchComment);
         if (i > 0) {
-            integralTaskService.tryGrantCourseCommentIntegral(param.getUserId(), param.getVideoId(), fsCourseWatchComment.getCommentId());
-            return R.ok().put("status", true).put("toast", "评价积分已发放");
+            int success = integralTaskService.tryGrantCourseCommentIntegral(param.getUserId(), param.getVideoId(), fsCourseWatchComment.getCommentId());
+            if (1 == success) {
+                return R.ok().put("status", true).put("toast", "评价积分已发放");
+            }
+            return R.ok().put("status", true);
         } else {
             return R.error();
         }

+ 3 - 0
fs-service/src/main/java/com/fs/his/constant/IntegralTaskConstants.java

@@ -22,4 +22,7 @@ public final class IntegralTaskConstants {
     public static final String REDIS_BROWSE_PREFIX = "integral:browse:";
     public static final String REDIS_SIGN_POPUP_PREFIX = "integral:sign:popup:";
     public static final String REDIS_TASK_DONE_PREFIX = "integral:task:done:";
+
+    /** 新手任务完成后多少小时隐藏 */
+    public static final int NEWBIE_COMPLETED_HIDE_HOURS = 24;
 }

+ 1 - 1
fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java

@@ -68,7 +68,7 @@ public interface FsIntegralGoodsMapper
      */
     public int deleteFsIntegralGoodsByGoodsIds(Long[] goodsIds);
 
-    @Select({"<script> select goods_id,bar_code, img_url, images, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, descs, create_time from fs_integral_goods" +
+    @Select({"<script> select goods_id,bar_code, img_url, images, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, visible_scope, descs, create_time from fs_integral_goods" +
             "<where>  \n" +
             "            <if test=\"goodsName != null  and goodsName != ''\"> and goods_name like concat('%', #{goodsName}, '%')</if>\n" +
             "            <if test=\"goodsType != null \"> and goods_type = #{goodsType}</if>\n" +

+ 4 - 1
fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralBatchMapper.java

@@ -10,7 +10,10 @@ public interface FsUserIntegralBatchMapper {
 
     int insertFsUserIntegralBatch(FsUserIntegralBatch batch);
 
-    List<FsUserIntegralBatch> selectExpiredBatches(@Param("now") Date now, @Param("limit") int limit);
+    List<FsUserIntegralBatch> selectExpiredBatches(@Param("now") Date now,
+                                                   @Param("launchDate") Date launchDate,
+                                                   @Param("expireDays") int expireDays,
+                                                   @Param("limit") int limit);
 
     int updateRemain(@Param("batchId") Long batchId, @Param("remain") Long remain);
 }

+ 2 - 1
fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java

@@ -31,6 +31,7 @@ public class FsIntegralGoodsListUParam  implements Serializable {
 
     private String appId;
 
-    /** 可见端 app / mini */
+    /** 可见端 app / mini(兼容旧参数) */
+    @ApiModelProperty(value = "可见端 app/mini,兼容旧参数 platform")
     private String platform;
 }

+ 30 - 0
fs-service/src/main/java/com/fs/his/param/SignNotifyParam.java

@@ -0,0 +1,30 @@
+package com.fs.his.param;
+
+import lombok.Data;
+
+import java.util.HashMap;
+
+/**
+ * 签到提醒小程序订阅参数(模板与内容由前端传入)
+ */
+@Data
+public class SignNotifyParam {
+
+    private String templateId;
+
+    private String maOpenId;
+
+    private Long userId;
+
+    private String appId;
+
+    /** 点击消息跳转页面,默认任务中心 */
+    private String page;
+
+    /**
+     * 计划发送时间 yyyy-MM-dd HH:mm:ss,不传则默认次日 08:00
+     */
+    private String sendTime;
+
+    private HashMap<String, String> data;
+}

+ 1 - 1
fs-service/src/main/java/com/fs/his/service/IIntegralTaskService.java

@@ -17,7 +17,7 @@ public interface IIntegralTaskService {
 
     void tryGrantFirstPurchase(Long userId, Long orderId);
 
-    void tryGrantCourseCommentIntegral(Long userId, Long videoId, Long commentId);
+    int tryGrantCourseCommentIntegral(Long userId, Long videoId, Long commentId);
 
     void processExpiredIntegralBatches();
 

+ 15 - 0
fs-service/src/main/java/com/fs/his/service/ISignSubscribeNotifyService.java

@@ -0,0 +1,15 @@
+package com.fs.his.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.his.param.SignNotifyParam;
+
+/**
+ * 签到提醒小程序订阅
+ */
+public interface ISignSubscribeNotifyService {
+
+    /**
+     * 用户订阅签到提醒,写入待发送任务
+     */
+    R subNotifySign(SignNotifyParam param);
+}

+ 112 - 42
fs-service/src/main/java/com/fs/his/service/impl/IntegralTaskServiceImpl.java

@@ -10,12 +10,10 @@ import com.fs.his.constant.IntegralTaskConstants;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserIntegralBatch;
 import com.fs.his.domain.FsUserIntegralLogs;
-import com.fs.his.domain.FsUserNewTask;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.mapper.FsUserIntegralBatchMapper;
 import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.mapper.FsUserMapper;
-import com.fs.his.mapper.FsUserNewTaskMapper;
 import com.fs.his.param.FsUserAddIntegralTemplateParam;
 import com.fs.his.param.IntegralBrowseCompleteParam;
 import com.fs.his.param.IntegralBrowseStartParam;
@@ -31,6 +29,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
 
 import java.time.Instant;
 import java.time.LocalDate;
@@ -50,8 +49,6 @@ public class IntegralTaskServiceImpl implements IIntegralTaskService {
     @Autowired
     private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
     @Autowired
-    private FsUserNewTaskMapper fsUserNewTaskMapper;
-    @Autowired
     private IFsUserIntegralLogsService userIntegralLogsService;
     @Autowired
     private IFsUserSignService userSignService;
@@ -59,6 +56,8 @@ public class IntegralTaskServiceImpl implements IIntegralTaskService {
     private RedisCache redisCache;
     @Autowired
     private FsUserIntegralBatchMapper fsUserIntegralBatchMapper;
+    @Autowired
+    private TransactionTemplate transactionTemplate;
 
     @Override
     public R getTaskCenter(Long userId, String platform) {
@@ -177,15 +176,15 @@ public class IntegralTaskServiceImpl implements IIntegralTaskService {
 
     @Override
     @Transactional
-    public void tryGrantCourseCommentIntegral(Long userId, Long videoId, Long commentId) {
+    public int tryGrantCourseCommentIntegral(Long userId, Long videoId, Long commentId) {
         if (userId == null || videoId == null) {
-            return;
+            return 0;
         }
         List<FsUserIntegralLogs> exists = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(
                 userId, FsUserIntegralLogTypeEnum.TYPE_33.getValue(), null);
         for (FsUserIntegralLogs item : exists) {
             if (videoId.toString().equals(item.getBusinessId())) {
-                return;
+                return 0;
             }
         }
         FsUserAddIntegralTemplateParam param = new FsUserAddIntegralTemplateParam();
@@ -194,42 +193,77 @@ public class IntegralTaskServiceImpl implements IIntegralTaskService {
         param.setBusinessId(videoId.toString());
         param.setRemark(commentId != null ? commentId.toString() : null);
         userIntegralLogsService.addIntegralTemplate(param);
+        return 1;
     }
 
     @Override
-    @Transactional
     public void processExpiredIntegralBatches() {
-        List<FsUserIntegralBatch> batches = fsUserIntegralBatchMapper.selectExpiredBatches(new Date(), 500);
-        for (FsUserIntegralBatch batch : batches) {
-            if (batch.getRemain() == null || batch.getRemain() <= 0) {
-                continue;
+        IntegralConfig config = IntegralConfigHelper.load(configService);
+        int expireDays = config.getIntegralExpireDays();
+        Date launchDate = DateUtils.parseDate(config.getIntegralExpireLaunchDate());
+        if (launchDate == null) {
+            launchDate = DateUtils.parseDate("2026-07-10");
+        }
+        Date now = new Date();
+        int batchSize = 500;
+        int totalExpired = 0;
+        while (true) {
+            List<FsUserIntegralBatch> batches = fsUserIntegralBatchMapper.selectExpiredBatches(
+                    now, launchDate, expireDays, batchSize);
+            if (batches == null || batches.isEmpty()) {
+                break;
             }
-            FsUser user = fsUserMapper.selectFsUserByUserId(batch.getUserId());
-            if (user == null) {
-                continue;
+            for (FsUserIntegralBatch batch : batches) {
+                try {
+                    transactionTemplate.execute(status -> {
+                        expireIntegralBatch(batch);
+                        return null;
+                    });
+                    totalExpired++;
+                } catch (Exception e) {
+                    log.error("积分批次过期处理失败,batchId={},userId={}", batch.getBatchId(), batch.getUserId(), e);
+                }
             }
-            long deduct = batch.getRemain();
-            long newBalance = Math.max(0, user.getIntegral() - deduct);
-            FsUser userMap = new FsUser();
-            userMap.setUserId(batch.getUserId());
-            userMap.setIntegral(newBalance);
-            fsUserMapper.updateFsUser(userMap);
-
-            fsUserIntegralBatchMapper.updateRemain(batch.getBatchId(), 0L);
-
-            FsUserIntegralLogs logs = new FsUserIntegralLogs();
-            logs.setIntegral(-deduct);
-            logs.setUserId(batch.getUserId());
-            logs.setBalance(newBalance);
-            logs.setLogType(FsUserIntegralLogTypeEnum.TYPE_7.getValue());
-            logs.setBusinessId(batch.getBatchId().toString());
-            logs.setCreateTime(new Date());
-            logs.setRemark("积分过期失效");
-            fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
+            if (batches.size() < batchSize) {
+                break;
+            }
+        }
+        if (totalExpired > 0) {
+            log.info("积分过期处理完成,共失效 {} 笔批次,规则:{} 之后获得且超过 {} 天",
+                    totalExpired, config.getIntegralExpireLaunchDate(), expireDays);
         }
     }
 
+    private void expireIntegralBatch(FsUserIntegralBatch batch) {
+        if (batch.getRemain() == null || batch.getRemain() <= 0) {
+            return;
+        }
+        FsUser user = fsUserMapper.selectFsUserByUserId(batch.getUserId());
+        if (user == null) {
+            return;
+        }
+        long deduct = batch.getRemain();
+        long newBalance = Math.max(0, user.getIntegral() - deduct);
+        FsUser userMap = new FsUser();
+        userMap.setUserId(batch.getUserId());
+        userMap.setIntegral(newBalance);
+        fsUserMapper.updateFsUser(userMap);
+
+        fsUserIntegralBatchMapper.updateRemain(batch.getBatchId(), 0L);
+
+        FsUserIntegralLogs logs = new FsUserIntegralLogs();
+        logs.setIntegral(-deduct);
+        logs.setUserId(batch.getUserId());
+        logs.setBalance(newBalance);
+        logs.setLogType(FsUserIntegralLogTypeEnum.TYPE_7.getValue());
+        logs.setBusinessId(batch.getBatchId().toString());
+        logs.setCreateTime(new Date());
+        logs.setRemark("积分过期失效");
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(logs);
+    }
+
     @Override
+    @Transactional
     public void markSignPopupShown(Long userId) {
         String popupKey = IntegralTaskConstants.REDIS_SIGN_POPUP_PREFIX + userId + ":" + LocalDate.now();
         redisCache.setCacheObject(popupKey, "1", 1, TimeUnit.DAYS);
@@ -313,21 +347,57 @@ public class IntegralTaskServiceImpl implements IIntegralTaskService {
     }
 
     private void hideCompletedNewbieTasks(List<Map<String, Object>> tasks, Long userId) {
-        FsUserNewTask newTask = fsUserNewTaskMapper.selectFsUserNewTaskByUserId(userId);
-        if (newTask == null || newTask.getCreateTime() == null) {
-            return;
-        }
-        long hours = ChronoUnit.HOURS.between(newTask.getCreateTime().toInstant(), Instant.now());
-        if (hours < 24) {
-            return;
-        }
+        Instant now = Instant.now();
         for (Map<String, Object> task : tasks) {
-            if (Integer.valueOf(2).equals(task.get("status"))) {
+            if (!Integer.valueOf(2).equals(task.get("status"))) {
+                continue;
+            }
+            Integer logType = resolveNewbieLogType(task.get("code"));
+            if (logType == null) {
+                continue;
+            }
+            Date completedAt = getTaskCompletedTime(userId, logType);
+            if (completedAt == null) {
+                continue;
+            }
+            task.put("completedAt", DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, completedAt));
+            long hours = ChronoUnit.HOURS.between(completedAt.toInstant(), now);
+            if (hours >= IntegralTaskConstants.NEWBIE_COMPLETED_HIDE_HOURS) {
                 task.put("hidden", true);
             }
         }
     }
 
+    private Integer resolveNewbieLogType(Object code) {
+        if (code == null) {
+            return null;
+        }
+        switch (code.toString()) {
+            case IntegralTaskConstants.TASK_REGISTER:
+                return FsUserIntegralLogTypeEnum.TYPE_20.getValue();
+            case IntegralTaskConstants.TASK_DOWNLOAD_APP:
+                return FsUserIntegralLogTypeEnum.TYPE_28.getValue();
+            case IntegralTaskConstants.TASK_FIRST_PURCHASE:
+                return FsUserIntegralLogTypeEnum.TYPE_32.getValue();
+            case IntegralTaskConstants.TASK_INVITED:
+                return FsUserIntegralLogTypeEnum.TYPE_19.getValue();
+            default:
+                return null;
+        }
+    }
+
+    private Date getTaskCompletedTime(Long userId, int logType) {
+        List<FsUserIntegralLogs> logs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, logType, null);
+        if (logs == null || logs.isEmpty()) {
+            return null;
+        }
+        return logs.stream()
+                .map(FsUserIntegralLogs::getCreateTime)
+                .filter(Objects::nonNull)
+                .min(Date::compareTo)
+                .orElse(null);
+    }
+
     private boolean hasLogType(Long userId, int logType) {
         List<FsUserIntegralLogs> logs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, logType, null);
         return logs != null && !logs.isEmpty();

+ 103 - 0
fs-service/src/main/java/com/fs/his/service/impl/SignSubscribeNotifyServiceImpl.java

@@ -0,0 +1,103 @@
+package com.fs.his.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsUserWx;
+import com.fs.his.mapper.FsUserWxMapper;
+import com.fs.his.param.SignNotifyParam;
+import com.fs.his.service.ISignSubscribeNotifyService;
+import com.fs.live.domain.LiveMiniprogramSubNotifyTask;
+import com.fs.live.mapper.LiveMiniprogramSubNotifyTaskMapper;
+import com.fs.store.dto.TemplateMessageSendRequestDTO;
+import com.fs.store.enums.MiniAppNotifyTaskStatusEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class SignSubscribeNotifyServiceImpl implements ISignSubscribeNotifyService {
+
+    private static final String TASK_NAME_SIGN = "签到提醒";
+    private static final String DEFAULT_PAGE = "pages/user/integral/points";
+    private static final DateTimeFormatter SEND_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Autowired
+    private LiveMiniprogramSubNotifyTaskMapper liveMiniprogramSubNotifyTaskMapper;
+
+    @Autowired
+    private FsUserWxMapper fsUserWxMapper;
+
+    @Override
+    public R subNotifySign(SignNotifyParam param) {
+        if (StringUtils.isEmpty(param.getAppId())) {
+            return R.error("签到订阅失败:appId为空");
+        }
+        if (StringUtils.isEmpty(param.getTemplateId())) {
+            return R.error("签到订阅失败:templateId为空");
+        }
+        if (param.getUserId() == null) {
+            return R.error("签到订阅失败:userId为空");
+        }
+        if (param.getData() == null || param.getData().isEmpty()) {
+            return R.error("签到订阅失败:模板内容data为空");
+        }
+
+        Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery()
+                .eq(FsUserWx::getFsUserId, param.getUserId())
+                .eq(FsUserWx::getAppId, param.getAppId());
+        FsUserWx fsUserWx = fsUserWxMapper.selectOne(queryWrapper);
+
+        String maOpenId = fsUserWx == null ? param.getMaOpenId() : fsUserWx.getOpenId();
+        if (StringUtils.isEmpty(maOpenId)) {
+            log.error("用户没有绑定微信,无法订阅签到提醒: userId={}", param.getUserId());
+            return R.error("未绑定微信,无法订阅签到提醒");
+        }
+
+        liveMiniprogramSubNotifyTaskMapper.cancelPendingSignNotifyByUserId(param.getUserId());
+
+        LiveMiniprogramSubNotifyTask notifyTask = new LiveMiniprogramSubNotifyTask();
+        notifyTask.setTaskName(TASK_NAME_SIGN);
+        notifyTask.setTemplateId(param.getTemplateId());
+        notifyTask.setTouser(maOpenId);
+        notifyTask.setFsUserId(param.getUserId());
+        notifyTask.setPage(StringUtils.isEmpty(param.getPage()) ? DEFAULT_PAGE : param.getPage());
+        notifyTask.setStatus(MiniAppNotifyTaskStatusEnum.WAITING.getValue());
+        notifyTask.setRetryCount(0);
+        notifyTask.setMaxRetries(3);
+        notifyTask.setCreateTime(LocalDateTime.now());
+        notifyTask.setUpdateTime(resolvePlanSendTime(param.getSendTime()));
+
+        Map<String, Object> requestParams = new HashMap<>();
+        requestParams.put("appId", param.getAppId());
+        notifyTask.setRequestParams(JSON.toJSONString(requestParams));
+
+        Map<String, TemplateMessageSendRequestDTO.TemplateDataValue> data = new HashMap<>();
+        param.getData().forEach((key, value) -> {
+            TemplateMessageSendRequestDTO.TemplateDataValue dataValue = new TemplateMessageSendRequestDTO.TemplateDataValue();
+            dataValue.setValue(value);
+            data.put(key, dataValue);
+        });
+        notifyTask.setData(JSON.toJSONString(data));
+
+        liveMiniprogramSubNotifyTaskMapper.insert(notifyTask);
+        return R.ok("success");
+    }
+
+    private LocalDateTime resolvePlanSendTime(String sendTime) {
+        if (StringUtils.isNotEmpty(sendTime)) {
+            return LocalDateTime.parse(sendTime, SEND_TIME_FORMATTER);
+        }
+        return LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.of(8, 0, 0));
+    }
+}

+ 3 - 0
fs-service/src/main/java/com/fs/his/utils/IntegralConfigHelper.java

@@ -52,6 +52,9 @@ public final class IntegralConfigHelper {
         if (config.getIntegralExpireDays() == null) {
             config.setIntegralExpireDays(90);
         }
+        if (StringUtils.isBlank(config.getIntegralExpireLaunchDate())) {
+            config.setIntegralExpireLaunchDate("2026-07-10");
+        }
         if (config.getTaskDisplayScope() == null || config.getTaskDisplayScope().isEmpty()) {
             config.setTaskDisplayScope(IntegralTaskScopeHelper.defaultTaskDisplayScope());
         } else {

+ 4 - 0
fs-service/src/main/java/com/fs/his/vo/FsIntegralGoodsListVO.java

@@ -51,6 +51,10 @@ public class FsIntegralGoodsListVO {
     /** 库存 */
     @Excel(name = "库存")
     private Long stock;
+
+    /** 可见范围 app,mini */
+    private String visibleScope;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsIntegralGoodsScrmMapper.java

@@ -86,6 +86,9 @@ public interface FsIntegralGoodsScrmMapper
             "<if test = 'maps.appId != null and maps.appId != \" \" '> " +
             " and ((FIND_IN_SET(#{maps.appId}, g.app_ids) > 0)) " +
             "</if>"+
+            "<if test = 'maps.platform != null and maps.platform != \"\" '> " +
+            " and ((FIND_IN_SET(#{maps.platform}, g.visible_scope) > 0) or g.visible_scope is null or g.visible_scope = '') " +
+            "</if>"+
             " order by g.goods_id desc "+
             "</script>"})
     List<FsIntegralGoodsListUVO> selectFsIntegralGoodsListUVO(@Param("maps")FsIntegralGoodsListUParam param);

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsIntegralGoodsListUParam.java

@@ -32,4 +32,8 @@ public class FsIntegralGoodsListUParam extends BaseQueryParam implements Seriali
     private Integer status;
 
     private String appId;
+
+    /** 可见端 app / mini(兼容旧参数) */
+    @ApiModelProperty(value = "可见端 app/mini,兼容旧参数 platform")
+    private String platform;
 }

+ 66 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -129,6 +129,10 @@ import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.utils.QwExternalContactRemarkMarkHelper;
+import com.fs.qwApi.domain.QwExternalContactRemarkResult;
+import com.fs.qwApi.param.QwExternalContactRemarkParam;
+import com.fs.qwApi.service.QwApiService;
 import com.fs.store.domain.FsStoreDelivers;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
@@ -518,6 +522,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private QwUserMapper qwUserMapper;
     @Autowired
+    private QwApiService qwApiService;
+    @Autowired
     private FastGptRoleMapper fastGptRoleMapper;
     @Autowired
     private WxWorkService wxWorkService;
@@ -7821,6 +7827,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             return;
         }
         for (QwExternalContact externalContact : qwExternalContact) {
+            updatePurchaseRemarkForPaySuccess(externalContact);
             Long qwUserId = externalContact.getQwUserId();
             if (qwUserId == null) {
                 continue;
@@ -7863,6 +7870,65 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
     }
 
+    /**
+     * 支付成功:为订单对应销售的外部联系人追加购买备注(*MMdd购),与完课备注独立互不影响。
+     */
+    private void updatePurchaseRemarkForPaySuccess(QwExternalContact externalContact) {
+        // 下面的 输出错误日志 需要删除掉 todo yhq 先放一个星期观察情况 20260706
+        if (externalContact == null || externalContact.getId() == null || externalContact.getQwUserId() == null) {
+            log.error("支付成功购买备注更新失败,外部联系人不存在");
+            return;
+        }
+        QwExternalContact latestContact = qwExternalContactMapper.selectQwExternalContactById(externalContact.getId());
+        if (latestContact == null) {
+            log.error("支付成功购买备注更新失败,外部联系人不存在");
+            return;
+        }
+        QwUser qwUser = qwUserMapper.selectQwUserById(latestContact.getQwUserId());
+        if (qwUser == null || QwExternalContactRemarkMarkHelper.shouldSkipAutoRemark(qwUser.getIsSendMsg())) {
+            log.error("支付成功购买备注更新失败,用户无发送欢迎语权限,qwUserId={}", latestContact.getQwUserId());
+            return;
+        }
+        int isSendMsg = qwUser.getIsSendMsg() == null ? 1 : qwUser.getIsSendMsg();
+        String oldRemark = latestContact.getRemark();
+        if (StringUtils.isEmpty(oldRemark)) {
+            oldRemark = latestContact.getName();
+        }
+        String newRemark = QwExternalContactRemarkMarkHelper.buildPurchaseRemark(oldRemark, latestContact.getName(), isSendMsg);
+        if (StringUtils.equals(oldRemark, newRemark)) {
+            log.error("支付成功购买备注无需更新,externalContactId={},remark={}", latestContact.getId(), oldRemark);
+            return;
+        }
+        if (StringUtils.isEmpty(latestContact.getUserId())
+                || StringUtils.isEmpty(latestContact.getExternalUserId())
+                || StringUtils.isEmpty(latestContact.getCorpId())) {
+            log.error("支付成功购买备注失败,外部联系人信息不完整,externalContactId={}", latestContact.getId());
+            return;
+        }
+        QwExternalContactRemarkParam remarkParam = new QwExternalContactRemarkParam();
+        remarkParam.setRemark(newRemark);
+        remarkParam.setUserid(latestContact.getUserId());
+        remarkParam.setExternal_userid(latestContact.getExternalUserId());
+        try {
+            QwExternalContactRemarkResult result = qwApiService.externalcontactRemark(remarkParam, latestContact.getCorpId());
+            if (result != null && result.getErrcode() == 0) {
+                QwExternalContact contactNew = new QwExternalContact();
+                contactNew.setId(latestContact.getId());
+                contactNew.setRemark(newRemark);
+                qwExternalContactMapper.updateQwExternalContact(contactNew);
+                log.error("支付成功添加购买备注:{}|{}|{}|{}|{} -> {}",
+                        latestContact.getName(), latestContact.getExternalUserId(),
+                        latestContact.getCorpId(), latestContact.getUserId(), oldRemark, newRemark);
+            } else {
+                log.error("支付成功购买备注失败:{}|{}|{} -> {},原因:{}",
+                        latestContact.getExternalUserId(), latestContact.getUserId(),
+                        oldRemark, newRemark, result != null ? result.getErrmsg() : "null");
+            }
+        } catch (Exception e) {
+            log.error("支付成功购买备注异常,externalContactId={}", latestContact.getId(), e);
+        }
+    }
+
     private boolean sendWxImageMsg(Long sendId, String uuid, Long serverId, String imageUrl) {
         WxwUploadCdnLinkImgDTO uploadDto = new WxwUploadCdnLinkImgDTO();
         uploadDto.setUuid(uuid);

+ 5 - 0
fs-service/src/main/java/com/fs/live/domain/LiveMiniprogramSubNotifyTask.java

@@ -31,6 +31,11 @@ public class LiveMiniprogramSubNotifyTask {
      */
     private String touser;
 
+    /**
+     * fs_user.user_id
+     */
+    private Long fsUserId;
+
     /**
      * 点击消息跳转的页面路径(可选)
      */

+ 16 - 2
fs-service/src/main/java/com/fs/live/mapper/LiveMiniprogramSubNotifyTaskMapper.java

@@ -29,9 +29,9 @@ public interface LiveMiniprogramSubNotifyTaskMapper {
      * @param task 任务实体
      * @return
      */
-    @Insert("INSERT INTO fs_miniprogram_sub_notify_task (task_name, template_id, touser, page, `data`, status, " +
+    @Insert("INSERT INTO fs_miniprogram_sub_notify_task (task_name, template_id, touser, fs_user_id, page, `data`, status, " +
             "retry_count, max_retries, request_params,request_body, response_body, error_message, create_time, update_time) " +
-            "VALUES (#{taskName}, #{templateId}, #{touser}, #{page}, #{data}, #{status}, #{retryCount}, " +
+            "VALUES (#{taskName}, #{templateId}, #{touser}, #{fsUserId}, #{page}, #{data}, #{status}, #{retryCount}, " +
             "#{maxRetries}, #{requestParams},#{requestBody}, #{responseBody}, #{errorMessage}, #{createTime}, #{updateTime})")
     @Options(useGeneratedKeys = true, keyProperty = "id")
     int insert(LiveMiniprogramSubNotifyTask task);
@@ -87,4 +87,18 @@ public interface LiveMiniprogramSubNotifyTaskMapper {
     @Select("SELECT * FROM fs_miniprogram_sub_notify_task WHERE retry_count<3 and status in (0,3) " +
             "and task_name = '直播间预约提醒'" )
     List<LiveMiniprogramSubNotifyTask> selectLivePendingData();
+
+    /**
+     * 查询待发送的签到提醒订阅
+     */
+    @Select("SELECT * FROM fs_miniprogram_sub_notify_task WHERE retry_count < 3 AND status IN (0,3) " +
+            "AND task_name = '签到提醒'")
+    List<LiveMiniprogramSubNotifyTask> selectSignPendingData();
+
+    /**
+     * 取消用户未完成的签到提醒任务(重新订阅时)
+     */
+    @Update("UPDATE fs_miniprogram_sub_notify_task SET status = 4, error_message = '用户重新订阅', update_time = NOW() " +
+            "WHERE task_name = '签到提醒' AND status IN (0,3) AND fs_user_id = #{userId}")
+    int cancelPendingSignNotifyByUserId(@Param("userId") Long userId);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -581,6 +581,8 @@ public class LiveServiceImpl implements ILiveService
 
         notifyTask.setTouser(maOpenId);
 
+        notifyTask.setFsUserId(userId);
+
         notifyTask.setCreateTime(LocalDateTime.now());
         // 状态等待执行
         notifyTask.setStatus(MiniAppNotifyTaskStatusEnum.WAITING.getValue());

+ 130 - 0
fs-service/src/main/java/com/fs/qw/utils/QwExternalContactRemarkMarkHelper.java

@@ -0,0 +1,130 @@
+package com.fs.qw.utils;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 企微外部联系人备注标记:完课(*MMdd完) 与 购买(*MMdd购) 独立维护,互不影响。
+ */
+public final class QwExternalContactRemarkMarkHelper {
+
+    private static final int REMARK_MAX_LENGTH = 20;
+    private static final Pattern FINISH_MARK_PATTERN = Pattern.compile("\\*(\\d{1,4})完");
+    private static final Pattern PURCHASE_MARK_PATTERN = Pattern.compile("\\*(\\d{1,4})购");
+
+    private QwExternalContactRemarkMarkHelper() {
+    }
+
+    public static boolean shouldSkipAutoRemark(Integer isSendMsg) {
+        return isSendMsg != null && isSendMsg == 5;
+    }
+
+    /**
+     * 构建购买备注,仅更新 *日期购 标记,保留 *日期完 标记不变。
+     * 始终按「去掉旧购标记 + 追加今日购标记」重组;仅当重组结果与原文完全一致时才跳过。
+     */
+    public static String buildPurchaseRemark(String oldRemark, String fallbackName, int isSendMsg) {
+        String remarkBase = resolveRemarkBase(oldRemark, fallbackName);
+        LocalDate today = LocalDate.now();
+        String monthDay = today.format(DateTimeFormatter.ofPattern("MMdd"));
+        String dayOfMonth = today.format(DateTimeFormatter.ofPattern("dd"));
+        String markToAdd = (isSendMsg == 3 || isSendMsg == 4) ? "*" + dayOfMonth + "购" : "*" + monthDay + "购";
+        String bodyWithoutPurchaseMark = removeMarks(remarkBase, PURCHASE_MARK_PATTERN);
+        String rebuilt = appendMark(bodyWithoutPurchaseMark, markToAdd, isSendMsg);
+        return rebuilt.equals(remarkBase) ? remarkBase : rebuilt;
+    }
+
+    /**
+     * 构建完课备注,仅更新 *日期完 标记,保留 *日期购 标记不变。
+     */
+    public static String buildFinishRemark(String oldRemark, String fallbackName, int isSendMsg) {
+        String remarkBase = resolveRemarkBase(oldRemark, fallbackName);
+        LocalDate today = LocalDate.now();
+        String monthDay = today.format(DateTimeFormatter.ofPattern("MMdd"));
+        String dayOfMonth = today.format(DateTimeFormatter.ofPattern("dd"));
+        String newMark = "*" + monthDay + "完";
+        String newMarkDay = "*" + dayOfMonth + "完";
+
+        if (!shouldUpdateMark(remarkBase, newMark, newMarkDay, FINISH_MARK_PATTERN)) {
+            return remarkBase;
+        }
+
+        String markToAdd = (isSendMsg == 3 || isSendMsg == 4) ? newMarkDay : newMark;
+        String bodyWithoutFinishMark = removeMarks(remarkBase, FINISH_MARK_PATTERN);
+        return appendMark(bodyWithoutFinishMark, markToAdd, isSendMsg);
+    }
+
+    private static String resolveRemarkBase(String oldRemark, String fallbackName) {
+        if (StringUtils.isBlank(oldRemark)) {
+            return StringUtils.defaultString(fallbackName, "");
+        }
+        return oldRemark;
+    }
+
+    private static boolean shouldUpdateMark(String remarkBase, String newMark, String newMarkDay, Pattern markPattern) {
+        List<String> existingMarks = extractMarks(remarkBase, markPattern);
+        for (String mark : existingMarks) {
+            String normalized = normalizeMark(mark, markPattern);
+            if (normalized.equals(newMark) || normalized.equals(newMarkDay)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static List<String> extractMarks(String remark, Pattern markPattern) {
+        List<String> marks = new ArrayList<>();
+        Matcher matcher = markPattern.matcher(remark);
+        while (matcher.find()) {
+            marks.add(matcher.group());
+        }
+        return marks;
+    }
+
+    private static String removeMarks(String remark, Pattern markPattern) {
+        if (StringUtils.isBlank(remark)) {
+            return "";
+        }
+        if (markPattern == FINISH_MARK_PATTERN) {
+            return remark.replaceAll("\\*\\d{1,4}完", "").trim();
+        }
+        return remark.replaceAll("\\*\\d{1,4}购", "").trim();
+    }
+
+    private static String appendMark(String body, String markToAdd, int isSendMsg) {
+        int keepLength = REMARK_MAX_LENGTH - markToAdd.length();
+        if (keepLength < 0) {
+            keepLength = 0;
+        }
+        String trimmedBody = body.length() > keepLength ? body.substring(0, keepLength) : body;
+        if (isSendMsg == 1 || isSendMsg == 3) {
+            return markToAdd + trimmedBody;
+        }
+        return trimmedBody + markToAdd;
+    }
+
+    private static String normalizeMark(String mark, Pattern markPattern) {
+        Matcher matcher = markPattern.matcher(mark);
+        if (!matcher.find()) {
+            return mark;
+        }
+        String digits = matcher.group(1);
+        String suffix = markPattern == FINISH_MARK_PATTERN ? "完" : "购";
+        switch (digits.length()) {
+            case 1:
+                return "*0" + digits + suffix;
+            case 2:
+                return mark;
+            case 3:
+                return "*0" + digits + suffix;
+            default:
+                return mark;
+        }
+    }
+}

+ 7 - 0
fs-service/src/main/resources/db/changelog/changes/20260613-live-user-add-is-del.sql

@@ -189,3 +189,10 @@ SET dict_label = '首次下载APP获取积分', dict_sort = 28
 WHERE dict_type = 'sys_integral_log_type' AND dict_value = '28';
 --rollback UPDATE sys_dict_data SET dict_label = '管理员后台扣除', dict_sort = 28 WHERE dict_type = 'sys_integral_log_type' AND dict_value = '28';
 
+--changeset yhq:20250706-fs-miniprogram-sub-notify-task-fs-user-id
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_miniprogram_sub_notify_task'
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fs_miniprogram_sub_notify_task' AND column_name = 'fs_user_id'
+ALTER TABLE `fs_miniprogram_sub_notify_task`
+    ADD COLUMN `fs_user_id` bigint DEFAULT NULL COMMENT 'fs_user.user_id' AFTER `touser`;
+--rollback ALTER TABLE fs_miniprogram_sub_notify_task DROP COLUMN fs_user_id;

+ 4 - 2
fs-service/src/main/resources/mapper/his/FsUserIntegralBatchMapper.xml

@@ -11,8 +11,10 @@
         select batch_id as batchId, user_id as userId, amount, remain, expire_time as expireTime,
                source_log_id as sourceLogId, create_time as createTime
         from fs_user_integral_batch
-        where remain > 0 and expire_time &lt;= #{now}
-        order by expire_time asc
+        where remain > 0
+          and create_time &gt;= #{launchDate}
+          and create_time &lt;= DATE_SUB(#{now}, INTERVAL #{expireDays} DAY)
+        order by create_time asc
         limit #{limit}
     </select>
 

+ 3 - 0
fs-service/src/main/resources/mapper/live/LiveMiniprogramSubNotifyTaskMapper.xml

@@ -9,6 +9,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         task_name,
         template_id,
         touser,
+        fs_user_id,
         page,
         data,
         status,
@@ -27,6 +28,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{item.taskName},
             #{item.templateId},
             #{item.touser},
+            #{item.fsUserId},
             #{item.page},
             #{item.data},
             #{item.status},
@@ -49,6 +51,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             task_name = #{item.taskName},
             template_id = #{item.templateId},
             touser = #{item.touser},
+            fs_user_id = #{item.fsUserId},
             page = #{item.page},
             data = #{item.data},
             status = #{item.status},

+ 13 - 0
fs-user-app/src/main/java/com/fs/app/controller/UserController.java

@@ -29,6 +29,8 @@ import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FsUserCouponUParam;
 import com.fs.his.param.FsUserEditPushParam;
+import com.fs.his.param.SignNotifyParam;
+import com.fs.his.service.ISignSubscribeNotifyService;
 import com.fs.his.service.IFsDoctorService;
 import com.fs.his.service.IFsPackageService;
 import com.fs.his.service.IFsUserCouponService;
@@ -114,6 +116,17 @@ public class UserController extends  AppBaseController {
     @Autowired
     private IFsCourseWatchLogService watchLogService;
 
+    @Autowired
+    private ISignSubscribeNotifyService signSubscribeNotifyService;
+
+    @Login
+    @ApiOperation("签到提醒小程序订阅")
+    @PostMapping("/subNotifySign")
+    public R subNotifySign(@RequestBody SignNotifyParam param) {
+        param.setUserId(Long.parseLong(getUserId()));
+        return signSubscribeNotifyService.subNotifySign(param);
+    }
+
     @Login
     @ApiOperation("获取用户信息")
     @PostMapping("/friendsSearch")