소스 검색

任务中心

yuhongqi 3 주 전
부모
커밋
62b4b46f34
27개의 변경된 파일888개의 추가작업 그리고 13개의 파일을 삭제
  1. 6 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  2. 6 1
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java
  3. 23 0
      fs-service/src/main/java/com/fs/his/config/IntegralConfig.java
  4. 25 0
      fs-service/src/main/java/com/fs/his/constant/IntegralTaskConstants.java
  5. 3 0
      fs-service/src/main/java/com/fs/his/domain/FsIntegralGoods.java
  6. 20 0
      fs-service/src/main/java/com/fs/his/domain/FsUserIntegralBatch.java
  7. 7 0
      fs-service/src/main/java/com/fs/his/domain/FsUserIntegralLogs.java
  8. 2 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  9. 3 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java
  10. 16 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralBatchMapper.java
  11. 3 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java
  12. 3 0
      fs-service/src/main/java/com/fs/his/param/FsUserAddIntegralTemplateParam.java
  13. 11 0
      fs-service/src/main/java/com/fs/his/param/IntegralBrowseCompleteParam.java
  14. 12 0
      fs-service/src/main/java/com/fs/his/param/IntegralBrowseStartParam.java
  15. 25 0
      fs-service/src/main/java/com/fs/his/service/IIntegralTaskService.java
  16. 85 10
      fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java
  17. 367 0
      fs-service/src/main/java/com/fs/his/service/impl/IntegralTaskServiceImpl.java
  18. 63 0
      fs-service/src/main/java/com/fs/his/utils/IntegralConfigHelper.java
  19. 70 0
      fs-service/src/main/java/com/fs/his/utils/IntegralTaskScopeHelper.java
  20. 3 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  21. 5 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  22. 42 0
      fs-service/src/main/resources/db/changelog/changes/20260613-live-user-add-is-del.sql
  23. 18 0
      fs-service/src/main/resources/db/changelog/dictData/dict_data_update.sql
  24. 5 1
      fs-service/src/main/resources/mapper/his/FsIntegralGoodsMapper.xml
  25. 23 0
      fs-service/src/main/resources/mapper/his/FsUserIntegralBatchMapper.xml
  26. 4 0
      fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml
  27. 38 1
      fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java

+ 6 - 0
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -148,6 +148,8 @@ public class Task {
     @Autowired
     IFsUserIntegralLogsService fsUserIntegralLogsService;
     @Autowired
+    IIntegralTaskService integralTaskService;
+    @Autowired
     SmsServiceImpl smsService;
     @Autowired
     FsStoreOrderItemMapper fsStoreOrderItemMapper;
@@ -1259,6 +1261,10 @@ public class Task {
         fsUserIntegralLogsService.subFsUserIntegralLogsByOrder5();
     }
 
+    public void processExpiredIntegralBatches() {
+        integralTaskService.processExpiredIntegralBatches();
+    }
+
     public void finishInquiry() {
         List<FsInquiryOrder> orders = inquiryOrderMapper.selectFsInquiryOrderByFinish();
         for (FsInquiryOrder order : orders) {

+ 6 - 1
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java

@@ -24,6 +24,7 @@ import org.springframework.stereotype.Service;
 import com.fs.course.mapper.FsCourseWatchCommentMapper;
 import com.fs.course.domain.FsCourseWatchComment;
 import com.fs.course.service.IFsCourseWatchCommentService;
+import com.fs.his.service.IIntegralTaskService;
 
 /**
  * 看课评论Service业务层处理
@@ -48,6 +49,9 @@ public class FsCourseWatchCommentServiceImpl extends ServiceImpl<FsCourseWatchCo
     @Autowired
     private SysKeywordMapper mapper;
 
+    @Autowired
+    private IIntegralTaskService integralTaskService;
+
     /**
      * 查询看课评论
      *
@@ -148,7 +152,8 @@ public class FsCourseWatchCommentServiceImpl extends ServiceImpl<FsCourseWatchCo
         fsCourseWatchComment.setCreateTime(DateUtils.getNowDate());
         int i = baseMapper.insertFsCourseWatchComment(fsCourseWatchComment);
         if (i > 0) {
-            return R.ok().put("status", true);
+            integralTaskService.tryGrantCourseCommentIntegral(param.getUserId(), param.getVideoId(), fsCourseWatchComment.getCommentId());
+            return R.ok().put("status", true).put("toast", "评价积分已发放");
         } else {
             return R.error();
         }

+ 23 - 0
fs-service/src/main/java/com/fs/his/config/IntegralConfig.java

@@ -3,6 +3,7 @@ package com.fs.his.config;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.util.Map;
 
 @Data
 public class IntegralConfig implements Serializable {
@@ -28,4 +29,26 @@ public class IntegralConfig implements Serializable {
     private Integer integralSubscriptCourse;//付费课程订阅积分比例
     private Integer downloadAppIntegral; // 首次下载app获取积分
     private Integer integralPlayGame; //  游戏获取积分
+
+    /** 全渠道首次完成购物奖励积分 */
+    private Integer integralFirstPurchase;
+    /** 邀请新用户单日上限次数 */
+    private Integer integralInviteDailyLimit;
+    /** 分享单日上限次数(APP+小程序合计) */
+    private Integer integralShareDailyLimit;
+    /** 观看课程单日上限次数 */
+    private Integer integralCourseDailyLimit;
+    /** 浏览商城单日上限次数 */
+    private Integer integralProductDailyLimit;
+    /** 浏览商城所需秒数 */
+    private Integer integralProductBrowseSeconds;
+    /** 点播课留言积分 */
+    private Integer integralComment;
+    /** 积分有效天数 */
+    private Integer integralExpireDays;
+    /** 积分有效期功能上线日(历史积分迁移基准) yyyy-MM-dd */
+    private String integralExpireLaunchDate;
+
+    /** 任务展示端配置,key=任务code,value=app / mini / app,mini */
+    private Map<String, String> taskDisplayScope;
 }

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

@@ -0,0 +1,25 @@
+package com.fs.his.constant;
+
+/**
+ * 0701 任务体系常量
+ */
+public final class IntegralTaskConstants {
+
+    private IntegralTaskConstants() {
+    }
+
+    public static final String TASK_SIGN = "SIGN";
+    public static final String TASK_REGISTER = "REGISTER";
+    public static final String TASK_BIND_PHONE = "BIND_PHONE";
+    public static final String TASK_FIRST_PURCHASE = "FIRST_PURCHASE";
+    public static final String TASK_DOWNLOAD_APP = "DOWNLOAD_APP";
+    public static final String TASK_INVITE = "INVITE";
+    public static final String TASK_SHARE = "SHARE";
+    public static final String TASK_COURSE = "COURSE";
+    public static final String TASK_BROWSE_MALL = "BROWSE_MALL";
+    public static final String TASK_INVITED = "INVITED";
+
+    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:";
+}

+ 3 - 0
fs-service/src/main/java/com/fs/his/domain/FsIntegralGoods.java

@@ -70,6 +70,9 @@ public class FsIntegralGoods extends BaseEntity
 
     private String appIds;
 
+    /** 可见范围 app,mini */
+    private String visibleScope;
+
     @TableField(exist = false)
     private String appId;
 

+ 20 - 0
fs-service/src/main/java/com/fs/his/domain/FsUserIntegralBatch.java

@@ -0,0 +1,20 @@
+package com.fs.his.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户积分批次 fs_user_integral_batch
+ */
+@Data
+public class FsUserIntegralBatch extends BaseEntity {
+
+    private Long batchId;
+    private Long userId;
+    private Long amount;
+    private Long remain;
+    private Date expireTime;
+    private Long sourceLogId;
+}

+ 7 - 0
fs-service/src/main/java/com/fs/his/domain/FsUserIntegralLogs.java

@@ -6,6 +6,7 @@ import io.swagger.models.auth.In;
 import lombok.Data;
 
 import java.math.BigDecimal;
+import java.util.Date;
 
 /**
  * 积分记录对象 fs_user_integral_logs
@@ -50,4 +51,10 @@ public class FsUserIntegralLogs extends BaseEntity
     private String phone;
 
     private BigDecimal commission;
+
+    /** 该笔获得积分过期时间 */
+    private Date expireTime;
+
+    /** 来源端 app/mini */
+    private String platform;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java

@@ -39,6 +39,8 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_29( 29,"玩游戏获取积分"),
     TYPE_30(30, "购物积分发放"),
     TYPE_31(31, "购物积分退款扣除"),
+    TYPE_32(32, "首次完成购物"),
+    TYPE_33(33, "内容留言积分"),
     ;
 
 

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

@@ -93,6 +93,9 @@ public interface FsIntegralGoodsMapper
             "<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);

+ 16 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralBatchMapper.java

@@ -0,0 +1,16 @@
+package com.fs.his.mapper;
+
+import com.fs.his.domain.FsUserIntegralBatch;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+public interface FsUserIntegralBatchMapper {
+
+    int insertFsUserIntegralBatch(FsUserIntegralBatch batch);
+
+    List<FsUserIntegralBatch> selectExpiredBatches(@Param("now") Date now, @Param("limit") int limit);
+
+    int updateRemain(@Param("batchId") Long batchId, @Param("remain") Long remain);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java

@@ -30,4 +30,7 @@ public class FsIntegralGoodsListUParam  implements Serializable {
     private Integer status;
 
     private String appId;
+
+    /** 可见端 app / mini */
+    private String platform;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/his/param/FsUserAddIntegralTemplateParam.java

@@ -15,4 +15,7 @@ public class FsUserAddIntegralTemplateParam {
     private Integer points;
 
     private String remark;
+
+    /** 来源端 app / mini */
+    private String platform;
 }

+ 11 - 0
fs-service/src/main/java/com/fs/his/param/IntegralBrowseCompleteParam.java

@@ -0,0 +1,11 @@
+package com.fs.his.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class IntegralBrowseCompleteParam implements Serializable {
+    private String browseToken;
+    private String platform;
+}

+ 12 - 0
fs-service/src/main/java/com/fs/his/param/IntegralBrowseStartParam.java

@@ -0,0 +1,12 @@
+package com.fs.his.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class IntegralBrowseStartParam implements Serializable {
+    private String pageType;
+    private Long goodsId;
+    private String platform;
+}

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

@@ -0,0 +1,25 @@
+package com.fs.his.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.his.param.IntegralBrowseCompleteParam;
+import com.fs.his.param.IntegralBrowseStartParam;
+
+/**
+ * 0701 任务体系
+ */
+public interface IIntegralTaskService {
+
+    R getTaskCenter(Long userId, String platform);
+
+    R startBrowse(Long userId, IntegralBrowseStartParam param);
+
+    R completeBrowse(Long userId, IntegralBrowseCompleteParam param);
+
+    void tryGrantFirstPurchase(Long userId, Long orderId);
+
+    void tryGrantCourseCommentIntegral(Long userId, Long videoId, Long commentId);
+
+    void processExpiredIntegralBatches();
+
+    void markSignPopupShown(Long userId);
+}

+ 85 - 10
fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java

@@ -9,9 +9,11 @@ import com.fs.his.domain.FsUser;
 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.domain.FsUserIntegralBatch;
 import com.fs.his.param.FsUserAddIntegralParam;
 import com.fs.his.param.FsUserAddIntegralTemplateParam;
 import com.fs.his.param.FsUserIntegralLogsListUParam;
@@ -19,8 +21,10 @@ import com.fs.his.param.FsUserIntegralLogsParam;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.vo.FsUserIntegralLogsListUVO;
 import com.fs.his.vo.FsUserIntegralLogsListVO;
+import com.fs.his.utils.IntegralConfigHelper;
 import com.fs.his.vo.SubIntegralVO;
 import com.fs.system.service.ISysConfigService;
+import cn.hutool.core.date.DateUtil;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -49,6 +53,8 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
     private ISysConfigService configService;
     @Autowired
     private FsUserNewTaskMapper fsUserNewTaskMapper;
+    @Autowired
+    private FsUserIntegralBatchMapper fsUserIntegralBatchMapper;
     /**
      * 查询积分记录
      *
@@ -196,7 +202,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
         if (user!=null){
             String json =configService.selectConfigByKey("his.integral");
-            IntegralConfig config= JSONUtil.toBean(json,IntegralConfig.class);
+            IntegralConfig config= IntegralConfigHelper.load(configService);
             System.out.println("Types: " + Arrays.toString(config.getIntegralTypeByOneDay()));
             System.out.println("User ID: " + param.getUserId());
             Integer todayTotalIntegral= fsUserIntegralLogsMapper.selectTodayTotalIntegral(config.getIntegralTypeByOneDay(),param.getUserId());
@@ -227,7 +233,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
         if (user!=null){
             String json =configService.selectConfigByKey("his.integral");
-            IntegralConfig config= JSONUtil.toBean(json,IntegralConfig.class);
+            IntegralConfig config= IntegralConfigHelper.load(configService);
             Integer[] integralTypeByOneDay = config.getIntegralTypeByOneDay();
             System.out.println("Types: " + Arrays.toString(integralTypeByOneDay));
             System.out.println("User ID: " + param.getUserId());
@@ -267,8 +273,8 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     break;
                 case 3: //分享获得积分
                     integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now());
-                    if (integralLogs.size() >= 5){
-                        return R.error("今日看课积分已达上限");
+                    if (integralLogs.size() >= config.getIntegralShareDailyLimit()){
+                        return R.error("今日分享积分已达上限");
                     }
                     integralNum = config.getIntegralShare();
                     logsType = FsUserIntegralLogTypeEnum.TYPE_3;
@@ -296,7 +302,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
 //                    break;
                 case 10: //观看课程获取积分
                     integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now());
-                    if (integralLogs.size() >= 3){
+                    if (integralLogs.size() >= config.getIntegralCourseDailyLimit()){
                         return R.error("今日看课积分已达上限");
                     }
                     integralNum = config.getIntegralCourse();
@@ -319,6 +325,10 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                     logsType = FsUserIntegralLogTypeEnum.TYPE_12;
                     break;
                 case 13: //浏览商品获得积分
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now());
+                    if (integralLogs.size() >= config.getIntegralProductDailyLimit()){
+                        return R.error("今日浏览商城积分已达上限");
+                    }
                     integralNum = config.getIntegralProduct();
                     logsType = FsUserIntegralLogTypeEnum.TYPE_13;
                     break;
@@ -334,6 +344,10 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
 //                    logsType = FsUserIntegralLogTypeEnum.TYPE_17;
 //                    break;
                 case 18: //邀请新用户获取积分
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), LocalDate.now());
+                    if (integralLogs.size() >= config.getIntegralInviteDailyLimit()){
+                        return R.error("今日邀请积分已达上限");
+                    }
                     integralNum = config.getIntegralInvite();
                     logsType = FsUserIntegralLogTypeEnum.TYPE_18;
                     break;
@@ -388,13 +402,34 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
                         param.setRemark(logsType.getDesc());
                     }
                     break;
+                case 32: // 首次完成购物
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    if (!integralLogs.isEmpty()){
+                        return R.error("已领取过该积分");
+                    }
+                    integralNum = config.getIntegralFirstPurchase();
+                    logsType = FsUserIntegralLogTypeEnum.TYPE_32;
+                    break;
+                case 33: // 点播课留言积分
+                    integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(param.getUserId(),param.getLogType(), null);
+                    if (StringUtils.isNotBlank(param.getBusinessId())) {
+                        for (FsUserIntegralLogs item : integralLogs) {
+                            if (param.getBusinessId().equals(item.getBusinessId())) {
+                                return R.error("该素材留言积分已发放");
+                            }
+                        }
+                    }
+                    integralNum = config.getIntegralComment();
+                    logsType = FsUserIntegralLogTypeEnum.TYPE_33;
+                    break;
                 default:
                     return R.error("积分类型错误,联系管理员");
             }
             if (integralNum!=null && logsType!=null){
-                R r = updateAddIntegral(integralNum, user.getUserId(), user.getIntegral(), logsType, param.getBusinessId(), param.getRemark());
-                log.info("用户id为{},{}", user.getUserId(),r.get("msg").toString());
-                return R.ok();
+                R r = updateAddIntegral(integralNum, user.getUserId(), user.getIntegral(), logsType, param.getBusinessId(), param.getRemark(), param.getPlatform());
+                log.info("用户id为{},{}", user.getUserId(),r.get("msg"));
+                return r.put("taskName", logsType.getDesc()).put("rewardPoints", integralNum)
+                        .put("toast", logsType.getDesc() + " +" + integralNum + "积分");
             }
 
         }
@@ -408,25 +443,46 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
      * @param userIntegral
      * @return
      */
-    public R updateAddIntegral(Integer integralNum, Long userId, Long userIntegral, FsUserIntegralLogTypeEnum logsType, String businessId, String remark) {
+    public R updateAddIntegral(Integer integralNum, Long userId, Long userIntegral, FsUserIntegralLogTypeEnum logsType, String businessId, String remark, String platform) {
+        IntegralConfig config = IntegralConfigHelper.load(configService);
         FsUser userMap=new FsUser();
         userMap.setUserId(userId);
         userMap.setIntegral(userIntegral+integralNum);
         fsUserMapper.updateFsUser(userMap);
+        Date now = new Date();
+        Date expireTime = DateUtil.offsetDay(now, config.getIntegralExpireDays());
         FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
         integralLogs.setIntegral(integralNum.longValue());
         integralLogs.setUserId(userId);
         integralLogs.setBalance(userMap.getIntegral());
         integralLogs.setLogType(logsType.getValue());
-        integralLogs.setCreateTime(new Date());
+        integralLogs.setCreateTime(now);
         integralLogs.setBusinessId(businessId);
         integralLogs.setRemark(remark);
+        integralLogs.setExpireTime(expireTime);
+        integralLogs.setPlatform(platform);
         if (fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs)>0){
+            createIntegralBatch(userId, integralNum.longValue(), expireTime, integralLogs.getId());
             return R.ok(logsType.getDesc()+":"+integralNum+"积分");
         }
         return R.error("获取积分失败");
     }
 
+    private void createIntegralBatch(Long userId, Long amount, Date expireTime, Long sourceLogId) {
+        try {
+            FsUserIntegralBatch batch = new FsUserIntegralBatch();
+            batch.setUserId(userId);
+            batch.setAmount(amount);
+            batch.setRemain(amount);
+            batch.setExpireTime(expireTime);
+            batch.setSourceLogId(sourceLogId);
+            batch.setCreateTime(new Date());
+            fsUserIntegralBatchMapper.insertFsUserIntegralBatch(batch);
+        } catch (Exception e) {
+            log.warn("写入积分批次失败,请确认已执行 20250701-任务体系.sql: {}", e.getMessage());
+        }
+    }
+
     private R addVideoIntegral(Integer integralFirstVideo,Integer integralFinishVideo,Long userId,Long userIntegral) {
         Integer videoIntegralCount = fsUserIntegralLogsMapper.selectTodayVideoIntegralCount(userId);
         Integer addIntegral = integralFirstVideo-videoIntegralCount;
@@ -499,6 +555,9 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         int isNewUser = 1;
         int isFinishConsultation = 0;
         int isFinishFirstOrderPoint = 0;
+        int taskSix = 0;
+        int isFinishDownloadAppIntegral = 0;
+        int isFinishFirstPurchase = 0;
 //        long expireDays = 0;
         Date createTime = null;
 //        FsUser fsUser = fsUserMapper.selectFsUserIsNew(userId);
@@ -546,14 +605,30 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             isFinishFirstOrderPoint = 1;
         }
 
+        integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_19.getValue(), null);
+        if (!integralLogs.isEmpty()) {
+            taskSix = 1;
+        }
+        integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_28.getValue(), null);
+        if (!integralLogs.isEmpty()) {
+            isFinishDownloadAppIntegral = 1;
+        }
+        integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_32.getValue(), null);
+        if (!integralLogs.isEmpty()) {
+            isFinishFirstPurchase = 1;
+        }
+
         Map<String,Object> map = new HashMap<>();
         map.put("taskOne",taskOne);
         map.put("taskTwo",taskTwo);
         map.put("taskThree",taskThree);
         map.put("taskFour",taskFour);
         map.put("taskFive",taskFive);
+        map.put("taskSix", taskSix);
         map.put("isFinishConsultation", isFinishConsultation);
         map.put("isFinishFirstOrderPoint", isFinishFirstOrderPoint);
+        map.put("isFinishDownloadAppIntegral", isFinishDownloadAppIntegral);
+        map.put("isFinishFirstPurchase", isFinishFirstPurchase);
         return R.ok().put("data",map).put("isNewUser",isNewUser).put("createTime",createTime);
     }
 

+ 367 - 0
fs-service/src/main/java/com/fs/his/service/impl/IntegralTaskServiceImpl.java

@@ -0,0 +1,367 @@
+package com.fs.his.service.impl;
+
+import cn.hutool.core.date.DateUtil;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.uuid.IdUtils;
+import com.fs.his.config.IntegralConfig;
+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;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import com.fs.his.service.IFsUserSignService;
+import com.fs.his.service.IIntegralTaskService;
+import com.fs.his.utils.IntegralConfigHelper;
+import com.fs.his.utils.IntegralTaskScopeHelper;
+import com.fs.system.service.ISysConfigService;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class IntegralTaskServiceImpl implements IIntegralTaskService {
+
+    private static final Logger log = LoggerFactory.getLogger(IntegralTaskServiceImpl.class);
+
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private FsUserMapper fsUserMapper;
+    @Autowired
+    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+    @Autowired
+    private FsUserNewTaskMapper fsUserNewTaskMapper;
+    @Autowired
+    private IFsUserIntegralLogsService userIntegralLogsService;
+    @Autowired
+    private IFsUserSignService userSignService;
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private FsUserIntegralBatchMapper fsUserIntegralBatchMapper;
+
+    @Override
+    public R getTaskCenter(Long userId, String platform) {
+        IntegralConfig config = IntegralConfigHelper.load(configService);
+        FsUser user = fsUserMapper.selectFsUserByUserId(userId);
+        if (user == null) {
+            return R.error("用户不存在");
+        }
+
+        String normalizedPlatform = IntegralTaskScopeHelper.normalizePlatform(platform);
+        Map<String, String> taskDisplayScope = IntegralTaskScopeHelper.resolveTaskDisplayScope(config);
+
+        String signJson = configService.selectConfigByKey("his.sign");
+        Long signNum = userSignService.getSign(user);
+        Boolean isDaySign = userSignService.isDaySign(user);
+        String popupKey = IntegralTaskConstants.REDIS_SIGN_POPUP_PREFIX + userId + ":" + LocalDate.now();
+        boolean needPopup = !Boolean.TRUE.equals(isDaySign) && redisCache.getCacheObject(popupKey) == null;
+
+        Map<String, Object> sign = new HashMap<>();
+        sign.put("signNum", signNum);
+        sign.put("isDaySign", isDaySign);
+        sign.put("needPopup", needPopup);
+        sign.put("configs", signJson);
+        sign.put("visibleScope", IntegralTaskScopeHelper.getTaskDisplayScope(taskDisplayScope, IntegralTaskConstants.TASK_SIGN));
+        sign.put("hidden", !IntegralTaskScopeHelper.isTaskVisible(taskDisplayScope, IntegralTaskConstants.TASK_SIGN, normalizedPlatform));
+
+        List<Map<String, Object>> newbieTasks = filterTasksByPlatform(buildNewbieTasks(userId, config, taskDisplayScope), taskDisplayScope, normalizedPlatform);
+        List<Map<String, Object>> dailyTasks = filterTasksByPlatform(buildDailyTasks(userId, config, taskDisplayScope), taskDisplayScope, normalizedPlatform);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("platform", normalizedPlatform);
+        data.put("sign", sign);
+        data.put("newbieTasks", newbieTasks);
+        data.put("dailyTasks", dailyTasks);
+        data.put("integralBalance", user.getIntegral());
+        data.put("expireTip", buildExpireTip(config));
+        return R.ok().put("data", data);
+    }
+
+    @Override
+    public R startBrowse(Long userId, IntegralBrowseStartParam param) {
+        IntegralConfig config = IntegralConfigHelper.load(configService);
+        List<FsUserIntegralLogs> todayLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(
+                userId, FsUserIntegralLogTypeEnum.TYPE_13.getValue(), LocalDate.now());
+        if (todayLogs.size() >= config.getIntegralProductDailyLimit()) {
+            return R.error("今日浏览商城积分已达上限");
+        }
+        String token = IdUtils.simpleUUID();
+        Map<String, Object> cache = new HashMap<>();
+        cache.put("userId", userId);
+        cache.put("startTime", System.currentTimeMillis());
+        cache.put("pageType", param.getPageType());
+        cache.put("goodsId", param.getGoodsId());
+        cache.put("platform", param.getPlatform());
+        redisCache.setCacheObject(IntegralTaskConstants.REDIS_BROWSE_PREFIX + token, cache, 5, TimeUnit.MINUTES);
+        return R.ok().put("browseToken", token).put("requiredSeconds", config.getIntegralProductBrowseSeconds());
+    }
+
+    @Override
+    @Transactional
+    public R completeBrowse(Long userId, IntegralBrowseCompleteParam param) {
+        if (StringUtils.isBlank(param.getBrowseToken())) {
+            return R.error("browseToken不能为空");
+        }
+        String key = IntegralTaskConstants.REDIS_BROWSE_PREFIX + param.getBrowseToken();
+        Map<String, Object> cache = redisCache.getCacheObject(key);
+        if (cache == null) {
+            return R.error("浏览计时已过期,请重新开始");
+        }
+        Object cacheUserId = cache.get("userId");
+        if (cacheUserId == null || !userId.equals(Long.valueOf(cacheUserId.toString()))) {
+            return R.error("浏览计时无效");
+        }
+        IntegralConfig config = IntegralConfigHelper.load(configService);
+        long startTime = Long.parseLong(cache.get("startTime").toString());
+        long elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000;
+        if (elapsedSeconds < config.getIntegralProductBrowseSeconds()) {
+            return R.error("浏览时长不足" + config.getIntegralProductBrowseSeconds() + "秒");
+        }
+        redisCache.deleteObject(key);
+
+        FsUserAddIntegralTemplateParam templateParam = new FsUserAddIntegralTemplateParam();
+        templateParam.setUserId(userId);
+        templateParam.setLogType(FsUserIntegralLogTypeEnum.TYPE_13.getValue());
+        Object goodsId = cache.get("goodsId");
+        templateParam.setBusinessId(goodsId != null ? goodsId.toString() : LocalDate.now().toString());
+        templateParam.setPlatform(param.getPlatform());
+        R result = userIntegralLogsService.addIntegralTemplate(templateParam);
+        if (!isSuccess(result)) {
+            return result;
+        }
+        markTaskDone(userId, IntegralTaskConstants.TASK_BROWSE_MALL, true);
+        return enrichTaskToast(result, "浏览商城60秒", config.getIntegralProduct());
+    }
+
+    @Override
+    @Transactional
+    public void tryGrantFirstPurchase(Long userId, Long orderId) {
+        if (userId == null || orderId == null) {
+            return;
+        }
+        List<FsUserIntegralLogs> exists = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(
+                userId, FsUserIntegralLogTypeEnum.TYPE_32.getValue(), null);
+        if (!exists.isEmpty()) {
+            return;
+        }
+        FsUserAddIntegralTemplateParam param = new FsUserAddIntegralTemplateParam();
+        param.setUserId(userId);
+        param.setLogType(FsUserIntegralLogTypeEnum.TYPE_32.getValue());
+        param.setBusinessId(orderId.toString());
+        R r = userIntegralLogsService.addIntegralTemplate(param);
+        if (isSuccess(r)) {
+            markTaskDone(userId, IntegralTaskConstants.TASK_FIRST_PURCHASE, false);
+        }
+    }
+
+    @Override
+    @Transactional
+    public void tryGrantCourseCommentIntegral(Long userId, Long videoId, Long commentId) {
+        if (userId == null || videoId == null) {
+            return;
+        }
+        List<FsUserIntegralLogs> exists = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(
+                userId, FsUserIntegralLogTypeEnum.TYPE_33.getValue(), null);
+        for (FsUserIntegralLogs item : exists) {
+            if (videoId.toString().equals(item.getBusinessId())) {
+                return;
+            }
+        }
+        FsUserAddIntegralTemplateParam param = new FsUserAddIntegralTemplateParam();
+        param.setUserId(userId);
+        param.setLogType(FsUserIntegralLogTypeEnum.TYPE_33.getValue());
+        param.setBusinessId(videoId.toString());
+        param.setRemark(commentId != null ? commentId.toString() : null);
+        userIntegralLogsService.addIntegralTemplate(param);
+    }
+
+    @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;
+            }
+            FsUser user = fsUserMapper.selectFsUserByUserId(batch.getUserId());
+            if (user == null) {
+                continue;
+            }
+            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
+    public void markSignPopupShown(Long userId) {
+        String popupKey = IntegralTaskConstants.REDIS_SIGN_POPUP_PREFIX + userId + ":" + LocalDate.now();
+        redisCache.setCacheObject(popupKey, "1", 1, TimeUnit.DAYS);
+    }
+
+    private List<Map<String, Object>> buildNewbieTasks(Long userId, IntegralConfig config, Map<String, String> taskDisplayScope) {
+        List<Map<String, Object>> tasks = new ArrayList<>();
+        tasks.add(buildLifetimeTask(IntegralTaskConstants.TASK_REGISTER, "首次注册登录",
+                config.getIntegralRegister(), hasLogType(userId, 20), taskDisplayScope));
+        tasks.add(buildLifetimeTask(IntegralTaskConstants.TASK_DOWNLOAD_APP, "首次下载APP",
+                config.getDownloadAppIntegral(), hasLogType(userId, 28), taskDisplayScope));
+        tasks.add(buildLifetimeTask(IntegralTaskConstants.TASK_FIRST_PURCHASE, "首次完成购物",
+                config.getIntegralFirstPurchase(), hasLogType(userId, 32), taskDisplayScope));
+        tasks.add(buildLifetimeTask(IntegralTaskConstants.TASK_INVITED, "填写朋友邀请码",
+                config.getIntegralInvited(), hasLogType(userId, 19), taskDisplayScope));
+        hideCompletedNewbieTasks(tasks, userId);
+        return tasks;
+    }
+
+    private List<Map<String, Object>> buildDailyTasks(Long userId, IntegralConfig config, Map<String, String> taskDisplayScope) {
+        List<Map<String, Object>> tasks = new ArrayList<>();
+        int shareCount = countTodayLog(userId, 3);
+        tasks.add(buildDailyTask(IntegralTaskConstants.TASK_SHARE, "分享得积分",
+                config.getIntegralShare(), shareCount, config.getIntegralShareDailyLimit(), taskDisplayScope));
+
+        int courseCount = countTodayLog(userId, 10);
+        tasks.add(buildDailyTask(IntegralTaskConstants.TASK_COURSE, "学习首页课程",
+                config.getIntegralCourse(), courseCount, config.getIntegralCourseDailyLimit(), taskDisplayScope));
+
+        int browseCount = countTodayLog(userId, 13);
+        tasks.add(buildDailyTask(IntegralTaskConstants.TASK_BROWSE_MALL, "浏览商城60秒",
+                config.getIntegralProduct(), browseCount, config.getIntegralProductDailyLimit(), taskDisplayScope));
+
+        int inviteCount = countTodayLog(userId, 18);
+        tasks.add(buildDailyTask(IntegralTaskConstants.TASK_INVITE, "邀好友享积分",
+                config.getIntegralInvite(), inviteCount, config.getIntegralInviteDailyLimit(), taskDisplayScope));
+        return tasks;
+    }
+
+    private List<Map<String, Object>> filterTasksByPlatform(List<Map<String, Object>> tasks, Map<String, String> taskDisplayScope, String platform) {
+        List<Map<String, Object>> visibleTasks = new ArrayList<>();
+        for (Map<String, Object> task : tasks) {
+            if (Boolean.TRUE.equals(task.get("hidden"))) {
+                continue;
+            }
+            String code = task.get("code") == null ? null : task.get("code").toString();
+            if (IntegralTaskScopeHelper.isTaskVisible(taskDisplayScope, code, platform)) {
+                visibleTasks.add(task);
+            }
+        }
+        return visibleTasks;
+    }
+
+    private Map<String, Object> buildLifetimeTask(String code, String title, Integer reward, boolean finished, Map<String, String> taskDisplayScope) {
+        Map<String, Object> task = new HashMap<>();
+        task.put("code", code);
+        task.put("title", title);
+        task.put("reward", reward == null ? 0 : reward);
+        task.put("status", finished ? 2 : 0);
+        task.put("hidden", false);
+        task.put("visibleScope", IntegralTaskScopeHelper.getTaskDisplayScope(taskDisplayScope, code));
+        return task;
+    }
+
+    private Map<String, Object> buildDailyTask(String code, String title, Integer reward, int doneCount, int limitCount, Map<String, String> taskDisplayScope) {
+        Map<String, Object> task = new HashMap<>();
+        task.put("code", code);
+        task.put("title", title);
+        task.put("reward", reward == null ? 0 : reward);
+        task.put("doneCount", doneCount);
+        task.put("limitCount", limitCount);
+        int status = 0;
+        if (doneCount >= limitCount) {
+            status = 3;
+        } else if (doneCount > 0) {
+            status = 2;
+        }
+        task.put("status", status);
+        task.put("visibleScope", IntegralTaskScopeHelper.getTaskDisplayScope(taskDisplayScope, code));
+        return task;
+    }
+
+    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;
+        }
+        for (Map<String, Object> task : tasks) {
+            if (Integer.valueOf(2).equals(task.get("status"))) {
+                task.put("hidden", true);
+            }
+        }
+    }
+
+    private boolean hasLogType(Long userId, int logType) {
+        List<FsUserIntegralLogs> logs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, logType, null);
+        return logs != null && !logs.isEmpty();
+    }
+
+    private int countTodayLog(Long userId, int logType) {
+        List<FsUserIntegralLogs> logs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, logType, LocalDate.now());
+        return logs == null ? 0 : logs.size();
+    }
+
+    private String buildExpireTip(IntegralConfig config) {
+        if (config.getIntegralExpireDays() == null) {
+            return null;
+        }
+        return "积分自获得起" + config.getIntegralExpireDays() + "天内有效,过期自动失效";
+    }
+
+    private void markTaskDone(Long userId, String taskCode, boolean daily) {
+        String period = daily ? LocalDate.now().toString() : "lifetime";
+        String key = IntegralTaskConstants.REDIS_TASK_DONE_PREFIX + userId + ":" + taskCode + ":" + period;
+        redisCache.setCacheObject(key, "1", daily ? 1 : 3650, TimeUnit.DAYS);
+    }
+
+    static R enrichTaskToast(R result, String taskName, Integer reward) {
+        if (result == null) {
+            return R.ok().put("taskName", taskName).put("rewardPoints", reward).put("toast", taskName + " +" + reward + "积分");
+        }
+        result.put("taskName", taskName);
+        result.put("rewardPoints", reward);
+        result.put("toast", taskName + " +" + reward + "积分");
+        return result;
+    }
+
+    private boolean isSuccess(R r) {
+        return r != null && r.get("code") != null && Integer.valueOf(200).equals(Integer.valueOf(r.get("code").toString()));
+    }
+}

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

@@ -0,0 +1,63 @@
+package com.fs.his.utils;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.his.config.IntegralConfig;
+import com.fs.system.service.ISysConfigService;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Map;
+
+/**
+ * 积分配置读取,补全 0701 任务体系默认值
+ */
+public final class IntegralConfigHelper {
+
+    private IntegralConfigHelper() {
+    }
+
+    public static IntegralConfig load(ISysConfigService configService) {
+        String json = configService.selectConfigByKey("his.integral");
+        IntegralConfig config = StringUtils.isBlank(json)
+                ? new IntegralConfig()
+                : JSONUtil.toBean(json, IntegralConfig.class);
+        if (config == null) {
+            config = new IntegralConfig();
+        }
+        applyDefaults(config);
+        return config;
+    }
+
+    public static void applyDefaults(IntegralConfig config) {
+        if (config.getIntegralFirstPurchase() == null) {
+            config.setIntegralFirstPurchase(20);
+        }
+        if (config.getIntegralInviteDailyLimit() == null) {
+            config.setIntegralInviteDailyLimit(3);
+        }
+        if (config.getIntegralShareDailyLimit() == null) {
+            config.setIntegralShareDailyLimit(5);
+        }
+        if (config.getIntegralCourseDailyLimit() == null) {
+            config.setIntegralCourseDailyLimit(5);
+        }
+        if (config.getIntegralProductDailyLimit() == null) {
+            config.setIntegralProductDailyLimit(1);
+        }
+        if (config.getIntegralProductBrowseSeconds() == null) {
+            config.setIntegralProductBrowseSeconds(60);
+        }
+        if (config.getIntegralComment() == null) {
+            config.setIntegralComment(100);
+        }
+        if (config.getIntegralExpireDays() == null) {
+            config.setIntegralExpireDays(90);
+        }
+        if (config.getTaskDisplayScope() == null || config.getTaskDisplayScope().isEmpty()) {
+            config.setTaskDisplayScope(IntegralTaskScopeHelper.defaultTaskDisplayScope());
+        } else {
+            Map<String, String> merged = IntegralTaskScopeHelper.defaultTaskDisplayScope();
+            merged.putAll(config.getTaskDisplayScope());
+            config.setTaskDisplayScope(merged);
+        }
+    }
+}

+ 70 - 0
fs-service/src/main/java/com/fs/his/utils/IntegralTaskScopeHelper.java

@@ -0,0 +1,70 @@
+package com.fs.his.utils;
+
+import com.fs.his.config.IntegralConfig;
+import com.fs.his.constant.IntegralTaskConstants;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 积分任务展示端(app / mini)配置
+ */
+public final class IntegralTaskScopeHelper {
+
+    private IntegralTaskScopeHelper() {
+    }
+
+    public static Map<String, String> defaultTaskDisplayScope() {
+        Map<String, String> map = new LinkedHashMap<>();
+        map.put(IntegralTaskConstants.TASK_SIGN, "app,mini");
+        map.put(IntegralTaskConstants.TASK_REGISTER, "app,mini");
+        map.put(IntegralTaskConstants.TASK_DOWNLOAD_APP, "app");
+        map.put(IntegralTaskConstants.TASK_FIRST_PURCHASE, "app,mini");
+        map.put(IntegralTaskConstants.TASK_INVITED, "app,mini");
+        map.put(IntegralTaskConstants.TASK_SHARE, "app,mini");
+        map.put(IntegralTaskConstants.TASK_COURSE, "app");
+        map.put(IntegralTaskConstants.TASK_BROWSE_MALL, "app,mini");
+        map.put(IntegralTaskConstants.TASK_INVITE, "app,mini");
+        return map;
+    }
+
+    public static Map<String, String> resolveTaskDisplayScope(IntegralConfig config) {
+        Map<String, String> scopes = defaultTaskDisplayScope();
+        if (config != null && config.getTaskDisplayScope() != null) {
+            scopes.putAll(config.getTaskDisplayScope());
+        }
+        return scopes;
+    }
+
+    public static String normalizePlatform(String platform) {
+        if (StringUtils.isBlank(platform)) {
+            return "app";
+        }
+        String value = platform.trim().toLowerCase();
+        if ("miniapp".equals(value) || "miniprogram".equals(value) || "wx".equals(value) || "h5".equals(value)) {
+            return "mini";
+        }
+        return value;
+    }
+
+    public static boolean isTaskVisible(Map<String, String> scopes, String taskCode, String platform) {
+        if (StringUtils.isBlank(taskCode)) {
+            return true;
+        }
+        String normalizedPlatform = normalizePlatform(platform);
+        String scope = scopes.get(taskCode);
+        if (StringUtils.isBlank(scope)) {
+            return true;
+        }
+        List<String> platforms = Arrays.asList(scope.split(","));
+        return platforms.contains(normalizedPlatform);
+    }
+
+    public static String getTaskDisplayScope(Map<String, String> scopes, String taskCode) {
+        String scope = scopes.get(taskCode);
+        return StringUtils.isBlank(scope) ? "app,mini" : scope;
+    }
+}

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

@@ -498,6 +498,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     @Autowired
     private IFsUserIntegralLogsService userIntegralLogsService;
     @Autowired
+    private IIntegralTaskService integralTaskService;
+    @Autowired
     private FsStoreProductPackageScrmMapper fsStoreProductPackageMapper;
     @Autowired
     private FsUserCompanyPackageScrmMapper fsUserCompanyPackageScrmMapper;
@@ -3114,6 +3116,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             final Long paySuccessOrderId = order.getId();
             CompletableFuture.runAsync(() -> safeSendPaySuccessMsgToWx(paySuccessOrderId));
         }
+        integralTaskService.tryGrantFirstPurchase(order.getUserId(), order.getId());
         return "SUCCESS";
     }
 

+ 5 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -72,6 +72,7 @@ import com.fs.his.mapper.*;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsStoreProductService;
 import com.fs.his.service.IFsUserService;
+import com.fs.his.service.IIntegralTaskService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.hisStore.config.FsErpConfig;
 import com.fs.hisStore.config.StoreConfig;
@@ -272,6 +273,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private IIntegralTaskService integralTaskService;
+
 
     @Autowired
     private FsErpFinishPushMapper fsErpFinishPushMapper;
@@ -914,6 +918,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             } catch (Exception e) {
                 log.error("推送erp失败:{}", e.getMessage());
             }
+            integralTaskService.tryGrantFirstPurchase(Long.valueOf(order.getUserId()), order.getOrderId());
             return "SUCCESS";
         } catch (Exception e) {
             log.info("支付错误:" + e.getMessage());

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

@@ -124,3 +124,45 @@ ALTER TABLE qw_external_contact
 ALTER TABLE qw_external_contact
     ADD COLUMN contact_mobiles VARCHAR(500) NULL DEFAULT NULL COMMENT '联系手机号码,多个英文逗号分隔';
 --rollback ALTER TABLE qw_external_contact DROP COLUMN contact_mobiles;
+
+--changeset yhq:20250701-fs-user-integral-batch
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_user_integral_batch'
+CREATE TABLE `fs_user_integral_batch` (
+    `batch_id` bigint NOT NULL AUTO_INCREMENT COMMENT '批次ID',
+    `user_id` bigint NOT NULL COMMENT '用户ID',
+    `amount` bigint NOT NULL DEFAULT 0 COMMENT '批次原始积分',
+    `remain` bigint NOT NULL DEFAULT 0 COMMENT '剩余可用积分',
+    `expire_time` datetime NOT NULL COMMENT '过期时间',
+    `source_log_id` bigint DEFAULT NULL COMMENT '来源流水ID',
+    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    PRIMARY KEY (`batch_id`),
+    KEY `idx_user_expire` (`user_id`,`expire_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户积分批次';
+--rollback DROP TABLE fs_user_integral_batch;
+
+--changeset yhq:20250701-fs-user-integral-logs-expire-time
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_user_integral_logs'
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fs_user_integral_logs' AND column_name = 'expire_time'
+ALTER TABLE `fs_user_integral_logs`
+    ADD COLUMN `expire_time` datetime DEFAULT NULL COMMENT '该笔获得积分过期时间' AFTER `business_id`;
+--rollback ALTER TABLE fs_user_integral_logs DROP COLUMN expire_time;
+
+--changeset yhq:20250701-fs-user-integral-logs-platform
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_user_integral_logs'
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fs_user_integral_logs' AND column_name = 'platform'
+ALTER TABLE `fs_user_integral_logs`
+    ADD COLUMN `platform` varchar(16) DEFAULT NULL COMMENT '来源端 app/mini' AFTER `expire_time`;
+--rollback ALTER TABLE fs_user_integral_logs DROP COLUMN platform;
+
+--changeset yhq:20250701-fs-integral-goods-visible-scope
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:1 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'fs_integral_goods'
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'fs_integral_goods' AND column_name = 'visible_scope'
+ALTER TABLE `fs_integral_goods`
+    ADD COLUMN `visible_scope` varchar(64) NOT NULL DEFAULT 'app,mini' COMMENT '可见范围 app,mini 多选' AFTER `app_ids`;
+--rollback ALTER TABLE fs_integral_goods DROP COLUMN visible_scope;
+
+

+ 18 - 0
fs-service/src/main/resources/db/changelog/dictData/dict_data_update.sql

@@ -74,3 +74,21 @@ VALUES
 --rollback DELETE FROM sys_dict_data WHERE dict_type = 'store_mall_func';
 --rollback DELETE FROM sys_dict_type WHERE dict_type = 'store_mall_func';
 
+
+--changeset yhq:20250701-sys-integral-log-type-first-purchase
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sys_dict_data WHERE dict_type = 'sys_integral_log_type' AND dict_value = '32'
+INSERT INTO `sys_dict_data`
+(`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+VALUES
+    (32, '首次完成购物', '32', 'sys_integral_log_type', NULL, 'default', 'N', '0', 'admin', NOW(), '', NULL, NULL);
+--rollback DELETE FROM sys_dict_data WHERE dict_type = 'sys_integral_log_type' AND dict_value = '32';
+
+--changeset yhq:20250701-sys-integral-log-type-comment
+--preconditions onFail:MARK_RAN
+--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM sys_dict_data WHERE dict_type = 'sys_integral_log_type' AND dict_value = '33'
+INSERT INTO `sys_dict_data`
+(`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+VALUES
+    (33, '内容留言积分', '33', 'sys_integral_log_type', NULL, 'default', 'N', '0', 'admin', NOW(), '', NULL, NULL);
+--rollback DELETE FROM sys_dict_data WHERE dict_type = 'sys_integral_log_type' AND dict_value = '33';

+ 5 - 1
fs-service/src/main/resources/mapper/his/FsIntegralGoodsMapper.xml

@@ -20,10 +20,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="createTime"    column="create_time"    />
         <result property="barCode"    column="bar_code"    />
         <result property="appIds"    column="app_ids"    />
+        <result property="visibleScope"    column="visible_scope"    />
     </resultMap>
 
     <sql id="selectFsIntegralGoodsVo">
-        select goods_id, img_url, images,bar_code, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, descs, create_time,app_ids from fs_integral_goods
+        select goods_id, img_url, images,bar_code, goods_name, ot_price, goods_type, status, integral, cash, sort, stock, descs, create_time,app_ids,visible_scope from fs_integral_goods
     </sql>
 
     <select id="selectFsIntegralGoodsList" parameterType="FsIntegralGoods" resultMap="FsIntegralGoodsResult">
@@ -61,6 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">create_time,</if>
             <if test="barCode != null">bar_code,</if>
             <if test="appIds != null">app_ids,</if>
+            <if test="visibleScope != null">visible_scope,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="goodsId != null">#{goodsId},</if>
@@ -78,6 +80,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">#{createTime},</if>
             <if test="barCode != null">#{barCode},</if>
             <if test="appIds != null">#{appIds},</if>
+            <if test="visibleScope != null">#{visibleScope},</if>
          </trim>
     </insert>
 
@@ -98,6 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createTime != null">create_time = #{createTime},</if>
             <if test="barCode != null">bar_code = #{barCode},</if>
             <if test="appIds != null">app_ids = #{appIds},</if>
+            <if test="visibleScope != null">visible_scope = #{visibleScope},</if>
         </trim>
         where goods_id = #{goodsId}
     </update>

+ 23 - 0
fs-service/src/main/resources/mapper/his/FsUserIntegralBatchMapper.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsUserIntegralBatchMapper">
+
+    <insert id="insertFsUserIntegralBatch" parameterType="com.fs.his.domain.FsUserIntegralBatch" useGeneratedKeys="true" keyProperty="batchId">
+        insert into fs_user_integral_batch (user_id, amount, remain, expire_time, source_log_id, create_time)
+        values (#{userId}, #{amount}, #{remain}, #{expireTime}, #{sourceLogId}, #{createTime})
+    </insert>
+
+    <select id="selectExpiredBatches" resultType="com.fs.his.domain.FsUserIntegralBatch">
+        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
+        limit #{limit}
+    </select>
+
+    <update id="updateRemain">
+        update fs_user_integral_batch set remain = #{remain} where batch_id = #{batchId}
+    </update>
+
+</mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/his/FsUserIntegralLogsMapper.xml

@@ -56,6 +56,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="integral != null">integral,</if>
             <if test="balance != null">balance,</if>
             <if test="businessId != null">business_id,</if>
+            <if test="expireTime != null">expire_time,</if>
+            <if test="platform != null">platform,</if>
             <if test="createTime != null">create_time,</if>
             <if test="businessType != null">business_type,</if>
             <if test="status != null">status,</if>
@@ -66,6 +68,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="integral != null">#{integral},</if>
             <if test="balance != null">#{balance},</if>
             <if test="businessId != null">#{businessId},</if>
+            <if test="expireTime != null">#{expireTime},</if>
+            <if test="platform != null">#{platform},</if>
             <if test="createTime != null">#{createTime},</if>
             <if test="businessType != null">#{businessType},</if>
             <if test="status != null">#{status},</if>

+ 38 - 1
fs-user-app/src/main/java/com/fs/app/controller/IntegralController.java

@@ -52,6 +52,9 @@ public class IntegralController extends  AppBaseController {
     private IFsIntegralCartService cartService;
     @Autowired
     private ISysConfigService configService;
+    @Autowired
+    private IIntegralTaskService integralTaskService;
+
     @ApiOperation("获取积分商品列表")
     @GetMapping("/getIntegralGoodsList")
     @Cacheable(value = "getIntegralGoodsList", key = "#param")
@@ -203,6 +206,37 @@ public class IntegralController extends  AppBaseController {
         return integralOrderService.payment(param, PaymentMethodEnum.H5_ALIPAY);
     }
 
+    @Login
+    @ApiOperation("任务中心")
+    @GetMapping("/taskCenter")
+    public R taskCenter(@RequestParam(value = "platform", defaultValue = "app") String platform) {
+        return integralTaskService.getTaskCenter(Long.parseLong(getUserId()), platform);
+    }
+
+    @Login
+    @ApiOperation("签到弹窗已展示")
+    @PostMapping("/signPopupShown")
+    public R signPopupShown() {
+        integralTaskService.markSignPopupShown(Long.parseLong(getUserId()));
+        return R.ok();
+    }
+
+    @Login
+    @ApiOperation("浏览商城开始计时")
+    @PostMapping("/browse/start")
+    public R browseStart(@RequestBody IntegralBrowseStartParam param) {
+        param.setPlatform(param.getPlatform() != null ? param.getPlatform() : "app");
+        return integralTaskService.startBrowse(Long.parseLong(getUserId()), param);
+    }
+
+    @Login
+    @ApiOperation("浏览商城完成计时")
+    @PostMapping("/browse/complete")
+    public R browseComplete(@RequestBody IntegralBrowseCompleteParam param) {
+        param.setPlatform(param.getPlatform() != null ? param.getPlatform() : "app");
+        return integralTaskService.completeBrowse(Long.parseLong(getUserId()), param);
+    }
+
     @Login
     @ApiOperation("获取用户签到记录")
     @GetMapping("/getUserSign")
@@ -221,7 +255,10 @@ public class IntegralController extends  AppBaseController {
     public R sign(HttpServletRequest request){
         FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
         Long integral=userSignService.sign(user);
-        return R.ok("签到获得" + integral + "积分");
+        return R.ok("签到获得" + integral + "积分")
+                .put("taskName", "签到奖励")
+                .put("rewardPoints", integral)
+                .put("toast", "今日签到积分已领取 +" + integral + "积分");
     }
 
     @Login