Просмотр исходного кода

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin 14 часов назад
Родитель
Сommit
82ca3e48ee
35 измененных файлов с 819 добавлено и 134 удалено
  1. 1 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  2. 1 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java
  3. 1 0
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  4. 2 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  5. 28 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveMixLiuTestOpenController.java
  6. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  7. 25 4
      fs-company/src/main/java/com/fs/user/FsUserAdminController.java
  8. 5 0
      fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java
  9. 70 2
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  10. 5 1
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  11. 2 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  12. 1 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  13. 3 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  14. 1 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  15. 39 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  16. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponUserScrmMapper.java
  17. 2 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  18. 12 0
      fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/request/V2TradePaymentScanpayQueryRequest.java
  19. 13 1
      fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java
  20. 6 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java
  21. 143 113
      fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java
  22. 2 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  23. 7 4
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  24. 39 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  25. 24 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  26. 2 1
      fs-service/src/main/java/com/fs/live/vo/LiveVo.java
  27. 3 0
      fs-service/src/main/java/com/fs/store/vo/h5/FsUserPageListVO.java
  28. 1 1
      fs-service/src/main/resources/application-config-druid-bjzm.yml
  29. 2 1
      fs-service/src/main/resources/mapper/MerchantAppConfigMapper.xml
  30. 2 1
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  31. 9 0
      fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml
  32. 293 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java
  33. 25 0
      fs-user-app/src/main/java/com/fs/app/vo/ReceivePointsVO.java
  34. 27 0
      fs-user-app/src/main/java/com/fs/app/vo/RemainingTimeVO.java
  35. 21 0
      fs-user-app/src/main/java/com/fs/app/vo/UpdateWatchDurationVO.java

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

@@ -1146,6 +1146,7 @@ public class Task {
                 V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                 request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                 request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
                 HuiFuQueryOrderResult o = null;
                 try {
                     o = huiFuService.queryOrder(request);

+ 1 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java

@@ -135,6 +135,7 @@ public class FsStorePaymentScrmController extends BaseController
             V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
             request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
             request.setOrgHfSeqId(payment.getTradeNo());
+            request.setAppId(payment.getAppId());
             HuiFuQueryOrderResult o = null;
             try {
                 o = huiFuService.queryOrder(request);

+ 1 - 0
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -266,6 +266,7 @@ public class LiveTask {
                 V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                 request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                 request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
                 HuiFuQueryOrderResult o = null;
                 try {
                     o = huiFuService.queryOrder(request);

+ 2 - 0
fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java

@@ -37,6 +37,8 @@ public class LiveKeysConstant {
     public static final Integer PRODUCT_DETAIL_CACHE_EXPIRE = 300; //商品详情缓存过期时间(秒)
 
     public static final String LIVE_TAG_MARK_CACHE = "live:tag:mark:%s"; //直播间打标签缓存,存储直播间ID、开始时间和视频时长
+    //记录用户观看直播间信息 直播间id、用户id、外部联系人id、qwUserId
+    public static final String LIVE_USER_WATCH_LOG_CACHE = "live:user:watch:log:%s:%s:%s:%s";
 
 
 }

+ 28 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveMixLiuTestOpenController.java

@@ -0,0 +1,28 @@
+package com.fs.company.controller.live;
+
+import com.fs.live.service.ILiveWatchUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author MixLiu
+ * @date 2025/12/18 下午3:26)
+ */
+
+@RestController
+@RequestMapping("/live/LiveMixLiuTestOpen")
+public class LiveMixLiuTestOpenController {
+
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
+    @GetMapping("/goToMarkUser/{liveId}")
+    public void goToMarkUser(@PathVariable Long liveId){
+        liveWatchUserService.qwTagMarkByLiveWatchLog(liveId);
+
+
+    }
+}

+ 1 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -132,6 +132,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/druid/**").anonymous()
                 .antMatchers("/qw/data/**").anonymous()
                 .antMatchers("/qw/user/selectCloudByCompany").anonymous()
+                .antMatchers("/live/LiveMixLiuTestOpen/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()

+ 25 - 4
fs-company/src/main/java/com/fs/user/FsUserAdminController.java

@@ -9,14 +9,14 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
-import com.fs.common.utils.StringUtils;
 import com.fs.company.cache.ICompanyUserCacheService;
+import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.course.dto.BatchSendCourseDTO;
 import com.fs.course.param.FsCourseLinkCreateParam;
+import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
-
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.PhoneUtil;
@@ -37,8 +37,6 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Date;
 
-import static com.fs.his.utils.PhoneUtil.encryptPhone;
-
 @Api(tags = "会员管理接口")
 @RestController
 @Slf4j
@@ -64,6 +62,9 @@ public class FsUserAdminController extends BaseController {
     @Autowired
     private OpenIMService openIMService;
 
+    @Autowired
+    private IFsUserCompanyUserService fsUserCompanyUserService;
+
     @PreAuthorize("@ss.hasPermi('user:fsUser:list')")
     @PostMapping("/list")
     @ApiOperation("会员列表(与移动端使用的相同查询)")
@@ -146,6 +147,15 @@ public class FsUserAdminController extends BaseController {
         return AjaxResult.success(fsUserService.selectFsUserPageListVOByUserId(userId));
     }
 
+    /**
+     * 获取项目用户详细信息
+     */
+    @GetMapping(value = "/member/{id}")
+    public AjaxResult getMemberInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserService.selectFsMemberUserPageListVOById(id));
+    }
+
     /**
      * 修改用户
      */
@@ -157,6 +167,17 @@ public class FsUserAdminController extends BaseController {
         return toAjax(fsUserService.updateFsUser(fsUser));
     }
 
+    /**
+     * 修改用户
+     */
+    @PreAuthorize("@ss.hasPermi('user:fsUser:edit')")
+    @Log(title = "用户", businessType = BusinessType.UPDATE)
+    @PutMapping("/member")
+    public AjaxResult editMemberUser(@RequestBody FsUserCompanyUser fsUser)
+    {
+        return toAjax(fsUserCompanyUserService.updateFsUserCompanyUser(fsUser));
+    }
+
 
     @ApiOperation("后台会员批量发送课程消息")
     @PostMapping("/batchSendCourse")

+ 5 - 0
fs-live-app/src/main/java/com/fs/framework/aspectj/LiveWatchUserAspect.java

@@ -8,6 +8,9 @@ import org.aspectj.lang.JoinPoint;
 import org.aspectj.lang.annotation.AfterReturning;
 import org.aspectj.lang.annotation.Aspect;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
 import java.util.Arrays;
@@ -17,11 +20,13 @@ import java.util.Set;
 @Aspect
 @Component
 @Slf4j
+@Order(Ordered.LOWEST_PRECEDENCE - 1)  // 调整切面优先级
 public class LiveWatchUserAspect {
 
 
 
     @Autowired
+    @Lazy
     private ILiveWatchUserService liveWatchUserService;
 
     @AfterReturning(pointcut = "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.insertLiveWatchUser(..)) || " +

+ 70 - 2
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -33,6 +33,7 @@ import javax.annotation.PostConstruct;
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -812,6 +813,7 @@ public class Task {
     @DistributeLock(key = "scanLiveWatchUserStatus", scene = "task")
     public void scanLiveWatchUserStatus() {
         try {
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             // 查询所有正在直播的直播间
             List<Live> activeLives = liveService.selectNoEndLiveList();
             if (activeLives == null || activeLives.isEmpty()) {
@@ -864,7 +866,7 @@ public class Task {
                             if (onlineSeconds == null || onlineSeconds <= 0) {
                                 continue;
                             }
-                            
+
                             // 获取用户的 companyId 和 companyUserId
                             LiveUserFirstEntry liveUserFirstEntry =
                                     liveUserFirstEntryService.selectEntityByLiveIdUserIdWithCache(liveId, userId);
@@ -878,7 +880,10 @@ public class Task {
                             if (qwUserId == null || qwUserId <= 0 || externalContactId == null || externalContactId <= 0) {
                                 continue;
                             }
-
+                            //更新最新用户活跃时间
+                            String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                            LocalDateTime now = LocalDateTime.now();
+                            redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                             // 使用 updateLiveWatchLogTypeByDuration 的逻辑更新观看记录状态
                             updateLiveWatchLogTypeByDuration(liveId, userId, qwUserId, externalContactId,
                                     onlineSeconds, totalVideoDuration, updateLog);
@@ -972,6 +977,69 @@ public class Task {
         }
     }
 
+    /**
+     * 每分钟扫描一次用户在线状态用于更新用户观看记录值
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    @DistributeLock(key = "updateLiveWatchUserStatus", scene = "task")
+    public void updateLiveWatchUserStatus() {
+        try {
+            Set<String> keys = redisCache.redisTemplate.keys("live:user:watch:log:*");
+            LocalDateTime now = LocalDateTime.now();
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            List<LiveWatchLog> updateLog = new ArrayList<>();
+            if (keys != null && !keys.isEmpty()) {
+                for (String key : keys) {
+                    String[] split = key.split(":");
+                    String cacheTime = redisCache.getCacheObject(key);
+                    //判断缓存的值是否已经距离现在超过一分钟
+                    if (StringUtils.isNotBlank(cacheTime)) {
+                        try {
+                            LocalDateTime cachedDateTime = LocalDateTime.parse(cacheTime, formatter);
+                            // 比较时间,判断是否超过1分钟(60秒)
+                            long secondsBetween = java.time.Duration.between(cachedDateTime, now).getSeconds();
+                            if (secondsBetween >= 60) {
+                                // 距离上次记录已超过1分钟,更新状态为看课中断
+                                // 查询 LiveWatchLog
+                                LiveWatchLog queryLog = new LiveWatchLog();
+                                queryLog.setLiveId(Long.valueOf(split[4]));
+                                queryLog.setQwUserId(String.valueOf(split[7]));
+                                queryLog.setExternalContactId(Long.valueOf(split[6]));
+                                queryLog.setLogType(1);
+                                List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
+                                if (logs != null && !logs.isEmpty()) {
+                                    for (LiveWatchLog log : logs) {
+                                        if (log.getLogType() != null && log.getLogType() == 2) {
+                                            continue;
+                                        }
+                                        log.setLogType(4);
+                                        updateLog.add(log);
+                                    }
+                                }
+                            }
+                        } catch (Exception e) {
+                            log.error("解析缓存时间失败: cacheTime={}, error={}", cacheTime, e.getMessage());
+                        }
+                    }
+                }
+                // 批量插入回放用户数据
+                if (!updateLog.isEmpty()) {
+                    int batchSize = 500;
+                    for (int i = 0; i < updateLog.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, updateLog.size());
+                        List<LiveWatchLog> batch = updateLog.subList(i, end);
+                        liveWatchLogService.batchUpdateLiveWatchLog(batch);
+                    }
+                    for (LiveWatchLog liveWatchLog : updateLog) {
+                        redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
+                    }
+                }
+            }
+        } catch (Exception ex) {
+            log.error("每分钟扫描一次用户在线状态用于更新用户观看记录值: error={}", ex.getMessage(), ex);
+        }
+    }
+
     /**
      * 批量同步Redis中的观看时长到数据库
      * 每2分钟执行一次,减少数据库压力

+ 5 - 1
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -39,6 +39,7 @@ import java.io.IOException;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.locks.Lock;
@@ -1191,7 +1192,7 @@ public class WebSocketServer {
             queryLog.setLiveId(liveId);
             queryLog.setQwUserId(String.valueOf(qwUserId));
             queryLog.setExternalContactId(externalContactId);
-
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
             List<LiveWatchLog> logs = liveWatchLogService.selectLiveWatchLogList(queryLog);
             if (logs != null && !logs.isEmpty()) {
                 for (LiveWatchLog log : logs) {
@@ -1199,6 +1200,9 @@ public class WebSocketServer {
                     if (log.getLogType() == null || log.getLogType() != 2) {
                         log.setLogType(1);
                         liveWatchLogService.updateLiveWatchLog(log);
+                        String liveUserWatchLogKey = String.format(LIVE_USER_WATCH_LOG_CACHE, liveId, userId,externalContactId,qwUserId);
+                        LocalDateTime now = LocalDateTime.now();
+                        redisCache.setCacheObject(liveUserWatchLogKey,formatter.format(now),5,TimeUnit.MINUTES);
                     }
                 }
             }

+ 2 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -50,6 +50,7 @@ import org.redisson.api.RedissonClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import com.fs.company.service.ICompanyService;
@@ -120,6 +121,7 @@ public class CompanyServiceImpl implements ICompanyService
     private TransactionTemplate transactionTemplate;
 
     @Autowired
+    @Lazy
     private ILiveService liveService;
     @Autowired
     private LiveOrderMapper liveOrderMapper;

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

@@ -32,6 +32,7 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_22(22,"首次完成积分商城下单"),
     TYPE_23(23,"管理员添加"),
     TYPE_24(24, "付费课程订阅"),
+    TYPE_25(25, "直播完课积分"),
     ;
 
 

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

@@ -188,6 +188,9 @@ public interface IFsUserService
 
     FsUserPageListVO selectFsUserPageListVOByUserId(Long userId);
 
+
+    FsUserPageListVO selectFsMemberUserPageListVOById(Long id);
+
     /**
      * 查询项目会员数据
      *

+ 1 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -389,6 +389,7 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
             V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
             request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(fsStorePayment.getCreateTime()));
             request.setOrgHfSeqId(fsStorePayment.getTradeNo());
+            request.setAppId(fsStorePayment.getAppId());
             HuiFuQueryOrderResult queryOrderResult = null;
             try {
                 queryOrderResult = huiFuService.queryOrder(request);

+ 39 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -1221,6 +1221,45 @@ public class FsUserServiceImpl implements IFsUserService {
         return item;
     }
 
+    @Override
+    public FsUserPageListVO selectFsMemberUserPageListVOById(Long id) {
+        FsUserCompanyUser userCompanyUser = userCompanyUserService.selectFsUserCompanyUserById(id);
+        if(userCompanyUser == null || userCompanyUser.getUserId() == null){
+            return null;
+        }
+
+        FsUser fsUser = fsUserMapper.selectFsUserByUserId(userCompanyUser.getUserId());
+        FsUserPageListVO item = new FsUserPageListVO();
+        BeanUtils.copyProperties(fsUser, item);
+        item.setNickname(fsUser.getNickName());
+        item.setStatus(userCompanyUser.getStatus()); // 取项目会员的状态
+        Map<Long, CompanyTag> tagMap = companyTagCacheService.queryAllTagMap();
+        if (item.getPhone() != null) {
+            item.setPhone(ParseUtils.parsePhone(item.getPhone()));
+        }
+        String userTagByUserId = null;
+        if (item.getUserId() != null && item.getCompanyUserId() != null) {
+            userTagByUserId = companyTagCacheService
+                    .findUserTagByUserId(item.getUserId(), item.getCompanyUserId());
+        }
+        if (StringUtils.isNotEmpty(userTagByUserId)) {
+            String[] split = userTagByUserId.split(",");
+            Set<String> tagNames = new HashSet<>();
+            for (String tag : split) {
+                if (StringUtils.isNotBlank(tag)) {
+                    Long tagL = Long.parseLong(tag);
+                    CompanyTag companyTag = tagMap.get(tagL);
+                    if (companyTag != null) {
+                        tagNames.add(companyTag.getTag());
+                    }
+                }
+            }
+            item.setTagIds(userTagByUserId);
+            item.setTag(String.join(",", tagNames));
+        }
+        return item;
+    }
+
     @Override
     @Transactional
     public void addMoney(FsStoreOrderScrm order) {

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponUserScrmMapper.java

@@ -76,7 +76,7 @@ public interface FsStoreCouponUserScrmMapper
             "and (find_in_set( #{maps.packageCateId},c.package_cate_ids) or c.package_cate_ids=0) " +
             "</if>" +
             "<if test = 'maps.useMinPrice != null     '> " +
-            "and cu.use_min_price &lt; #{maps.useMinPrice} " +
+            "and cu.use_min_price &lt;= #{maps.useMinPrice} " +
             "</if>" +
             "<if test = 'maps.couponType != null     '> " +
             "and c.type = #{maps.couponType} " +

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -59,6 +59,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
@@ -121,6 +122,7 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     private FsStoreProductCategoryScrmMapper fsStoreProductCategoryScrmMapper;
 
     @Autowired
+    @Lazy
     private ILiveService liveService;
 
     @Autowired

+ 12 - 0
fs-service/src/main/java/com/fs/huifuPay/sdk/opps/core/request/V2TradePaymentScanpayQueryRequest.java

@@ -47,6 +47,10 @@ public class V2TradePaymentScanpayQueryRequest extends BaseRequest {
     @JSONField(name = "party_order_id")
     private String partyOrderId;
 
+
+
+    String appId; //多小程序支付
+
     @Override
     public FunctionCodeEnum getFunctionCode() {
         return FunctionCodeEnum.V2_TRADE_PAYMENT_SCANPAY_QUERY;
@@ -121,4 +125,12 @@ public class V2TradePaymentScanpayQueryRequest extends BaseRequest {
         this.partyOrderId = partyOrderId;
     }
 
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
 }

+ 13 - 1
fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java

@@ -131,7 +131,19 @@ public class HuiFuServiceImpl implements HuiFuService {
 
     @Override
     public HuiFuQueryOrderResult queryOrder(V2TradePaymentScanpayQueryRequest request) throws Exception{
-        doInit(getMerConfig());
+        if (request.getAppId() != null) {
+            FsHfpayConfigMapper fsHfpayConfigMapper = SpringUtils.getBean(FsHfpayConfigMapper.class);
+            FsHfpayConfig fsHfpayConfig = fsHfpayConfigMapper.selectByAppId(request.getAppId());
+            if (fsHfpayConfig != null) {
+                //多汇付支付获取配置
+                doInit(getMerConfig(fsHfpayConfig));
+            } else {
+                //多小程序
+                doInit(getMerConfig());
+            }
+        } else {
+            doInit(getMerConfig());
+        }
         Map<String, Object> response = doExecute(request);
         String jsonString = JSONObject.toJSONString(response);
         HuiFuQueryOrderResult huiFuQueryOrderResult = JSON.parseObject(jsonString, HuiFuQueryOrderResult.class);

+ 6 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java

@@ -33,6 +33,12 @@ public interface LiveCompletionPointsRecordMapper {
      */
     LiveCompletionPointsRecord selectLatestByUser(@Param("userId") Long userId);
 
+    /**
+     * 查询用户在某直播间最近一次完课记录(不限制日期)
+     */
+    LiveCompletionPointsRecord selectLatestByUserAndLiveId(@Param("liveId") Long liveId, 
+                                                            @Param("userId") Long userId);
+
     /**
      * 查询用户未领取的完课记录列表
      */

+ 143 - 113
fs-service/src/main/java/com/fs/live/service/impl/LiveAfterSalesServiceImpl.java

@@ -85,6 +85,9 @@ import com.fs.live.service.ILiveAfterSalesService;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.interceptor.TransactionAspectSupport;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 售后记录Service业务层处理
@@ -162,6 +165,9 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
 //    @Autowired
 //    private FsStoreDeliversMapper fsStoreDeliversMapper;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
 
     /**
      * 查询售后记录
@@ -388,139 +394,163 @@ public class LiveAfterSalesServiceImpl implements ILiveAfterSalesService {
     @Transactional
     public R applyForAfterSales(String userId, LiveAfterSalesParam param) {
         log.info("申请退款请求信息:"+JSONUtil.toJsonStr(param));
-        LiveOrder order=liveOrderService.selectOrderIdByOrderCode(param.getOrderCode());
-        if(!order.getUserId().equals(userId)){
-            throw new CustomException("非法操作");
-        }
-        if(order.getStatus()==0){
-            return R.error("未支付订单不能申请售后");
-        }
-        if("1".equals(configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"))
-                && StringUtils.isEmpty(order.getExtendOrderId())
-                && !CloudHostUtils.hasCloudHostName("康年堂")){
-            log.info("erpOpen:{}",configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"));
-            return R.error("仓库未生成订单,暂时不能申请退款,请联系客服");
-        }
-        if(order.getStatus()== OrderInfoEnum.STATUS_NE3.getValue()){
-            return R.error("已取消订单不能申请售后");
-        }
-        if(order.getStatus()== OrderInfoEnum.STATUS_NE1.getValue()){
-            return R.error("已提交申请,等待处理");
-        }
+        
+        // 使用Redis分布式锁,确保同一订单只能有一个服务器处理申请售后 因为卓美有一个订单生成了三个售后订单,数据一摸一样,除了id
+        String lockKey = "live:afterSales:apply:" + param.getOrderCode();
+        RLock lock = redissonClient.getLock(lockKey);
+        
+        try {
+            // 尝试获取锁,等待时间3秒,锁过期时间30秒
+            boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
+            if (!locked) {
+                log.warn("申请售后订单锁获取失败,订单号:{}", param.getOrderCode());
+                return R.error("系统繁忙,请稍后重试");
+            }
+            
+            log.info("申请售后订单锁获取成功,订单号:{}", param.getOrderCode());
+            
+            LiveOrder order=liveOrderService.selectOrderIdByOrderCode(param.getOrderCode());
+            if(!order.getUserId().equals(userId)){
+                throw new CustomException("非法操作");
+            }
+            if(order.getStatus()==0){
+                return R.error("未支付订单不能申请售后");
+            }
+            if("1".equals(configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"))
+                    && StringUtils.isEmpty(order.getExtendOrderId())
+                    && !CloudHostUtils.hasCloudHostName("康年堂")){
+                log.info("erpOpen:{}",configUtil.generateConfigByKey(SysConfigEnum.HIS_CONFIG.getKey()).getString("erpOpen"));
+                return R.error("仓库未生成订单,暂时不能申请退款,请联系客服");
+            }
+            if(order.getStatus()== OrderInfoEnum.STATUS_NE3.getValue()){
+                return R.error("已取消订单不能申请售后");
+            }
+            if(order.getStatus()== OrderInfoEnum.STATUS_NE1.getValue()){
+                return R.error("已提交申请,等待处理");
+            }
 //        if(storeAfterSalesParam.getRefundAmount().compareTo(order.getPayPrice())==1){
 //            return R.error("退款金额不能大于支付金额");
 //        }
-        //已完成订单七天后不能申请退款
-        if(order.getStatus().equals(OrderInfoEnum.STATUS_3.getValue())) {
-            String json=configService.selectConfigByKey("store.config");
-            com.fs.hisStore.config.StoreConfig config=JSONUtil.toBean(json, com.fs.hisStore.config.StoreConfig.class);
-            //已完成订单
-            if (order.getFinishTime() != null) {
-                if (config.getStoreAfterSalesDay() != null && config.getStoreAfterSalesDay() > 0) {
-                    //判断完成时间是否超过指定时间
-                    Calendar calendarAfterSales = new GregorianCalendar();
-                    calendarAfterSales.setTime(order.getFinishTime());
-                    calendarAfterSales.add(calendarAfterSales.DATE, config.getStoreAfterSalesDay()); //把日期往后增加一天,整数  往后推,负数往前移动
-                    if (calendarAfterSales.getTime().getTime() < new Date().getTime()) {
-                        return R.error("此订单已超过售后时间,不能提交售后");
-                    }
+            //已完成订单七天后不能申请退款
+            if(order.getStatus().equals(OrderInfoEnum.STATUS_3.getValue())) {
+                String json=configService.selectConfigByKey("store.config");
+                com.fs.hisStore.config.StoreConfig config=JSONUtil.toBean(json, com.fs.hisStore.config.StoreConfig.class);
+                //已完成订单
+                if (order.getFinishTime() != null) {
+                    if (config.getStoreAfterSalesDay() != null && config.getStoreAfterSalesDay() > 0) {
+                        //判断完成时间是否超过指定时间
+                        Calendar calendarAfterSales = new GregorianCalendar();
+                        calendarAfterSales.setTime(order.getFinishTime());
+                        calendarAfterSales.add(calendarAfterSales.DATE, config.getStoreAfterSalesDay()); //把日期往后增加一天,整数  往后推,负数往前移动
+                        if (calendarAfterSales.getTime().getTime() < new Date().getTime()) {
+                            return R.error("此订单已超过售后时间,不能提交售后");
+                        }
 
+                    }
                 }
             }
-        }
-        //商品除去优惠后的总价格
-        //BigDecimal totalPrice = BigDecimal.ZERO;
-        //拿到所有的商品
-        List<LiveOrderItem> orderItems = liveOrderItemService.selectCheckedByOrderId(order.getOrderId());
-        for (LiveOrderItem item : orderItems) {
-            StoreOrderProductDTO cartInfo = JSONObject.parseObject(item.getJsonInfo(), StoreOrderProductDTO.class);
-            LiveAfterSalesProductParam prosuctParam = param.getProductList().stream().filter(p -> p.getProductId().equals(item.getProductId())).findFirst().orElse(new LiveAfterSalesProductParam());
-            if (prosuctParam.getProductId() != null) {
+            //商品除去优惠后的总价格
+            //BigDecimal totalPrice = BigDecimal.ZERO;
+            //拿到所有的商品
+            List<LiveOrderItem> orderItems = liveOrderItemService.selectCheckedByOrderId(order.getOrderId());
+            for (LiveOrderItem item : orderItems) {
+                StoreOrderProductDTO cartInfo = JSONObject.parseObject(item.getJsonInfo(), StoreOrderProductDTO.class);
+                LiveAfterSalesProductParam prosuctParam = param.getProductList().stream().filter(p -> p.getProductId().equals(item.getProductId())).findFirst().orElse(new LiveAfterSalesProductParam());
+                if (prosuctParam.getProductId() != null) {
 //                //商品优惠前总金额
 //                BigDecimal totalAmountOfGoods = NumberUtil.mul(cartInfo.getPrice(), item.getNum());
 //                //商品优惠总金额
 //                BigDecimal commodityDiscountAmount = NumberUtil.mul(NumberUtil.div(totalAmountOfGoods, NumberUtil.sub(order.getTotalPrice(), order.getPayPostage())), order.getCouponPrice());
 //                //商品优惠后总金额
 //                totalPrice = NumberUtil.add(totalPrice, NumberUtil.sub(totalAmountOfGoods, commodityDiscountAmount));
-                item.setIsAfterSales(1);
-                LiveOrderItem orderItem=new LiveOrderItem();
+                    item.setIsAfterSales(1);
+                    LiveOrderItem orderItem=new LiveOrderItem();
 //                BeanUtil.copyProperties(item, orderItem);
-                try {
-                    BeanUtils.copyProperties(orderItem,item);
-                } catch (IllegalAccessException e) {
-                    throw new RuntimeException(e);
-                } catch (InvocationTargetException e) {
-                    throw new RuntimeException(e);
-                }
-                liveOrderItemService.updateLiveOrderItem(orderItem);
+                    try {
+                        BeanUtils.copyProperties(orderItem,item);
+                    } catch (IllegalAccessException e) {
+                        throw new RuntimeException(e);
+                    } catch (InvocationTargetException e) {
+                        throw new RuntimeException(e);
+                    }
+                    liveOrderItemService.updateLiveOrderItem(orderItem);
 
+                }
             }
-        }
-        //更新订单状态
-        Integer orderStatus=order.getStatus();
-        order.setStatus(OrderInfoEnum.STATUS_NE1.getValue());
-        order.setRefundStatus(String.valueOf(OrderInfoEnum.REFUND_STATUS_1.getValue()));
-        order.setRefundExplain(param.getReasons());
-        order.setCancelReason(param.getExplains());
-        order.setIsAfterSales(1);
-        order.setRefundTime(new Date());
-        liveOrderService.updateLiveOrder(order);
-        //生成售后订单
-        LiveAfterSales storeAfterSales = new LiveAfterSales();
-        storeAfterSales.setOrderId(order.getOrderId());
-        storeAfterSales.setRefundAmount(param.getRefundAmount());
-        storeAfterSales.setRefundType(param.getServiceType());
-        storeAfterSales.setReasons(param.getReasons());
-        storeAfterSales.setExplains(param.getExplains());
-        storeAfterSales.setExplainImg(param.getExplainImg());
-        storeAfterSales.setStatus(AfterSalesStatusEnum.STATUS_0.getValue());
-        storeAfterSales.setSalesStatus(0);
-        storeAfterSales.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
-        storeAfterSales.setIsDel(0);
-        storeAfterSales.setOrderStatus(orderStatus);
-        storeAfterSales.setUserId(Long.valueOf(userId));
-        storeAfterSales.setCompanyId(order.getCompanyId());
-        storeAfterSales.setCompanyUserId(order.getCompanyUserId());
-        liveAfterSalesService.insertLiveAfterSales(storeAfterSales);
-        //售后商品详情
-        for (LiveAfterSalesProductParam productParam : param.getProductList()) {
-            LiveOrderItem item = orderItems.stream().filter(p -> p.getProductId().equals(productParam.getProductId())).findFirst().orElse(new LiveOrderItem());
-            LiveAfterSalesItem storeAfterSalesItem = new LiveAfterSalesItem();
-            storeAfterSalesItem.setAfterSalesId(storeAfterSales.getId());
-            storeAfterSalesItem.setProductId(item.getProductId());
-            storeAfterSalesItem.setJsonInfo(item.getJsonInfo());
-            storeAfterSalesItem.setIsDel(0);
-            liveAfterSalesItemMapper.insertLiveAfterSalesItem(storeAfterSalesItem);
-        }
-        //操作记录
-        LiveAfterSalesLogs storeAfterSalesStatus = new LiveAfterSalesLogs();
-        storeAfterSalesStatus.setStoreAfterSalesId(storeAfterSales.getId());
-        storeAfterSalesStatus.setChangeType(0);
-        storeAfterSalesStatus.setChangeMessage(AfterSalesStatusEnum.STATUS_0.getDesc());
-        storeAfterSalesStatus.setChangeTime(Timestamp.valueOf(LocalDateTime.now()));
-        FsUserScrm user=userService.selectFsUserById(Long.valueOf(userId));
-        storeAfterSalesStatus.setOperator(user.getNickname());
-        liveAfterSalesLogsMapper.insertLiveAfterSalesLogs(storeAfterSalesStatus);
+            //更新订单状态
+            Integer orderStatus=order.getStatus();
+            order.setStatus(OrderInfoEnum.STATUS_NE1.getValue());
+            order.setRefundStatus(String.valueOf(OrderInfoEnum.REFUND_STATUS_1.getValue()));
+            order.setRefundExplain(param.getReasons());
+            order.setCancelReason(param.getExplains());
+            order.setIsAfterSales(1);
+            order.setRefundTime(new Date());
+            liveOrderService.updateLiveOrder(order);
+            //生成售后订单
+            LiveAfterSales storeAfterSales = new LiveAfterSales();
+            storeAfterSales.setOrderId(order.getOrderId());
+            storeAfterSales.setRefundAmount(param.getRefundAmount());
+            storeAfterSales.setRefundType(param.getServiceType());
+            storeAfterSales.setReasons(param.getReasons());
+            storeAfterSales.setExplains(param.getExplains());
+            storeAfterSales.setExplainImg(param.getExplainImg());
+            storeAfterSales.setStatus(AfterSalesStatusEnum.STATUS_0.getValue());
+            storeAfterSales.setSalesStatus(0);
+            storeAfterSales.setCreateTime(Timestamp.valueOf(LocalDateTime.now()));
+            storeAfterSales.setIsDel(0);
+            storeAfterSales.setOrderStatus(orderStatus);
+            storeAfterSales.setUserId(Long.valueOf(userId));
+            storeAfterSales.setCompanyId(order.getCompanyId());
+            storeAfterSales.setCompanyUserId(order.getCompanyUserId());
+            liveAfterSalesService.insertLiveAfterSales(storeAfterSales);
+            //售后商品详情
+            for (LiveAfterSalesProductParam productParam : param.getProductList()) {
+                LiveOrderItem item = orderItems.stream().filter(p -> p.getProductId().equals(productParam.getProductId())).findFirst().orElse(new LiveOrderItem());
+                LiveAfterSalesItem storeAfterSalesItem = new LiveAfterSalesItem();
+                storeAfterSalesItem.setAfterSalesId(storeAfterSales.getId());
+                storeAfterSalesItem.setProductId(item.getProductId());
+                storeAfterSalesItem.setJsonInfo(item.getJsonInfo());
+                storeAfterSalesItem.setIsDel(0);
+                liveAfterSalesItemMapper.insertLiveAfterSalesItem(storeAfterSalesItem);
+            }
+            //操作记录
+            LiveAfterSalesLogs storeAfterSalesStatus = new LiveAfterSalesLogs();
+            storeAfterSalesStatus.setStoreAfterSalesId(storeAfterSales.getId());
+            storeAfterSalesStatus.setChangeType(0);
+            storeAfterSalesStatus.setChangeMessage(AfterSalesStatusEnum.STATUS_0.getDesc());
+            storeAfterSalesStatus.setChangeTime(Timestamp.valueOf(LocalDateTime.now()));
+            FsUserScrm user=userService.selectFsUserById(Long.valueOf(userId));
+            storeAfterSalesStatus.setOperator(user.getNickname());
+            liveAfterSalesLogsMapper.insertLiveAfterSalesLogs(storeAfterSalesStatus);
 
 //        //更新OMS
-        IErpOrderService erpOrderService = getErpService();
-        ErpRefundUpdateRequest request=new ErpRefundUpdateRequest();
-        request.setTid(order.getOrderCode());
-        request.setOid(order.getOrderCode());
-        request.setRefund_state(1);
-        request.setStoreAfterSalesId(storeAfterSales.getId());
-        request.setOrderStatus(orderStatus);
-        if (StringUtils.isNotBlank(order.getExtendOrderId())){
-            BaseResponse response=erpOrderService.refundUpdateLive(request);
-            if(response.getSuccess()){
-                return R.ok();
+            IErpOrderService erpOrderService = getErpService();
+            ErpRefundUpdateRequest request=new ErpRefundUpdateRequest();
+            request.setTid(order.getOrderCode());
+            request.setOid(order.getOrderCode());
+            request.setRefund_state(1);
+            request.setStoreAfterSalesId(storeAfterSales.getId());
+            request.setOrderStatus(orderStatus);
+            if (StringUtils.isNotBlank(order.getExtendOrderId())){
+                BaseResponse response=erpOrderService.refundUpdateLive(request);
+                if(response.getSuccess()){
+                    return R.ok();
+                }
+                else{
+                    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
+                    return R.error(response.getErrorDesc());
+                }
             }
-            else{
-                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-                return R.error(response.getErrorDesc());
+            return R.ok();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        } finally {
+            // 释放锁
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                log.info("申请售后订单锁释放成功,订单号:{}", param.getOrderCode());
             }
         }
-        return R.ok();
     }
 
     @Override

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

@@ -222,9 +222,9 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         integralLog.setUserId(userId);
         integralLog.setIntegral(Long.valueOf(record.getPointsAwarded()));
         integralLog.setBalance(newIntegral);
-        integralLog.setLogType(5); // 5-直播完课积分
+        integralLog.setLogType(25); // 5-直播完课积分
         integralLog.setBusinessId("live_completion_" + recordId); // 业务ID:直播完课记录ID
-        integralLog.setBusinessType(5); // 5-直播完课
+        integralLog.setBusinessType(25); // 5-直播完课
         integralLog.setStatus(1);
         integralLog.setCreateTime(new Date());
         fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLog);

+ 7 - 4
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -3193,7 +3193,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                         });
                         String s = (String) resultMap.get("package");
                         resultMap.put("packageValue", s);
-                        return R.ok().put("payType", param.getPayType()).put("result", resultMap);
+                        return R.ok().put("payType", param.getPayType()).put("result", resultMap).put("type", "hf");
                     } else {
                         return R.error(result.getResp_desc());
                     }
@@ -3532,6 +3532,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
                     request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                     request.setOrgHfSeqId(payment.getTradeNo());
+                    request.setAppId(payment.getAppId());
                     HuiFuQueryOrderResult queryOrderResult = null;
                     try {
                         queryOrderResult = huiFuService.queryOrder(request);
@@ -3624,9 +3625,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         }
 
         LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveOrder.getLiveId(), Long.parseLong(liveOrder.getUserId()));
-        liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
-        liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
-        liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        if (ObjectUtil.isNotEmpty(liveUserFirstEntry)) {
+            liveOrder.setCompanyId(liveUserFirstEntry.getCompanyId());
+            liveOrder.setCompanyUserId(liveUserFirstEntry.getCompanyUserId());
+            liveOrder.setTuiUserId(liveUserFirstEntry.getCompanyUserId());
+        }
         String orderSn = OrderCodeUtils.getOrderSn();
 //        String orderSn = "123"; // todo yhq
         log.info("订单生成:"+orderSn);

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

@@ -1,6 +1,7 @@
 package com.fs.live.service.impl;
 
 import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
@@ -56,6 +57,7 @@ import org.apache.http.impl.client.HttpClients;
 import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import com.fs.common.utils.sign.Md5Utils;
@@ -128,6 +130,10 @@ public class LiveServiceImpl implements ILiveService
     @Autowired
     LiveTagConfigMapper liveTagConfigMapper;
 
+    @Autowired
+    @Lazy
+    private ILiveWatchUserService liveWatchUserService;
+
     private static String TOKEN_VALID_CODE = "40001";
 
     private static volatile Integer version = 0;
@@ -294,6 +300,8 @@ public class LiveServiceImpl implements ILiveService
 			long seconds = live.getStartTime().until(now, ChronoUnit.SECONDS);
 			liveVo.setNowDuration(seconds);
 		}
+        Map<String, Integer> liveFlagWithCache = liveWatchUserService.getLiveFlagWithCache(live.getLiveId());
+        liveVo.setLiveFlag(liveFlagWithCache.get("liveFlag"));
         ThreadUtil.execute(()->{
             redisCache.deleteObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, live.getLiveId()));
             redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, live.getLiveId()), liveVo,LiveKeysConstant.LIVE_HOME_PAGE_DETAIL_EXPIRE, TimeUnit.SECONDS);
@@ -975,7 +983,38 @@ public class LiveServiceImpl implements ILiveService
             redisCache.redisTemplate.opsForZSet().add("live:auto_task:" + live.getLiveId(), JSON.toJSONString(liveAutoTask),liveAutoTask.getAbsValue().getTime());
             redisCache.redisTemplate.expire("live:auto_task:"+live.getLiveId(), 1, TimeUnit.DAYS);
         });
+        String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey);
+        String cacheKey2 = String.format(LiveKeysConstant.LIVE_FLAG_CACHE, live.getLiveId());
+        redisCache.deleteObject(cacheKey2);
+
+        // 将开启的直播间信息写入Redis缓存,用于打标签定时任务
+        try {
+            // 获取视频时长
+            Long videoDuration = 0L;
+            List<LiveVideo> videos = liveVideoService.listByLiveId(live.getLiveId(), 1);
+            if (CollUtil.isNotEmpty(videos)) {
+                videoDuration = videos.stream()
+                        .filter(v -> v.getDuration() != null)
+                        .mapToLong(LiveVideo::getDuration)
+                        .sum();
+            }
 
+            // 如果视频时长大于0,将直播间信息存入Redis
+            if (videoDuration > 0 && live.getStartTime() != null) {
+                Map<String, Object> tagMarkInfo = new HashMap<>();
+                tagMarkInfo.put("liveId", live.getLiveId());
+                tagMarkInfo.put("startTime", live.getStartTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli());
+                tagMarkInfo.put("videoDuration", videoDuration);
+
+                String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
+                redisCache.setCacheObject(tagMarkKey, JSON.toJSONString(tagMarkInfo), 24, TimeUnit.HOURS);
+                log.info("手动开直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}",
+                        live.getLiveId(), live.getStartTime(), videoDuration);
+            }
+        } catch (Exception e) {
+            log.error("手动开写入直播间打标签缓存失败: liveId={}, error={}", live.getLiveId(), e.getMessage(), e);
+        }
 
         return R.ok();
     }

+ 24 - 2
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -41,6 +41,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
@@ -399,7 +400,21 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         try {
             // 从 Redis 获取用户进入时间
             String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
-            Long entryTime = redisCache.getCacheObject(entryTimeKey);
+            Object entryTimeObj = redisCache.getCacheObject(entryTimeKey);
+            Long entryTime = null;
+            if (entryTimeObj != null) {
+                if (entryTimeObj instanceof Long) {
+                    entryTime = (Long) entryTimeObj;
+                } else if (entryTimeObj instanceof String) {
+                    try {
+                        entryTime = Long.parseLong((String) entryTimeObj);
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析进入时间字符串为Long: {}", entryTimeObj);
+                    }
+                } else if (entryTimeObj instanceof Number) {
+                    entryTime = ((Number) entryTimeObj).longValue();
+                }
+            }
             // 获取当前直播/回放状态
             Map<String, Integer> flagMap = this.getLiveFlagWithCache(liveId);
             Integer currentLiveFlag = flagMap.get("liveFlag");
@@ -863,6 +878,9 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
         //查询直播间的标签配置
         List<LiveTagItemVO> liveTagConfig = liveTagConfigMapper.getLiveTagListByliveId(liveId);
         log.info("处理直播间打标签: liveTagConfig={}", liveTagConfig);
+        if(null == liveTagConfig || liveTagConfig.isEmpty()){
+            return;
+        }
         /**
          * 8	回放已下单
          * 7	直播已下单
@@ -881,7 +899,10 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
                 ));
         //查询直播间的看课记录
         List<LiveWatchLog> liveWatchLogs = liveWatchLogMapper.selectLiveWatchLogByLiveId(liveId);
-
+        log.info("处理直播间打标签: liveWatchLogs={}", liveWatchLogs);
+        if(null == liveWatchLogs || liveWatchLogs.isEmpty()){
+            return;
+        }
         //根据配置给每位用户打上标签
         List<HandleUserTagVO> handleUserTagVOS = new ArrayList<>();
         liveWatchLogs.forEach(liveLog -> {
@@ -936,6 +957,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             }
             handleUserTagVOS.add(addItem);
         });
+        log.info("处理直播间打标签最终打标签:{}",handleUserTagVOS);
         handleUserTags2Qw(handleUserTagVOS);
     }
 

+ 2 - 1
fs-service/src/main/java/com/fs/live/vo/LiveVo.java

@@ -58,7 +58,8 @@ public class LiveVo {
     private Integer previewVideoType;
     private Long previewVideoId;
     private Integer globalVisible;
-    
+    private Integer liveFlag;
+
     /** 是否开启直播完课积分功能 */
     private Boolean completionPointsEnabled;
     

+ 3 - 0
fs-service/src/main/java/com/fs/store/vo/h5/FsUserPageListVO.java

@@ -106,4 +106,7 @@ public class FsUserPageListVO {
     @ApiModelProperty(value = "是否购买 1:是 0 否")
     private BigDecimal isBuy;
 
+    // 项目会员 主键
+    private Long id;
+
 }

+ 1 - 1
fs-service/src/main/resources/application-config-druid-bjzm.yml

@@ -92,7 +92,7 @@ headerImg:
 
 ipad:
   ipadUrl: http://aipad.klbycp.com
-  aiApi: 1212121212
+  aiApi: http://49.232.181.28:3000/api
   voiceApi:
   commonApi:
 wx_miniapp_temp:

+ 2 - 1
fs-service/src/main/resources/mapper/MerchantAppConfigMapper.xml

@@ -56,9 +56,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <where>
             <if test="merchantId != null and merchantId != ''"> and merchant_id = #{merchantId}</if>
             <if test="merchantType != null  and merchantType != ''"> and merchant_type = #{merchantType}</if>
-            <if test="appId != null  and appId != ''"> and app_id = #{appId}</if>
+            <if test="appId != null  and appId != ''"> and  FIND_IN_SET(#{appId},app_id)  </if>
             <if test="params.beginCreatedTime != null and params.beginCreatedTime != '' and params.endCreatedTime != null and params.endCreatedTime != ''"> and created_time between #{params.beginCreatedTime} and #{params.endCreatedTime}</if>
             <if test="isDeleted != null "> and is_deleted = #{isDeleted}</if>
+             <if test="id != null"> and id = #{id}</if>
         </where>
     </select>
 

+ 2 - 1
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -358,9 +358,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         fs_user.nick_name as nickname,
         fs_user.avatar,
         fs_user.phone,
-        fs_user.status,
         fs_user.create_time,
         fs_user.remark,
+        ucu.id,
+        ucu.status,
         ucu.company_user_id,
         ucu.company_id,
         ucu.project_id,

+ 9 - 0
fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml

@@ -89,6 +89,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         LIMIT 1
     </select>
 
+    <!-- 查询用户在某直播间最近一次完课记录(不限制日期) -->
+    <select id="selectLatestByUserAndLiveId" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC, id DESC
+        LIMIT 1
+    </select>
+
     <!-- 查询用户未领取的完课记录列表 -->
     <select id="selectUnreceivedByUser" resultMap="LiveCompletionPointsRecordResult">
         SELECT * FROM live_completion_points_record

+ 293 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java

@@ -1,15 +1,29 @@
 package com.fs.app.controller.live;
 
 import com.fs.app.controller.AppBaseController;
+import com.fs.app.vo.ReceivePointsVO;
+import com.fs.app.vo.RemainingTimeVO;
+import com.fs.app.vo.UpdateWatchDurationVO;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
+import com.fs.common.exception.base.BaseException;
 import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserService;
+import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.mapper.LiveCompletionPointsRecordMapper;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -27,6 +41,15 @@ public class LiveCompletionPointsController extends AppBaseController {
     @Autowired
     private IFsUserService fsUserService;
 
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private IFsUserIntegralLogsService fsUserIntegralLogsService;
+
+    @Autowired
+    private LiveCompletionPointsRecordMapper completionPointsRecordMapper;
+
     /**
      * 领取完课积分
      */
@@ -107,4 +130,274 @@ public class LiveCompletionPointsController extends AppBaseController {
             return R.error("创建失败: " + e.getMessage());
         }
     }
+
+    /**
+     * 第一个接口:查询当前用户当前直播间领取积分的剩余时长
+     * GET请求,传入直播间id
+     * 查询当前用户和当前直播间的积分记录(不限制日期),如果不存在就生成看课记录
+     */
+    @GetMapping("/remaining-time")
+    public R getRemainingTime(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        
+        try {
+            // 1. 获取直播间信息
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                return R.error("直播间不存在");
+            }
+
+            // 2. 查询当前用户和当前直播间的最近一次完课记录(不限制日期)
+            LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+            
+            // 3. 如果没有记录,查询直播间配置并生成记录
+            if (record == null) {
+                completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, null);
+                // 重新查询
+                record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+            }
+
+            // 4. 计算剩余时长
+            RemainingTimeVO vo = new RemainingTimeVO();
+            Long videoDuration = live.getDuration() != null ? live.getDuration() : 0L;
+            Long watchDuration = record != null && record.getWatchDuration() != null 
+                    ? record.getWatchDuration() : 0L;
+            
+            vo.setVideoDuration(videoDuration);
+            vo.setWatchDuration(watchDuration);
+            vo.setRemainingTime(Math.max(0, videoDuration - watchDuration));
+            vo.setHasReceived(record != null && record.getReceiveStatus() != null && record.getReceiveStatus() == 1);
+            
+            return R.ok().put("data", vo);
+        } catch (Exception e) {
+            return R.error("查询失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 第二个接口:更新用户的看课时长
+     * POST请求,传入直播间id和看课时长
+     * 更新用户看课completionPointsRecordService看课记录里面的时长
+     */
+    @PostMapping("/update-watch-duration")
+    public R updateWatchDuration(@RequestParam Long liveId, @RequestParam Long watchDuration) {
+        Long userId = Long.parseLong(getUserId());
+        
+        try {
+            // 1. 获取直播间信息
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                return R.error("直播间不存在");
+            }
+
+            // 2. 判断当前时间是否在直播期间(状态为2,直播中)
+            boolean isLiveInProgress = false;
+            LocalDateTime now = LocalDateTime.now();
+            
+            if (live.getStatus() != null && live.getStatus() == 2) {
+                // status=2 表示直播中
+                isLiveInProgress = true;
+            } else if (live.getStartTime() != null && live.getFinishTime() != null) {
+                // 判断当前时间是否在开播时间和结束时间之间
+                isLiveInProgress = (now.isAfter(live.getStartTime()) || now.isEqual(live.getStartTime()))
+                        && (now.isBefore(live.getFinishTime()) || now.isEqual(live.getFinishTime()));
+            }
+
+            if (!isLiveInProgress) {
+                return R.error("当前不在直播期间,无法更新看课时长");
+            }
+
+            // 3. 查询当前直播间的完课记录(不限制日期)
+            LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+            
+            // 4. 计算看课时长
+            Date updateTime = null;
+            if (record != null && record.getUpdateTime() != null) {
+                updateTime = record.getUpdateTime();
+            }
+            
+            // 判断更新时间与直播间开始时间的关系
+            Date startTime = live.getStartTime() != null 
+                    ? java.sql.Timestamp.valueOf(live.getStartTime()) : null;
+            
+            Date currentTime = new Date();
+            long timeDiff = 0L;
+            
+            if (updateTime != null && startTime != null) {
+                if (updateTime.before(startTime)) {
+                    // 更新时间小于直播间开始时间,使用直播间开始时间进行计算
+                    timeDiff = (currentTime.getTime() - startTime.getTime()) / 1000; // 转换为秒
+                } else {
+                    // 更新时间大于等于开播时间,按照更新时间进行计算
+                    timeDiff = (currentTime.getTime() - updateTime.getTime()) / 1000; // 转换为秒
+                }
+            } else if (startTime != null) {
+                // 没有更新记录,使用直播间开始时间计算
+                timeDiff = (currentTime.getTime() - startTime.getTime()) / 1000; // 转换为秒
+            }
+            
+            // 5. 如果请求传入的时间大于这个时间差,就使用计算出的看课时长,否则使用请求传入的时长
+            Long finalWatchDuration;
+            if (watchDuration > timeDiff) {
+                // 请求传入的时间大于时间差,使用计算出的看课时长
+                finalWatchDuration = timeDiff;
+            } else {
+                // 否则使用请求传入的时长
+                finalWatchDuration = watchDuration;
+            }
+            
+            // 6. 更新完课记录中的看课时长
+            if (record == null) {
+                // 如果没有记录,先创建记录
+                completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, finalWatchDuration);
+                record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+            } else {
+                // 更新现有记录的看课时长
+                Long currentWatchDuration = record.getWatchDuration() != null 
+                        ? record.getWatchDuration() : 0L;
+                record.setWatchDuration(currentWatchDuration + finalWatchDuration);
+                
+                // 重新计算完课比例
+                Long videoDuration = live.getDuration();
+                if (videoDuration != null && videoDuration > 0) {
+                    BigDecimal completionRate = BigDecimal.valueOf(record.getWatchDuration())
+                            .multiply(BigDecimal.valueOf(100))
+                            .divide(BigDecimal.valueOf(videoDuration), 2, java.math.RoundingMode.HALF_UP);
+                    if (completionRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+                        completionRate = BigDecimal.valueOf(100);
+                    }
+                    record.setCompletionRate(completionRate);
+                }
+                
+                completionPointsRecordMapper.updateRecord(record);
+            }
+
+            UpdateWatchDurationVO vo = new UpdateWatchDurationVO();
+            vo.setWatchDuration(finalWatchDuration);
+            vo.setTotalWatchDuration(record != null && record.getWatchDuration() != null 
+                    ? record.getWatchDuration() : finalWatchDuration);
+            
+            return R.ok().put("data", vo);
+        } catch (Exception e) {
+            return R.error("更新失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 第三个接口:用户领取看课积分
+     * POST请求,传入直播间id
+     * 只查询这个直播间的看课记录(不限制日期),检查是否达到了完课标准
+     * 达到了更新了看课记录里面的领取条件,给用户发积分
+     * 没达到,返回报错
+     */
+    @PostMapping("/receive-points")
+    @RepeatSubmit
+    public R receivePoints(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        
+        try {
+            // 1. 查询当前用户和当前直播间的最近一次完课记录(不限制日期)
+            LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+            
+            if (record == null) {
+                return R.error("您还没有看课记录,无法领取积分");
+            }
+
+            // 2. 获取直播间信息和配置
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                return R.error("直播间不存在");
+            }
+
+            // 3. 检查看课记录里面的时长是否达到完课标准
+            Long watchDuration = record.getWatchDuration();
+            if (watchDuration == null || watchDuration <= 0) {
+                return R.error("您的看课时长不足,无法领取积分");
+            }
+
+            // 4. 检查完课比例是否达到标准
+            BigDecimal completionRate = record.getCompletionRate();
+            if (completionRate == null) {
+                // 重新计算完课比例
+                Long videoDuration = live.getDuration();
+                if (videoDuration == null || videoDuration <= 0) {
+                    return R.error("直播间视频时长配置错误");
+                }
+                completionRate = BigDecimal.valueOf(watchDuration)
+                        .multiply(BigDecimal.valueOf(100))
+                        .divide(BigDecimal.valueOf(videoDuration), 2, java.math.RoundingMode.HALF_UP);
+                if (completionRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+                    completionRate = BigDecimal.valueOf(100);
+                }
+                record.setCompletionRate(completionRate);
+            }
+
+            // 5. 从直播间配置获取完课标准
+            String configJson = live.getConfigJson();
+            Integer requiredCompletionRate = null;
+            if (configJson != null && !configJson.isEmpty()) {
+                try {
+                    com.alibaba.fastjson.JSONObject jsonConfig = com.alibaba.fastjson.JSON.parseObject(configJson);
+                    requiredCompletionRate = jsonConfig.getInteger("completionRate");
+                } catch (Exception e) {
+                    // 解析失败,忽略
+                }
+            }
+
+            // 6. 判断是否达到完课标准
+            if (requiredCompletionRate != null && completionRate.compareTo(BigDecimal.valueOf(requiredCompletionRate)) < 0) {
+                return R.error("您的完课比例未达到标准(" + requiredCompletionRate + "%),当前完课比例:" + completionRate + "%");
+            }
+
+            // 7. 检查是否已领取
+            if (record.getReceiveStatus() != null && record.getReceiveStatus() == 1) {
+                return R.error("该完课积分已领取");
+            }
+
+            // 8. 领取积分(更新看课记录的领取状态,给用户加积分)
+            LiveCompletionPointsRecord receivedRecord = completionPointsRecordService.receiveCompletionPoints(record.getId(), userId);
+
+            ReceivePointsVO vo = new ReceivePointsVO();
+            vo.setRecord(receivedRecord);
+            vo.setPoints(receivedRecord.getPointsAwarded());
+            vo.setContinuousDays(receivedRecord.getContinuousDays());
+            
+            return R.ok().put("data", vo);
+        } catch (BaseException e) {
+            return R.error(e.getMessage());
+        } catch (Exception e) {
+            return R.error("领取失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 第四个接口:查询用户自己的积分记录
+     * GET请求
+     */
+    @GetMapping("/integral-logs")
+    public R getIntegralLogs(@RequestParam(required = false) Integer type) {
+        Long userId = Long.parseLong(getUserId());
+        
+        try {
+            FsUserIntegralLogs query = new FsUserIntegralLogs();
+            query.setUserId(userId);
+            
+            List<FsUserIntegralLogs> logs = fsUserIntegralLogsService.selectFsUserIntegralLogsList(query);
+            
+            // 如果指定了类型,进行过滤
+            if (type != null) {
+                if (type == 1) {
+                    // 获得积分(积分大于0)
+                    logs.removeIf(log -> log.getIntegral() == null || log.getIntegral() <= 0);
+                } else if (type == 2) {
+                    // 消耗积分(积分小于0)
+                    logs.removeIf(log -> log.getIntegral() == null || log.getIntegral() >= 0);
+                }
+            }
+            
+            return R.ok().put("data", logs);
+        } catch (Exception e) {
+            return R.error("查询失败: " + e.getMessage());
+        }
+    }
 }

+ 25 - 0
fs-user-app/src/main/java/com/fs/app/vo/ReceivePointsVO.java

@@ -0,0 +1,25 @@
+package com.fs.app.vo;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 领取积分返回VO
+ */
+@Data
+public class ReceivePointsVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /** 完课记录 */
+    private LiveCompletionPointsRecord record;
+    
+    /** 获得的积分 */
+    private Integer points;
+    
+    /** 连续天数 */
+    private Integer continuousDays;
+}
+

+ 27 - 0
fs-user-app/src/main/java/com/fs/app/vo/RemainingTimeVO.java

@@ -0,0 +1,27 @@
+package com.fs.app.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 剩余时长返回VO
+ */
+@Data
+public class RemainingTimeVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /** 剩余时长(秒) */
+    private Long remainingTime;
+    
+    /** 已观看时长(秒) */
+    private Long watchDuration;
+    
+    /** 视频总时长(秒) */
+    private Long videoDuration;
+    
+    /** 是否领取过 */
+    private Boolean hasReceived;
+}
+

+ 21 - 0
fs-user-app/src/main/java/com/fs/app/vo/UpdateWatchDurationVO.java

@@ -0,0 +1,21 @@
+package com.fs.app.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 更新看课时长返回VO
+ */
+@Data
+public class UpdateWatchDurationVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+    
+    /** 本次更新的看课时长(秒) */
+    private Long watchDuration;
+    
+    /** 总看课时长(秒) */
+    private Long totalWatchDuration;
+}
+