Sfoglia il codice sorgente

feat(tag): 实现看课自动打标签功能

- 新增看课中和完课标签的自动打标签逻辑
- 扩展FsTagUpdateQueue实体类,增加标签组和标签相关字段
- 修改FsTagUpdateQueueMapper,支持新字段的插入和更新操作
-优化FsTagUpdateServiceImpl,实现看课中和完课批量打标签处理- 在FsUserCourseVideoMapper中添加selectAllMap方法,提高查询效率
- 扩展FsUserCourseVideoVO,增加标签组和标签名称字段- 实现IQwUserCacheService接口,添加查询企微用户corpId的方法
- 修改QwWatchLogMapper和相关服务,修正统计方法命名-优化QwUserMapper,添加根据用户ID查询corpId的方法
- 在FsUserCourseVideoController中添加更新课堂视频的接口
- 扩展QwTagGroupMapper,支持根据名称查询标签组- 优化打标签任务处理逻辑,增加错误处理和日志记录
- 修改QwTagGroupMapper.xml,添加根据名称查询标签组的SQL- 在application-druid-bjczwh-test.yml中添加自动打标签配置项
-优化CompanyUserMapper.xml中的SQL查询语句格式- 在FsCourseWatchLogServiceImpl中实现看课记录的批量处理和标签更新
- 扩展QwUserCourseVideoServiceImpl,增加标签名称查询逻辑
xw 2 settimane fa
parent
commit
fde240976c
19 ha cambiato i file con 317 aggiunte e 94 eliminazioni
  1. 11 0
      fs-company/src/main/java/com/fs/company/controller/course/FsUserCourseVideoController.java
  2. 4 0
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  3. 5 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  4. 34 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  5. 30 5
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  6. 39 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoVO.java
  7. 2 0
      fs-service/src/main/java/com/fs/qw/cache/IQwUserCacheService.java
  8. 15 0
      fs-service/src/main/java/com/fs/qw/cache/impl/QwUserCacheServiceImpl.java
  9. 2 0
      fs-service/src/main/java/com/fs/qw/mapper/QwTagGroupMapper.java
  10. 3 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  11. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwWatchLogMapper.java
  12. 2 2
      fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java
  13. 27 0
      fs-service/src/main/java/com/fs/tag/domain/FsTagUpdateQueue.java
  14. 8 33
      fs-service/src/main/java/com/fs/tag/mapper/FsTagUpdateQueueMapper.java
  15. 113 52
      fs-service/src/main/java/com/fs/tag/service/impl/FsTagUpdateServiceImpl.java
  16. 9 0
      fs-service/src/main/resources/application-druid-bjczwh-test.yml
  17. 3 1
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml
  18. 6 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  19. 3 0
      fs-service/src/main/resources/mapper/qw/QwTagGroupMapper.xml

+ 11 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsUserCourseVideoController.java

@@ -87,6 +87,17 @@ public class FsUserCourseVideoController extends BaseController
         return toAjax(fsUserCourseVideoService.insertFsUserCourseVideo(fsUserCourseVideo));
     }
 
+    /**
+     * 更新课堂视频
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourseVideo:update')")
+    @Log(title = "更新课堂视频", businessType = BusinessType.UPDATE)
+    @PostMapping("/update")
+    public AjaxResult update(@RequestBody FsUserCourseVideo fsUserCourseVideo)
+    {
+
+        return toAjax(fsUserCourseVideoService.updateFsUserCourseVideo(fsUserCourseVideo));
+    }
     /**
      * 修改课堂视频
      */

+ 4 - 0
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -90,12 +90,14 @@ public class SendMsg {
         if (qwUserList.isEmpty()) {
             List<QwIpadServer> serverList = qwIpadServerMapper.selectList(new QueryWrapper<QwIpadServer>().eq("group_no", groupNo));
             if (serverList.isEmpty()) {
+                log.info("没找到可用的服务器 {} ",serverList);
                 return new ArrayList<>();
             }
             List<Long> serverIds = PubFun.listToNewList(serverList, QwIpadServer::getId);
             List<QwUser> qwUsers = qwUserMapper.selectList(new QueryWrapper<QwUser>().eq("send_msg_type", 1).eq("server_status", 1).eq("ipad_status", 1).in("server_id", serverIds));
             qwUserList.addAll(qwUsers);
         }
+        log.info("getQwUserList {}",JSON.toJSONString(qwUserList));
         return qwUserList;
     }
 
@@ -169,6 +171,7 @@ public class SendMsg {
         // 获取当前企微待发送记录
         List<QwSopLogs> qwSopLogList = qwSopLogsMapper.selectByQwUserId(qwUser.getId());
         if (qwSopLogList.isEmpty()) {
+            log.info("获取当前企微待发送记录为空");
             return;
         }
         // 获取企微用户
@@ -178,6 +181,7 @@ public class SendMsg {
         long end1 = System.currentTimeMillis();
         // 判断这个企微是否需要发送
         if (!sendServer.isSend(user, parentVo)) {
+            log.info("当前这个企微不需要发送 数据{}",user);
             return;
         }
         log.info("销售:{}, 消息:{}, 耗时: {}, 时间:{}", user.getQwUserName(), qwSopLogList.size(), end1 - start1, qwMap.get(qwUser.getId()));

+ 5 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -10,6 +10,7 @@ import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.param.FsUserCourseRedPageParam;
+import org.apache.ibatis.annotations.MapKey;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -265,4 +266,8 @@ public interface FsUserCourseVideoMapper
     List<FsUserCourseVideoAppletVO> getFsUserCourseVideoAppletVOListByIds(@Param("videoIds") List<Long> videoIds);
 
     FsUserCourseVO selectFsUserCourseVideoVoByVideoIdAndCourdeId(@Param("videoId") Long videoId,@Param("courseId") Long courseId);
+
+    @Select("select video_id,is_first,course_sort,tg_id,watching_tg_id,watched_tg_id,watching_tag_id,watched_tag_id,tag_group_id from fs_user_course_video")
+    @MapKey("videoId")
+    Map<Long, FsUserCourseVideo> selectAllMap();
 }

+ 34 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -52,6 +52,7 @@ import com.fs.system.service.ISysConfigService;
 import com.fs.system.vo.DictVO;
 import com.fs.tag.service.FsTagUpdateService;
 import com.hc.openapi.tool.util.StringUtils;
+import org.apache.commons.collections4.CollectionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -376,6 +377,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             String[] parts = key.split(":");
@@ -423,12 +425,17 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
             logs.add(watchLog);
         }
         batchUpdateFsUserCourseWatchLog(logs,100);
+
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
     public Long getFsUserVideoDuration(Long videoId){
         //将视频时长也存到redis
@@ -457,6 +464,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         Collection<String> keys = redisCache.keys("h5wxuser:watch:heartbeat:*");
         LocalDateTime now = LocalDateTime.now();
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> watchingLogs = new ArrayList<>();
         for (String key : keys) {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
             String[] parts = key.split(":");
@@ -481,10 +489,14 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 redisCache.deleteObject(key);
             }else {
                 watchLog.setLogType(1);
+                watchingLogs.add(watchLog);
             }
             logs.add(watchLog);
         }
         batchUpdateFsUserCourseWatchLog(logs,100);
+        if(CollectionUtils.isNotEmpty(watchingLogs)){
+            fsTagUpdateService.onCourseWatchingBatch(watchingLogs);
+        }
 
     }
 
@@ -508,6 +520,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             Long videoId=null;
@@ -556,6 +569,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
@@ -563,6 +577,12 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         }
 
         batchUpdateFsCourseWatchLogIsOpen(logs,100);
+
+
+        // 完课打标签
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
 
     public void batchUpdateFsCourseWatchLogIsOpen(List<FsCourseWatchLog> logs, int batchSize) {
@@ -784,6 +804,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             Long qwUserId=null;
@@ -842,6 +863,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
@@ -849,6 +871,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         }
 
         batchUpdateFsCourseWatchLog(logs,100);
+
+        // 完课打标签
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
 
     @Override
@@ -858,6 +885,8 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         Collection<String> keys = redisCache.keys("h5user:watch:heartbeat:*");
         LocalDateTime now = LocalDateTime.now();
         List<FsCourseWatchLog> logs = new ArrayList<>();
+
+        List<FsCourseWatchLog> watchingLogs = new ArrayList<>();
         for (String key : keys) {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
             //取key中数据
@@ -892,10 +921,15 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 redisCache.deleteObject(key);
             }else {
                 watchLog.setLogType(1);
+                watchingLogs.add(watchLog);
             }
             logs.add(watchLog);
         }
         batchUpdateFsCourseWatchLog(logs,100);
+
+        if(CollectionUtils.isNotEmpty(watchingLogs)){
+            fsTagUpdateService.onCourseWatchingBatch(watchingLogs);
+        }
     }
 
     @Override

+ 30 - 5
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -50,10 +50,7 @@ import com.fs.his.service.IFsUserWxService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.domain.*;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.mapper.QwGroupChatMapper;
-import com.fs.qw.mapper.QwGroupChatUserMapper;
-import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.mapper.*;
 import com.fs.qw.param.FsUserCourseRedPageParam;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwExternalContactService;
@@ -255,6 +252,11 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
     @Autowired
     private IFsUserCoursePeriodDaysService fsUserCoursePeriodDaysService;
 
+    @Autowired
+    private QwTagGroupMapper qwTagGroupMapper;
+    @Autowired
+    private QwTagMapper qwTagMapper;
+
 
 
 
@@ -923,7 +925,30 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
     @Override
     public List<FsUserCourseVideoVO> selectFsUserCourseVideoListByCourseIdAndCompany(FsUserCourseVideoParam fsUserCourseVideo) {
-        return fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseIdAndCompany(fsUserCourseVideo);
+        List<FsUserCourseVideoVO> fsUserCourseVideoVOS = fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseIdAndCompany(fsUserCourseVideo);
+        for (FsUserCourseVideoVO item : fsUserCourseVideoVOS) {
+            if(ObjectUtils.isNotNull(item.getTgId())){
+                QwTagGroup qwTagGroup = qwTagGroupMapper.selectQwTagGroupById(item.getTgId());
+                if(ObjectUtils.isNotNull(qwTagGroup)){
+                    item.setTagGroupName(qwTagGroup.getName());
+                }
+            }
+
+            if(ObjectUtils.isNotNull(item.getWatchingTgId())){
+                QwTag qwTag = qwTagMapper.selectQwTagById(item.getWatchingTgId());
+                if(ObjectUtils.isNotNull(qwTag)){
+                    item.setWatchingTagName(qwTag.getName());
+                }
+            }
+
+            if(ObjectUtils.isNotNull(item.getWatchedTgId())) {
+                QwTag qwTag = qwTagMapper.selectQwTagById(item.getWatchedTgId());
+                if(ObjectUtils.isNotNull(qwTag)){
+                    item.setWatchedTagName(qwTag.getName());
+                }
+            }
+        }
+        return fsUserCourseVideoVOS;
     }
 
     @Override

+ 39 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoVO.java

@@ -68,4 +68,43 @@ public class FsUserCourseVideoVO extends BaseEntity {
     private String redPacketMoney;
 
     private String companyRedPacketMoney;
+
+    /**
+     * 标签组表中的ID
+     */
+    private Long tgId;
+    /**
+     * 看课标签 表中的ID
+     */
+    private Long watchingTgId;
+    /**
+     * 完课标签 表中的ID
+     */
+    private Long watchedTgId;
+
+    /**
+     * 看课中标签ID
+     */
+    private String watchingTagId;
+    /**
+     * 完课标签ID
+     */
+    private String watchedTagId;
+    /**
+     * 标签组ID
+     */
+    private String tagGroupId;
+
+    /**
+     * 标签组名称
+     */
+    private String tagGroupName;
+    /**
+     * 看课标签
+     */
+    private String watchingTagName;
+    /**
+     * 完课标签
+     */
+    private String watchedTagName;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/qw/cache/IQwUserCacheService.java

@@ -2,4 +2,6 @@ package com.fs.qw.cache;
 
 public interface IQwUserCacheService {
     String queryQwUserNameByUserId(String userId);
+
+    String queryCorpIdByQwUserId(Long qwUserId);
 }

+ 15 - 0
fs-service/src/main/java/com/fs/qw/cache/impl/QwUserCacheServiceImpl.java

@@ -2,6 +2,7 @@ package com.fs.qw.cache.impl;
 
 import com.fs.qw.cache.IQwUserCacheService;
 import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwUserService;
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
@@ -14,6 +15,9 @@ import java.util.concurrent.TimeUnit;
 public class QwUserCacheServiceImpl implements IQwUserCacheService {
     @Autowired
     private IQwUserService qwUserService;
+
+    @Autowired
+    private QwUserMapper qwUserMapper;
     /**
      * 企微用户名昵称缓存类
      */
@@ -21,6 +25,12 @@ public class QwUserCacheServiceImpl implements IQwUserCacheService {
             .maximumSize(5000)
             .expireAfterWrite(12, TimeUnit.HOURS)
             .build();
+
+    private static final Cache<Long, String> QW_USER_ID_CACHE = Caffeine.newBuilder()
+            .maximumSize(5000)
+            .expireAfterWrite(5, TimeUnit.HOURS)
+            .build();
+
     @Override
     public String queryQwUserNameByUserId(String userId) {
         return QW_USER_CACHE.get(userId,e-> {
@@ -31,4 +41,9 @@ public class QwUserCacheServiceImpl implements IQwUserCacheService {
             return "-";
         });
     }
+
+    @Override
+    public String queryCorpIdByQwUserId(Long qwUserId) {
+        return QW_USER_ID_CACHE.get(qwUserId,e-> qwUserMapper.selectCorpIdById(qwUserId));
+    }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwTagGroupMapper.java

@@ -83,4 +83,6 @@ public interface QwTagGroupMapper
 
     List<QwTagGroupListVO> selectQwTagGroups(QwTagGroup qwTagGroup);
 
+    QwTagGroup selectQwTagGroupByName(@Param("tagGroup") String tagGroup, @Param("corpId") String corpId);
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java

@@ -442,4 +442,7 @@ public interface QwUserMapper extends BaseMapper<QwUser>
 
     List<QwOptionsVO> selectQwCompanyListOptionsVOBySys();
 
+    @Select("select corp_id from qw_user where id=#{id} limit 1")
+    String selectCorpIdById(@Param("id") Long id);
+
 }

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwWatchLogMapper.java

@@ -149,7 +149,7 @@ public interface QwWatchLogMapper extends BaseMapper<QwWatchLog>{
             "COUNT(CASE WHEN day = 1 and status in (1,2) THEN 1 END) AS d1Online,\n" +
             "COUNT(CASE WHEN day = 1 and status=2 THEN 1 END) AS d1Over\n" +
             " from qw_watch_log where qw_user_id=#{id} and DATE(line_time) = DATE(#{createTime})")
-    QwWatchLogStatisticsListVO selectQwWatchLogByQwUserId(@Param("id")Long id,@Param("createTime") Date createTime);
+    QwWatchLogStatisticsListVO selectQwExtCountByDayAndOther(@Param("id")Long id,@Param("createTime") Date createTime);
     @Select("SELECT count(1) from qw_watch_log where ext_id=#{id} and `day`=0 ")
     int selectQwWatchLogIsFirst(Long id);
     @Select("select \n" +

+ 2 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java

@@ -159,7 +159,7 @@ public class QwWatchLogServiceImpl extends ServiceImpl<QwWatchLogMapper, QwWatch
         for (QwWatchLogStatisticsListVO vo : vos) {
             Long id = vo.getId();
             Date createTime = vo.getCreateTime();
-            QwWatchLogStatisticsListVO stat = qwWatchLogMapper.selectQwWatchLogByQwUserId(id, createTime);
+            QwWatchLogStatisticsListVO stat = qwWatchLogMapper.selectQwExtCountByDayAndOther(id, createTime);
             vo.setD1Online(stat.getD1Online());
             vo.setD1Over(stat.getD1Over());
             vo.setFirstOnline(stat.getFirstOnline());
@@ -193,7 +193,7 @@ public class QwWatchLogServiceImpl extends ServiceImpl<QwWatchLogMapper, QwWatch
         for (QwWatchLogStatisticsListVO vo : vos) {
             Long id = vo.getId();
             Date createTime = vo.getCreateTime();
-            QwWatchLogStatisticsListVO stat = qwWatchLogMapper.selectQwWatchLogByQwUserId(id, createTime);
+            QwWatchLogStatisticsListVO stat = qwWatchLogMapper.selectQwExtCountByDayAndOther(id, createTime);
             vo.setD1Online(stat.getD1Online());
             vo.setD1Over(stat.getD1Over());
             vo.setFirstOnline(stat.getFirstOnline());

+ 27 - 0
fs-service/src/main/java/com/fs/tag/domain/FsTagUpdateQueue.java

@@ -86,4 +86,31 @@ public class FsTagUpdateQueue {
      * 下次执行时间
      */
     private LocalDateTime nextExecuteTime;
+
+
+    /**
+     * 看课中标签ID
+     */
+    private String watchingTagId;
+    /**
+     * 完课标签ID
+     */
+    private String watchedTagId;
+    /**
+     * 标签组ID
+     */
+    private String tagGroupId;
+
+    /**
+     * 标签组表中的ID
+     */
+    private Long tgId;
+    /**
+     * 看课标签 表中的ID
+     */
+    private Long watchingTgId;
+    /**
+     * 完课标签 表中的ID
+     */
+    private Long watchedTgId;
 }

+ 8 - 33
fs-service/src/main/java/com/fs/tag/mapper/FsTagUpdateQueueMapper.java

@@ -87,13 +87,13 @@ public interface FsTagUpdateQueueMapper {
     @Insert("<script>" +
             "INSERT IGNORE INTO fs_tag_update_queue (" +
             "course_log_id, is_first, course_id, tag_id, tag_name, operation_type, video_id, status, retry_count, " +
-            "corp_id, qw_user_id, qw_external_contact_id, fail_msg, payload, response, create_time, update_time, update_by, create_by, log_type" +
+            "corp_id, qw_user_id, qw_external_contact_id, fail_msg, payload, response, create_time, update_time, update_by, create_by, log_type,tg_id,watching_tg_id,watched_tg_id,watching_tag_id,watched_tag_id,tag_group_id" +
             ") VALUES " +
             "<foreach collection='list' item='item' separator=','>" +
             "(" +
             "#{item.courseLogId}, #{item.isFirst}, #{item.courseId}, #{item.tagId}, #{item.tagName}, #{item.operationType}, #{item.videoId}, #{item.status}, #{item.retryCount}, " +
             "#{item.corpId}, #{item.qwUserId}, #{item.qwExternalContactId}, #{item.failMsg}, #{item.payload}, #{item.response}, #{item.createTime}," +
-            " #{item.updateTime}, #{item.updateBy}, #{item.createBy}, #{item.logType}" +
+            " #{item.updateTime}, #{item.updateBy}, #{item.createBy}, #{item.logType},#{item.tgId},#{item.watchingTgId},#{item.watchedTgId},#{item.watchingTagId},#{item.watchedTagId},#{item.tagGroupId}" +
             ")" +
             "</foreach>" +
             "</script>")
@@ -128,44 +128,19 @@ public interface FsTagUpdateQueueMapper {
 
 
 
-    @Update("<script>" +
-            "UPDATE fs_tag_update_queue" +
-            "<set>" +
-            "<if test='courseLogId != null'>course_log_id = #{courseLogId},</if>" +
-            "<if test='isFirst != null'>is_first = #{isFirst},</if>" +
-            "<if test='courseId != null'>course_id = #{courseId},</if>" +
-            "<if test='tagId != null'>tag_id = #{tagId},</if>" +
-            "<if test='tagName != null'>tag_name = #{tagName},</if>" +
-            "<if test='operationType != null'>operation_type = #{operationType},</if>" +
-            "<if test='videoId != null'>video_id = #{videoId},</if>" +
-            "<if test='status != null'>status = #{status},</if>" +
-            "<if test='retryCount != null'>retry_count = #{retryCount},</if>" +
-            "<if test='corpId != null'>corp_id = #{corpId},</if>" +
-            "<if test='qwUserId != null'>qw_user_id = #{qwUserId},</if>" +
-            "<if test='qwExternalContactId != null'>qw_external_contact_id = #{qwExternalContactId},</if>" +
-            "<if test='failMsg != null'>fail_msg = #{failMsg},</if>" +
-            "<if test='payload != null'>payload = #{payload},</if>" +
-            "<if test='response != null'>response = #{response},</if>" +
-            "<if test='createTime != null'>create_time = #{createTime},</if>" +
-            "<if test='updateTime != null'>update_time = #{updateTime},</if>" +
-            "<if test='updateBy != null'>update_by = #{updateBy},</if>" +
-            "<if test='createBy != null'>create_by = #{createBy},</if>" +
-            "<if test='logType != null'>log_type = #{logType},</if>" +
-            "<if test='nextExecuteTime != null'>next_execute_time = #{nextExecuteTime},</if>" +
-            "</set>" +
-            " WHERE id = #{id}" +
-            "</script>")
-    int updateSelectiveById(FsTagUpdateQueue record);
-
+    /**
+     * 批量更新(循环调用单条更新避免SQL语法错误)
+     */
+    @Deprecated
     default int batchUpdateSelective(@Param("list") List<FsTagUpdateQueue> list) {
         if (list == null || list.isEmpty()) {
             return 0;
         }
         int count = 0;
         for (FsTagUpdateQueue item : list) {
-            count += updateSelectiveById(item);
+            count += updateSelective(item);
         }
         return count;
     }
 
-}
+}

+ 113 - 52
fs-service/src/main/java/com/fs/tag/service/impl/FsTagUpdateServiceImpl.java

@@ -5,12 +5,26 @@ import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCourse;
 import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.mapper.FsUserCourseMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.qw.cache.IQwUserCacheService;
 import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwTag;
+import com.fs.qw.domain.QwTagGroup;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwTagGroupMapper;
+import com.fs.qw.mapper.QwTagMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwTagGroupService;
+import com.fs.qw.vo.QwTagGroupAddParam;
+import com.fs.qw.vo.QwTagVO;
+import com.fs.qwApi.domain.QwAddTagResult;
 import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.domain.inner.InTag;
+import com.fs.qwApi.domain.inner.TagData;
+import com.fs.qwApi.param.QwAddTagParam;
 import com.fs.qwApi.param.QwEditUserTagParam;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.tag.domain.FsTagUpdateQueue;
@@ -19,7 +33,9 @@ import com.fs.tag.service.FsTagUpdateService;
 import com.google.common.util.concurrent.RateLimiter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.http.util.Asserts;
+import org.checkerframework.checker.signature.qual.PolySignature;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
@@ -28,6 +44,8 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.PostConstruct;
 import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.locks.ReentrantLock;
@@ -36,6 +54,7 @@ import java.util.concurrent.locks.ReentrantLock;
 @Service("fsTagUpdateService")
 public class FsTagUpdateServiceImpl implements FsTagUpdateService {
 
+
     @Autowired
     private FsTagUpdateQueueMapper fsTagUpdateQueueMapper;
     @Autowired
@@ -44,6 +63,19 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
     private IQwUserCacheService qwUserCacheService;
     @Autowired
     private QwApiService qwApiService;
+
+    @Autowired
+    private QwTagMapper qwTagMapper;
+
+    @Autowired
+    private QwTagGroupMapper qwTagGroupMapper;
+
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+
+    @Autowired
+    private FsUserCourseMapper fsUserCourseMapper;
+
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
 
@@ -52,9 +84,19 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
 
     @Value("${tag.rate.limit:30}")
     private Integer RATE_LIMIT_NUM;
+    /**
+     * 标签组最大数量
+     */
+    private static final Integer TAG_MAX_NUM = 100;
 
+    /**
+     * 接口限流
+     */
     private RateLimiter rateLimiter;
 
+    /**
+     * 看课自动打标签开关
+     */
     @Value("${qw.enableAutoTag:0}")
     private Integer enableAutoTag;
 
@@ -66,30 +108,35 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
     @Override
     @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
     public void onCourseWatchingBatch(List<FsCourseWatchLog> logs) {
+
         if(ObjectUtil.equal(enableAutoTag,0)){
             return;
         }
+        Map<Long, FsUserCourseVideo> courseVideoMap = fsUserCourseVideoMapper.selectAllMap();
+        // 用户(这里用户用的是企微外部联系人ID)+videoId+status 唯一
+
+        // 先导课看课记录
         List<FsTagUpdateQueue> batchData = new ArrayList<>();
         for (FsCourseWatchLog item : logs) {
             FsTagUpdateQueue task = new FsTagUpdateQueue();
             task.setCourseId(item.getCourseId());
             task.setVideoId(item.getVideoId());
             task.setCourseLogId(item.getLogId());
+            task.setTagId(null);
+            task.setTagName(null);
+
             task.setLogType(0);
             task.setOperationType(0);
             task.setStatus(0);
             task.setRetryCount(0);
-            task.setQwExternalContactId(String.valueOf(item.getQwExternalContactId()));
+             task.setQwExternalContactId(String.valueOf(item.getQwExternalContactId()));
             task.setQwUserId(String.valueOf(item.getQwUserId()));
-            
-            // 通过qwExternalContactId获取corpId
-            QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(item.getQwExternalContactId());
-            if(qwExternalContact != null && StringUtils.isNotNull(qwExternalContact.getCorpId())){
-                task.setCorpId(qwExternalContact.getCorpId());
+
+            FsUserCourseVideo fsUserCourseVideo = courseVideoMap.get(task.getVideoId());
+            String corpId = qwUserCacheService.queryCorpIdByQwUserId(item.getQwUserId());
+            if(StringUtils.isNotNull(corpId)){
+                task.setCorpId(corpId);
             }
-            
-            // 通过videoId查询视频信息
-            FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(task.getVideoId());
             if(ObjectUtils.isNull(fsUserCourseVideo)) {
                 String errorMsg = String.format("该条记录 %d 找不到对应的课堂视频", task.getVideoId());
                 log.error(errorMsg);
@@ -99,46 +146,55 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
                 batchData.add(task);
                 continue;
             }
-            task.setTagId(fsUserCourseVideo.getWatchingTagId());
+            task.setTagGroupId(fsUserCourseVideo.getTagGroupId());
+            task.setTgId(fsUserCourseVideo.getTgId());
+            task.setWatchingTagId(fsUserCourseVideo.getWatchingTagId());
+            task.setWatchedTagId(fsUserCourseVideo.getWatchedTagId());
+            task.setWatchingTgId(fsUserCourseVideo.getWatchingTgId());
+            task.setWatchedTgId(fsUserCourseVideo.getWatchedTgId());
+
             if(ObjectUtil.equal(fsUserCourseVideo.getIsFirst(),1)) {
                 task.setIsFirst(1);
             } else {
                 task.setIsFirst(0);
             }
+            task.setSort(fsUserCourseVideo.getCourseSort());
             batchData.add(task);
         }
-        if(CollectionUtils.isNotEmpty(batchData)){
-            fsTagUpdateQueueMapper.batchInsert(batchData);
-        }
+
+
+        fsTagUpdateQueueMapper.batchInsert(batchData);
     }
 
+
     @Override
     @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
     public void onCourseWatchFinishedBatch(List<FsCourseWatchLog> logs) {
         if(ObjectUtil.equal(enableAutoTag,0)){
             return;
         }
+        Map<Long, FsUserCourseVideo> courseVideoMap = fsUserCourseVideoMapper.selectAllMap();
+
+        // 先导课看课记录
         List<FsTagUpdateQueue> batchData = new ArrayList<>();
         for (FsCourseWatchLog item : logs) {
             FsTagUpdateQueue task = new FsTagUpdateQueue();
             task.setCourseId(item.getCourseId());
             task.setVideoId(item.getVideoId());
             task.setCourseLogId(item.getLogId());
-            task.setLogType(1);
+            task.setTagId(null);
+            task.setTagName(null);
             task.setOperationType(0);
             task.setStatus(0);
             task.setRetryCount(0);
-            task.setQwExternalContactId(String.valueOf(item.getQwExternalContactId()));
+             task.setQwExternalContactId(String.valueOf(item.getQwExternalContactId()));
             task.setQwUserId(String.valueOf(item.getQwUserId()));
-            
-            // 通过qwExternalContactId获取corpId
-            QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(item.getQwExternalContactId());
-            if(qwExternalContact != null && StringUtils.isNotNull(qwExternalContact.getCorpId())){
-                task.setCorpId(qwExternalContact.getCorpId());
+            task.setLogType(1);
+            String corpId = qwUserCacheService.queryCorpIdByQwUserId(item.getQwUserId());
+            if(StringUtils.isNotNull(corpId)){
+                task.setCorpId(corpId);
             }
-            
-            // 通过videoId查询视频信息
-            FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(task.getVideoId());
+            FsUserCourseVideo fsUserCourseVideo = courseVideoMap.get(task.getVideoId());
             if(ObjectUtils.isNull(fsUserCourseVideo)) {
                 String errorMsg = String.format("该条记录 %d 找不到对应的课堂视频", task.getVideoId());
                 log.error(errorMsg);
@@ -148,17 +204,24 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
                 batchData.add(task);
                 continue;
             }
-            task.setTagId(fsUserCourseVideo.getWatchedTagId());
+            task.setTagGroupId(fsUserCourseVideo.getTagGroupId());
+            task.setTgId(fsUserCourseVideo.getTgId());
+            task.setWatchingTagId(fsUserCourseVideo.getWatchingTagId());
+            task.setWatchedTagId(fsUserCourseVideo.getWatchedTagId());
+            task.setWatchingTgId(fsUserCourseVideo.getWatchingTgId());
+            task.setWatchedTgId(fsUserCourseVideo.getWatchedTgId());
+
             if(ObjectUtil.equal(fsUserCourseVideo.getIsFirst(),1)) {
                 task.setIsFirst(1);
             } else {
                 task.setIsFirst(0);
             }
+            task.setSort(fsUserCourseVideo.getCourseSort());
             batchData.add(task);
         }
-        if(CollectionUtils.isNotEmpty(batchData)){
-            fsTagUpdateQueueMapper.batchInsert(batchData);
-        }
+
+
+        fsTagUpdateQueueMapper.batchInsert(batchData);
     }
 
     @Override
@@ -171,6 +234,7 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
         ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();
         ExecutorService executor = Executors.newFixedThreadPool(TAG_THREAD_NUM);
         CountDownLatch latch = new CountDownLatch(tasks.size());
+
         for (FsTagUpdateQueue task : tasks) {
             executor.submit(() -> {
                 String lockKey = task.getCourseId() + "_" + task.getVideoId();
@@ -193,6 +257,7 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
             executor.shutdown();
             if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
                 executor.shutdownNow();
+                // 再次等待确保已经关闭
                 if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                     log.warn("线程池未能在预期时间内关闭");
                 }
@@ -201,57 +266,53 @@ public class FsTagUpdateServiceImpl implements FsTagUpdateService {
             executor.shutdownNow();
             Thread.currentThread().interrupt();
         }
+
         if(CollectionUtils.isNotEmpty(tasks)){
             fsTagUpdateQueueMapper.batchUpdateSelective(tasks);
         }
+
     }
 
     private void processSingleTask(FsTagUpdateQueue fsTagUpdateQueue) {
         try {
-            if(StringUtils.isEmpty(fsTagUpdateQueue.getTagId())){
-                throw new IllegalArgumentException("标签ID为空,请先在视频配置中设置标签");
-            }
+            // 调用企微API更新标签
             QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
-            Long externalContactId = Long.parseLong(fsTagUpdateQueue.getQwExternalContactId());
-            QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(externalContactId);
+            QwExternalContact qwExternalContact = qwExternalContactMapper
+                    .selectQwExternalContactById(Long.valueOf(fsTagUpdateQueue.getQwExternalContactId()));
             if(qwExternalContact == null) {
                 throw new IllegalArgumentException(String.format("企微外部联系人 %s 未找到!", fsTagUpdateQueue.getQwExternalContactId()));
             }
             qwEditUserTagParam.setUserid(qwExternalContact.getUserId());
             qwEditUserTagParam.setExternal_userid(qwExternalContact.getExternalUserId());
+
             rateLimiter.acquire();
-            qwEditUserTagParam.setAdd_tag(Collections.singletonList(fsTagUpdateQueue.getTagId()));
-            if(ObjectUtil.equal(fsTagUpdateQueue.getLogType(),1)){
-                FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(fsTagUpdateQueue.getVideoId());
-                if(fsUserCourseVideo != null && StringUtils.isNotEmpty(fsUserCourseVideo.getWatchingTagId())){
-                    qwEditUserTagParam.setRemove_tag(Collections.singletonList(fsUserCourseVideo.getWatchingTagId()));
-                }
+
+            // 如果是看课中
+            if(ObjectUtil.equal(fsTagUpdateQueue.getLogType(),0)){
+                qwEditUserTagParam.setAdd_tag(Collections.singletonList(fsTagUpdateQueue.getWatchingTagId()));
+            } else {
+                // 已完课
+                qwEditUserTagParam.setAdd_tag(Collections.singletonList(fsTagUpdateQueue.getWatchedTagId()));
+                qwEditUserTagParam.setRemove_tag(Collections.singletonList(fsTagUpdateQueue.getWatchingTagId()));
             }
+
             QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, fsTagUpdateQueue.getCorpId());
             fsTagUpdateQueue.setPayload(JSON.toJSONString(qwEditUserTagParam));
             fsTagUpdateQueue.setResponse(JSON.toJSONString(qwResult));
-            if(qwResult == null) {
-                throw new RuntimeException("企微API返回结果为null");
-            }
+            // 打标签成功
             if(ObjectUtil.equal(qwResult.getErrcode(),0)) {
                 fsTagUpdateQueue.setStatus(2);
                 fsTagUpdateQueue.setRetryCount(0);
-                log.info("打标签成功 - 外部联系人:{}, 标签ID:{}, 类型:{}", 
-                    fsTagUpdateQueue.getQwExternalContactId(), 
-                    fsTagUpdateQueue.getTagId(),
-                    fsTagUpdateQueue.getLogType() == 0 ? "看课中" : "完课");
             } else {
-                String errorMsg = String.format("打标签失败 errcode=%s, errmsg=%s", 
-                    qwResult.getErrcode(), qwResult.getErrmsg());
-                log.error("{},请求参数:{}", errorMsg, JSON.toJSONString(qwEditUserTagParam));
-                throw new RuntimeException(errorMsg);
+                throw new RuntimeException(String.format("打标签失败 原因: %s", JSON.toJSONString(qwResult)));
             }
         } catch (Exception e){
             fsTagUpdateQueue.setStatus(3);
             fsTagUpdateQueue.setRetryCount(fsTagUpdateQueue.getRetryCount()+1);
-            fsTagUpdateQueue.setFailMsg(ExceptionUtils.getStackTrace(e));
+            fsTagUpdateQueue.setFailMsg(ExceptionUtils.getFullStackTrace(e));
             fsTagUpdateQueue.setNextExecuteTime(LocalDateTime.now().plusHours(1));
-            log.error("处理打标签任务失败 - 任务ID:{}, 错误:{}", fsTagUpdateQueue.getId(), e.getMessage(), e);
         }
     }
+
+
 }

+ 9 - 0
fs-service/src/main/resources/application-druid-bjczwh-test.yml

@@ -153,3 +153,12 @@ openIM:
     url: https://web.im.ysya.top/api
 #是否为新商户,新商户不走mpOpenId
 isNewWxMerchant: true
+
+qw:
+    enableAutoTag: 1
+tag:
+    thread:
+        num: 5
+    rate:
+        limit: 30
+

+ 3 - 1
fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

@@ -434,7 +434,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
 
     <sql id="selectUserVo">
-        select u.user_id,u.company_id,u.qw_user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time,u.id_card, u.remark,u.user_type,u.open_id,u.qr_code_weixin,u.qr_code_wecom,u.jpush_id,u.domain,u.is_audit,u.address_id,
+        select u.user_id,u.company_id,u.qw_user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar,
+               u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by,
+               u.create_time,u.id_card, u.remark,u.user_type,u.open_id,u.qr_code_weixin,u.qr_code_wecom,u.jpush_id,u.domain,u.is_audit,u.address_id,
                d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
                r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,
                u.is_need_register_member, u.is_allowed_all_register,u.doctor_id

+ 6 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml

@@ -233,6 +233,12 @@
             <if test="listingEndTime != null">listing_end_time = #{listingEndTime},</if>
             <if test="projectId != null">project_id = #{projectId},</if>
             <if test="isFirst != null">is_first = #{isFirst},</if>
+            <if test="tagGroupId != null">tag_group_id = #{tagGroupId},</if>
+            <if test="watchingTagId != null">watching_tag_id = #{watchingTagId},</if>
+            <if test="watchedTagId != null">watched_tag_id = #{watchedTagId},</if>
+            <if test="tgId != null">tg_id = #{tgId},</if>
+            <if test="watchingTgId != null">watching_tg_id = #{watchingTgId},</if>
+            <if test="watchedTgId != null">watched_tg_id = #{watchedTgId},</if>
         </trim>
         where video_id = #{videoId}
     </update>

+ 3 - 0
fs-service/src/main/resources/mapper/qw/QwTagGroupMapper.xml

@@ -108,4 +108,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      </where>
         )
     </select>
+    <select id="selectQwTagGroupByName" resultType="com.fs.qw.domain.QwTagGroup">
+        select * from qw_tag_group where name=#{tagGroup} and corp_id=#{corpId} limit 1
+    </select>
 </mapper>