瀏覽代碼

Merge remote-tracking branch 'origin/master'

xgb 4 天之前
父節點
當前提交
19dfb97202
共有 20 個文件被更改,包括 878 次插入28 次删除
  1. 5 0
      fs-ipad-task/pom.xml
  2. 4 0
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  3. 52 0
      fs-ipad-task/src/test/java/com/fs/app/task/SendMsgTest.java
  4. 6 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  5. 41 2
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  6. 3 3
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.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. 1 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. 1 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java
  13. 89 0
      fs-service/src/main/java/com/fs/tag/domain/FsTagUpdateQueue.java
  14. 161 0
      fs-service/src/main/java/com/fs/tag/mapper/FsTagUpdateQueueMapper.java
  15. 29 0
      fs-service/src/main/java/com/fs/tag/service/FsTagUpdateService.java
  16. 457 0
      fs-service/src/main/java/com/fs/tag/service/impl/FsTagUpdateServiceImpl.java
  17. 2 2
      fs-service/src/main/resources/application-dev.yml
  18. 3 1
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml
  19. 3 0
      fs-service/src/main/resources/mapper/qw/QwTagGroupMapper.xml
  20. 0 17
      fs-service/src/test/java/com/fs/his/service/impl/FsUserServiceImplTest.java

+ 5 - 0
fs-ipad-task/pom.xml

@@ -105,6 +105,11 @@
             <groupId>com.fs</groupId>
             <artifactId>fs-service</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

+ 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()));

+ 52 - 0
fs-ipad-task/src/test/java/com/fs/app/task/SendMsgTest.java

@@ -0,0 +1,52 @@
+package com.fs.app.task;
+
+import com.fs.FsIpadTaskApplication;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.tag.service.FsTagUpdateService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = FsIpadTaskApplication.class)
+@RequiredArgsConstructor
+@Slf4j
+public class SendMsgTest {
+    @Autowired
+    private SendMsg sendMsg;
+
+    @Autowired
+    private FsTagUpdateService fsTagUpdateService;
+
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Test
+    public void sendMsg2() {
+        sendMsg.sendMsg2();
+    }
+
+    @Test
+    public void testLogWrite(){
+        List<Long> testLogIds = Arrays.asList(177L,180L,182L,183L,184L,185L);
+
+        for(Long logId : testLogIds){
+            FsCourseWatchLog fsCourseWatchLog = fsCourseWatchLogMapper.selectFsCourseWatchLogByLogId(logId);
+            fsTagUpdateService.onCourseWatchFinishedBatch(Collections.singletonList(fsCourseWatchLog));
+        }
+    }
+
+    @Test
+    public void handleData(){
+        fsTagUpdateService.handleData();
+    }
+}

+ 6 - 1
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;
@@ -145,7 +146,7 @@ public interface FsUserCourseVideoMapper
     Long selectFsUserCourseVideoByCourseSort(@Param("courseId")Long courseId, @Param("courseSort")Long courseSort);
 
 
-    @Select("select video_id dict_value, title dict_label , thumbnail  dict_img_url from fs_user_course_video where course_id=#{id} and is_del = 0 order by course_sort")
+    @Select("select video_id dict_value, title dict_label  from fs_user_course_video where course_id=#{id} and is_del = 0 order by course_sort")
     List<OptionsVO> selectFsUserCourseVodeAllList(Long id);
 
     @Select({"<script> " +
@@ -255,4 +256,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 from fs_user_course_video")
+    @MapKey("videoId")
+    Map<Long, FsUserCourseVideo> selectAllMap();
 }

+ 41 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -48,10 +48,13 @@ import com.fs.sop.mapper.SopUserLogsMapper;
 import com.fs.store.service.cache.IFsUserCacheService;
 import com.fs.store.service.cache.IFsUserCourseCacheService;
 import com.fs.system.service.ISysConfigService;
+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;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
@@ -89,7 +92,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
     @Autowired
-    RedisCache redisCache;
+    private RedisCache redisCache;
     @Autowired
     private IQwExternalContactCacheService qwExternalContactCacheService;
     @Autowired
@@ -130,6 +133,9 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private IFsUserCoursePeriodDaysService userCoursePeriodDaysService;
 
+    @Autowired
+    private FsTagUpdateService fsTagUpdateService;
+
     /**
      * 查询短链课程看课记录
      *
@@ -367,6 +373,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(":");
@@ -405,12 +412,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
@@ -439,6 +451,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(":");
@@ -463,11 +476,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);
+        }
     }
 
     @Override
@@ -490,6 +506,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;
@@ -538,6 +555,8 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
@@ -545,6 +564,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         }
 
         batchUpdateFsCourseWatchLogIsOpen(logs,100);
+
+        // 完课打标签
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
 
     public void batchUpdateFsCourseWatchLogIsOpen(List<FsCourseWatchLog> logs, int batchSize) {
@@ -766,6 +790,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;
@@ -824,6 +849,8 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
@@ -831,6 +858,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         }
 
         batchUpdateFsCourseWatchLog(logs,100);
+
+        // 完课打标签
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
 
     @Override
@@ -840,6 +872,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中数据
@@ -874,10 +908,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

+ 3 - 3
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -459,9 +459,9 @@ public class AiHookServiceImpl implements AiHookService {
                 return R.ok();
             }
 
-            //对用户处理的内容做处理
-            String maskedContent = processContent(qwContent);
-            String contentEmj = replaceWxEmo(maskedContent);
+            //对用户处理的内容做处理,去除手机号替换
+            //String maskedContent = processContent(qwContent);
+            String contentEmj = replaceWxEmo(qwContent);
             if(!contentEmj.contains("表情包")){
                 if(!contentEmj.isEmpty()){
                     addSaveAiMsg(1,1,contentEmj,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);

+ 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));
+    }
 }

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

@@ -83,4 +83,5 @@ 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

@@ -19,6 +19,7 @@ import org.apache.ibatis.annotations.Update;
 import org.springframework.stereotype.Repository;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 企微用户Mapper接口
@@ -448,5 +449,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);
     List<Long> selectDeptByParentId(@Param("deptId")Long deptId,@Param("corpId") String corpId);
 }

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

@@ -142,7 +142,7 @@ public interface QwWatchLogMapper extends BaseMapper<QwWatchLog>{
             "ORDER BY\n" +
             "    DATE(qec.create_time) "+
             "</script>"})
-    List<QwWatchLogStatisticsListVO> selectQwExtCountByDayAnd(FsCourseWatchLogListParam param);
+    List<QwWatchLogStatisticsListVO> selectQwExtCountByDayAndOther(FsCourseWatchLogListParam param);
     @Select("select \n" +
             "COUNT(CASE WHEN day = 0 and status in (1,2) THEN 1 END) AS firstOnline,\n" +
             "COUNT(CASE WHEN day = 0 and status=2 THEN 1 END) AS firstOver,\n" +

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

@@ -155,7 +155,7 @@ public class QwWatchLogServiceImpl extends ServiceImpl<QwWatchLogMapper, QwWatch
         if (param.getCompanyUserId()!=null){
             param.setIds(companyUserMapper.selectQwUserIdsByCompany(param.getCompanyUserId()));
         }
-        List<QwWatchLogStatisticsListVO> vos = qwWatchLogMapper.selectQwExtCountByDayAnd(param);
+        List<QwWatchLogStatisticsListVO> vos = qwWatchLogMapper.selectQwExtCountByDayAndOther(param);
         for (QwWatchLogStatisticsListVO vo : vos) {
             Long id = vo.getId();
             Date createTime = vo.getCreateTime();

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

@@ -0,0 +1,89 @@
+package com.fs.tag.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 标签更新对列表
+ */
+@Data
+public class FsTagUpdateQueue {
+    /** id */
+    private Long id;
+
+    /** 看课日志id */
+    private Long courseLogId;
+
+    /** 是否是先导课*/
+    private Integer isFirst;
+
+    /**
+     * 课程排序
+     */
+    private Long sort;
+
+    /** 课程ID */
+    private Long courseId;
+
+    /** 标签id */
+    private String tagId;
+
+    /** 标签名称 */
+    private String tagName;
+
+    /** 操作类型(0 ADD 1 REMOVE) 默认0 */
+    private Integer operationType;
+
+    /** 视频ID */
+    private Long videoId;
+
+    /** 0未处理 1处理中 2成功 3失败 */
+    private Integer status;
+
+    /** 重试次数 */
+    private Integer retryCount;
+
+    /** 企微主体id */
+    private String corpId;
+
+    /** 企微user_id */
+    private Long qwUserId;
+
+    /** 企微外部联系人id */
+    private Long qwExternalContactId;
+    /**
+     * 记录类型
+     * 0 正在看课
+     * 1 已完课
+     */
+    private Integer logType;
+
+    /** 失败原因 */
+    private String failMsg;
+
+    /** 请求参数 */
+    private String payload;
+
+    /** 返回结果 */
+    private String response;
+
+    /** 创建时间 */
+    private LocalDateTime createTime;
+
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+
+    /** 更新人 */
+    private Long updateBy;
+
+    /** 创建人 */
+    private Long createBy;
+
+    /**
+     * 下次执行时间
+     */
+    private LocalDateTime nextExecuteTime;
+
+}

+ 161 - 0
fs-service/src/main/java/com/fs/tag/mapper/FsTagUpdateQueueMapper.java

@@ -0,0 +1,161 @@
+package com.fs.tag.mapper;
+
+import com.fs.tag.domain.FsTagUpdateQueue;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+/**
+ * 标签更新对列表 Mapper
+ */
+@Mapper
+public interface FsTagUpdateQueueMapper {
+
+    @Select("select * from fs_tag_update_queue where retry_count < 3 and status in (0,3) and (next_execute_time < now() or next_execute_time is null) limit 500")
+    List<FsTagUpdateQueue> selectPending();
+
+    @Select("<script>" +
+            "SELECT * FROM fs_tag_update_queue " +
+            "<where>" +
+            "<if test='id != null'> AND id = #{id} </if>" +
+            "<if test='courseLogId != null'> AND course_log_id = #{courseLogId} </if>" +
+            "<if test='courseId != null'> AND course_id = #{courseId} </if>" +
+            "<if test='tagId != null'> AND tag_id = #{tagId} </if>" +
+            "<if test='tagName != null'> AND tag_name = #{tagName} </if>" +
+            "<if test='operationType != null'> AND operation_type = #{operationType} </if>" +
+            "<if test='videoId != null'> AND video_id = #{videoId} </if>" +
+            "<if test='status != null'> AND status = #{status} </if>" +
+            "<if test='retryCount != null'> AND retry_count = #{retryCount} </if>" +
+            "<if test='corpId != null'> AND corp_id = #{corpId} </if>" +
+            "<if test='qwUserId != null'> AND qw_user_id = #{qwUserId} </if>" +
+            "</where>" +
+            "</script>")
+    List<FsTagUpdateQueue> selectByConditions(FsTagUpdateQueue condition);
+
+    @Insert("<script>" +
+            "INSERT INTO fs_tag_update_queue " +
+            "(course_log_id, course_id, tag_id, tag_name, operation_type, video_id, status, retry_count, corp_id, qw_user_id, fail_msg, payload, response, create_time, update_time, update_by, create_by,log_type) " +
+            "VALUES " +
+            "<trim prefix='(' suffix=')' suffixOverrides=','>" +
+            "<if test='courseLogId != null'>course_log_id,</if>" +
+            "<if test='courseId != null'>course_id,</if>" +
+            "<if test='tagId != null'>tag_id,</if>" +
+            "<if test='tagName != null'>tag_name,</if>" +
+            "<if test='operationType != null'>operation_type,</if>" +
+            "<if test='videoId != null'>video_id,</if>" +
+            "<if test='status != null'>status,</if>" +
+            "<if test='retryCount != null'>retry_count,</if>" +
+            "<if test='corpId != null'>corp_id,</if>" +
+            "<if test='qwUserId != null'>qw_user_id,</if>" +
+            "<if test='qwExternalContactId != null'>qw_external_contact_id,</if>" +
+            "<if test='failMsg != null'>fail_msg,</if>" +
+            "<if test='payload != null'>payload,</if>" +
+            "<if test='response != null'>response,</if>" +
+            "<if test='createTime != null'>create_time,</if>" +
+            "<if test='updateTime != null'>update_time,</if>" +
+            "<if test='updateBy != null'>update_by,</if>" +
+            "<if test='createBy != null'>create_by,</if>" +
+            "<if test='logType != null'>log_type,</if>" +
+            "</trim>" +
+            "<trim prefix='VALUES (' suffix=')' suffixOverrides=','>" +
+            "<if test='courseLogId != null'>#{courseLogId},</if>" +
+            "<if test='courseId != null'>#{courseId},</if>" +
+            "<if test='tagId != null'>#{tagId},</if>" +
+            "<if test='tagName != null'>#{tagName},</if>" +
+            "<if test='operationType != null'>#{operationType},</if>" +
+            "<if test='videoId != null'>#{videoId},</if>" +
+            "<if test='status != null'>#{status},</if>" +
+            "<if test='retryCount != null'>#{retryCount},</if>" +
+            "<if test='corpId != null'>#{corpId},</if>" +
+            "<if test='qwExternalContactId != null'>#{qwExternalContactId},</if>" +
+            "<if test='qwUserId != null'>#{qwUserId},</if>" +
+            "<if test='failMsg != null'>#{failMsg},</if>" +
+            "<if test='payload != null'>#{payload},</if>" +
+            "<if test='response != null'>#{response},</if>" +
+            "<if test='createTime != null'>#{createTime},</if>" +
+            "<if test='updateTime != null'>#{updateTime},</if>" +
+            "<if test='updateBy != null'>#{updateBy},</if>" +
+            "<if test='createBy != null'>#{createBy},</if>" +
+            "<if test='log_type != null'>#{logType},</if>" +
+            "</trim>" +
+            "</script>")
+    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
+    int insertSelective(FsTagUpdateQueue record);
+
+
+
+    @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" +
+            ") 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}" +
+            ")" +
+            "</foreach>" +
+            "</script>")
+    int batchInsert(@Param("list") List<FsTagUpdateQueue> list);
+
+
+    @Update("<script>" +
+            "UPDATE fs_tag_update_queue " +
+            "<set>" +
+            "<if test='courseLogId != null'>course_log_id = #{courseLogId},</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='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>" +
+            "</set> " +
+            "WHERE id = #{id}" +
+            "</script>")
+    int updateSelective(FsTagUpdateQueue record);
+
+
+
+    @Update("<script>" +
+            "<foreach collection='list' item='item' separator=';'>" +
+            "UPDATE fs_tag_update_queue" +
+            "<set>" +
+            "<if test='item.courseLogId != null'>course_log_id = #{item.courseLogId},</if>" +
+            "<if test='item.isFirst != null'>is_first = #{item.isFirst},</if>" +
+            "<if test='item.courseId != null'>course_id = #{item.courseId},</if>" +
+            "<if test='item.tagId != null'>tag_id = #{item.tagId},</if>" +
+            "<if test='item.tagName != null'>tag_name = #{item.tagName},</if>" +
+            "<if test='item.operationType != null'>operation_type = #{item.operationType},</if>" +
+            "<if test='item.videoId != null'>video_id = #{item.videoId},</if>" +
+            "<if test='item.status != null'>status = #{item.status},</if>" +
+            "<if test='item.retryCount != null'>retry_count = #{item.retryCount},</if>" +
+            "<if test='item.corpId != null'>corp_id = #{item.corpId},</if>" +
+            "<if test='item.qwUserId != null'>qw_user_id = #{item.qwUserId},</if>" +
+            "<if test='item.qwExternalContactId != null'>qw_external_contact_id = #{item.qwExternalContactId},</if>" +
+            "<if test='item.failMsg != null'>fail_msg = #{item.failMsg},</if>" +
+            "<if test='item.payload != null'>payload = #{item.payload},</if>" +
+            "<if test='item.response != null'>response = #{item.response},</if>" +
+            "<if test='item.createTime != null'>create_time = #{item.createTime},</if>" +
+            "<if test='item.updateTime != null'>update_time = #{item.updateTime},</if>" +
+            "<if test='item.updateBy != null'>update_by = #{item.updateBy},</if>" +
+            "<if test='item.createBy != null'>create_by = #{item.createBy},</if>" +
+            "<if test='item.logType != null'>log_type = #{item.logType},</if>" +
+            "<if test='item.nextExecuteTime != null'>next_execute_time = #{item.nextExecuteTime},</if>" +
+            "</set>" +
+            " WHERE id = #{item.id}" +
+            "</foreach>" +
+            "</script>")
+    int batchUpdateSelective(@Param("list") List<FsTagUpdateQueue> list);
+
+}

+ 29 - 0
fs-service/src/main/java/com/fs/tag/service/FsTagUpdateService.java

@@ -0,0 +1,29 @@
+package com.fs.tag.service;
+
+import com.fs.course.domain.FsCourseWatchLog;
+
+import java.util.List;
+
+/**
+ * 完课打备注服务类
+ */
+public interface FsTagUpdateService {
+
+    /**
+     * 正课正在看课
+     * @param logs 日志
+     */
+    void onCourseWatchingBatch(List<FsCourseWatchLog> logs);
+
+    /**
+     * 正课完成看课
+     * @param logs 日志
+     */
+    void onCourseWatchFinishedBatch(List<FsCourseWatchLog> logs);
+
+
+    /**
+     * 处理数据
+     */
+    void handleData();
+}

+ 457 - 0
fs-service/src/main/java/com/fs/tag/service/impl/FsTagUpdateServiceImpl.java

@@ -0,0 +1,457 @@
+package com.fs.tag.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+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;
+import com.fs.tag.mapper.FsTagUpdateQueueMapper;
+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.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;
+import org.springframework.transaction.annotation.Propagation;
+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;
+
+@Slf4j
+@Service("fsTagUpdateService")
+public class FsTagUpdateServiceImpl implements FsTagUpdateService {
+
+
+    @Autowired
+    private FsTagUpdateQueueMapper fsTagUpdateQueueMapper;
+    @Autowired
+    private FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    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;
+
+    @Value("${tag.thread.num:5}")
+    private Integer TAG_THREAD_NUM;
+
+    @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;
+
+    @PostConstruct
+    public void init(){
+        this.rateLimiter = RateLimiter.create(RATE_LIMIT_NUM);
+    }
+
+    @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(item.getQwExternalContactId());
+            task.setQwUserId(item.getQwUserId());
+
+            String corpId = qwUserCacheService.queryCorpIdByQwUserId(item.getQwUserId());
+            if(StringUtils.isNotNull(corpId)){
+                task.setCorpId(corpId);
+            }
+
+            FsUserCourseVideo fsUserCourseVideo = courseVideoMap.get(task.getVideoId());
+
+            if(ObjectUtils.isNull(fsUserCourseVideo)) {
+                String errorMsg = String.format("该条记录 %d 找不到对应的课堂视频", task.getVideoId());
+                log.error(errorMsg);
+                task.setStatus(3);
+                task.setRetryCount(3);
+                task.setFailMsg(errorMsg);
+                batchData.add(task);
+                continue;
+            }
+            if(ObjectUtil.equal(fsUserCourseVideo.getIsFirst(),1)) {
+                task.setIsFirst(1);
+            } else {
+                task.setIsFirst(0);
+            }
+            task.setSort(fsUserCourseVideo.getCourseSort());
+            batchData.add(task);
+        }
+
+
+        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.setTagId(null);
+            task.setTagName(null);
+            task.setOperationType(0);
+            task.setStatus(0);
+            task.setRetryCount(0);
+            task.setQwExternalContactId(item.getQwExternalContactId());
+            task.setQwUserId(item.getQwUserId());
+            task.setLogType(1);
+
+            String corpId = qwUserCacheService.queryCorpIdByQwUserId(item.getQwUserId());
+            if(StringUtils.isNotNull(corpId)){
+                task.setCorpId(corpId);
+            }
+
+            FsUserCourseVideo fsUserCourseVideo = courseVideoMap.get(task.getVideoId());
+            if(ObjectUtils.isNull(fsUserCourseVideo)) {
+                String errorMsg = String.format("该条记录 %d 找不到对应的课堂视频", task.getVideoId());
+                log.error(errorMsg);
+                task.setStatus(3);
+                task.setRetryCount(3);
+                task.setFailMsg(errorMsg);
+                batchData.add(task);
+                continue;
+            }
+            if(ObjectUtil.equal(fsUserCourseVideo.getIsFirst(),1)) {
+                task.setIsFirst(1);
+            } else {
+                task.setIsFirst(0);
+            }
+            task.setSort(fsUserCourseVideo.getCourseSort());
+            batchData.add(task);
+        }
+
+
+        fsTagUpdateQueueMapper.batchInsert(batchData);
+    }
+
+    @Override
+    public void handleData() {
+        List<FsTagUpdateQueue> tasks = fsTagUpdateQueueMapper.selectPending();
+        if(CollectionUtils.isEmpty(tasks)){
+            log.info("找不到可处理的任务,已跳过!");
+            return;
+        }
+        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();
+                ReentrantLock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
+                lock.lock();
+                try {
+                    processSingleTask(task);
+                } finally {
+                    lock.unlock();
+                    latch.countDown();
+                }
+            });
+        }
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        try {
+            executor.shutdown();
+            if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
+                executor.shutdownNow();
+                // 再次等待确保已经关闭
+                if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
+                    log.warn("线程池未能在预期时间内关闭");
+                }
+            }
+        } catch (InterruptedException e) {
+            executor.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+
+        if(CollectionUtils.isNotEmpty(tasks)){
+            fsTagUpdateQueueMapper.batchUpdateSelective(tasks);
+        }
+
+    }
+
+    private void processSingleTask(FsTagUpdateQueue fsTagUpdateQueue) {
+        try {
+            String courseName = getCourseName(fsTagUpdateQueue.getCourseId());
+            String tagGroupName = String.format("%s-看课标签组(自动创建)",courseName);
+            String tagName01 = String.format("%d上", fsTagUpdateQueue.getVideoId());
+            String tagName02 = String.format("%d完", fsTagUpdateQueue.getVideoId());
+            String corpId = fsTagUpdateQueue.getCorpId();
+
+            // 是否存在该标签
+            QwTagVO qwTagVO = qwTagMapper.selectQwTagByName(tagName01, corpId);
+            if(ObjectUtil.isNull(qwTagVO)){
+                // 是否存在标签组
+                QwTagGroup qwTagGroup = qwTagGroupMapper.selectQwTagGroupByName(tagGroupName, corpId);
+                // 创建标签组 & 标签
+                if(ObjectUtil.isNull(qwTagGroup)) {
+                    log.info("当前标签组不存在 建标签组 & 标签");
+                    QwTagGroupAddParam qwTagGroupAddParam = buildQwTagGroupAndTags(tagGroupName, fsTagUpdateQueue);
+                    qwTagGroupService.insertQwTagGroupParam(qwTagGroupAddParam);
+
+                } else {
+                    // 这个标签组满了吗?
+                    List<QwTagVO> qwTags = getQwTags(qwTagGroup);
+                    // 如果满了,找下一个标签
+                    if(CollectionUtils.isNotEmpty(qwTags) && qwTags.size() >= TAG_MAX_NUM){
+                        log.info("标签组1 {}满了,查找标签组2",tagGroupName);
+                        tagGroupName = String.format("%s-看课标签组2(自动创建)",courseName);
+                        qwTagGroup = qwTagGroupMapper.selectQwTagGroupByName(tagGroupName, corpId);
+                        // 判断是否满了
+                        if(ObjectUtil.isNotNull(qwTagGroup)) {
+                            List<QwTagVO> qwTags1 = getQwTags(qwTagGroup);
+                            if(CollectionUtils.isNotEmpty(qwTags1) && qwTags1.size() >= TAG_MAX_NUM){
+                                log.info("标签组2 {} 满了,查找标签组3",courseName);
+                                tagGroupName = String.format("%s-看课标签组3(自动创建)",courseName);
+                                qwTagGroup = qwTagGroupMapper.selectQwTagGroupByName(tagGroupName, corpId);
+                                List<QwTagVO> qwTags2 = getQwTags(qwTagGroup);
+                                if(CollectionUtils.isNotEmpty(qwTags2) && qwTags2.size() >= TAG_MAX_NUM){
+                                    log.error("最大支持3个标签组! tagGroupName={}",tagGroupName);
+                                    throw new UnsupportedOperationException("最大支持分成3个组!");
+                                }
+                            }
+                        }
+                    }
+                    // 创建标签
+                    log.info("当前标签 {}、{} 在标签组 {}不存在,正在创建标签",tagName01,tagName02,qwTagGroup);
+                    createNotExistLabel(fsTagUpdateQueue, qwTagGroup, tagName01, tagName02);
+                }
+            }
+
+            QwTagVO qwTagVO1 = qwTagMapper.selectQwTagByName(tagName01, corpId);
+            QwTagVO qwTagVO2 = qwTagMapper.selectQwTagByName(tagName02, corpId);
+
+            if(ObjectUtil.isNull(qwTagVO1)){
+                throw new IllegalArgumentException(String.format("标签 %s-%s 未添加成功!",tagName01,corpId));
+            }
+            if(ObjectUtil.isNull(qwTagVO2)){
+                throw new IllegalArgumentException(String.format("标签 %s-%s 未添加成功!",tagName02,corpId));
+            }
+
+
+            // 调用企微API更新标签
+            QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+            QwExternalContact qwExternalContact = qwExternalContactMapper
+                    .selectQwExternalContactById(fsTagUpdateQueue.getQwExternalContactId());
+            if(qwExternalContact == null) {
+                throw new IllegalArgumentException(String.format("企微外部联系人 %s 未找到!", fsTagUpdateQueue.getQwExternalContactId()));
+            }
+            qwEditUserTagParam.setUserid(qwExternalContact.getUserId());
+            qwEditUserTagParam.setExternal_userid(qwExternalContact.getExternalUserId());
+
+            rateLimiter.acquire();
+
+            // 如果是看课中
+            if(ObjectUtil.equal(fsTagUpdateQueue.getLogType(),0)){
+                qwEditUserTagParam.setAdd_tag(Collections.singletonList(qwTagVO1.getTagId()));
+            } else {
+                // 已完课
+                qwEditUserTagParam.setAdd_tag(Collections.singletonList(qwTagVO2.getTagId()));
+            }
+
+            QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, fsTagUpdateQueue.getCorpId());
+            fsTagUpdateQueue.setPayload(JSON.toJSONString(qwEditUserTagParam));
+            fsTagUpdateQueue.setResponse(JSON.toJSONString(qwResult));
+            fsTagUpdateQueue.setTagName(tagGroupName);
+            fsTagUpdateQueue.setTagId(qwTagVO1.getGroupId());
+            // 打标签成功
+            if(ObjectUtil.equal(qwResult.getErrcode(),0)) {
+                fsTagUpdateQueue.setStatus(2);
+                fsTagUpdateQueue.setRetryCount(0);
+            } else {
+                throw new RuntimeException(String.format("打标签失败 原因: %s", JSON.toJSONString(qwResult)));
+            }
+        } catch (Exception e){
+            fsTagUpdateQueue.setStatus(3);
+            fsTagUpdateQueue.setRetryCount(fsTagUpdateQueue.getRetryCount()+1);
+            fsTagUpdateQueue.setFailMsg(ExceptionUtils.getFullStackTrace(e));
+            fsTagUpdateQueue.setNextExecuteTime(LocalDateTime.now().plusHours(1));
+        }
+    }
+
+    private void createNotExistLabel(FsTagUpdateQueue fsTagUpdateQueue, QwTagGroup qwTagGroup, String tagName01, String tagName02) {
+        QwAddTagParam qwAddTagParam = buildQwTagGroupAndTags(qwTagGroup, tagName01, tagName02);
+        QwAddTagResult qwAddTagResult = qwApiService.addTag(qwAddTagParam, fsTagUpdateQueue.getCorpId());
+        if(ObjectUtil.equal(qwAddTagResult.getErrcode(),0)){
+            List<InTag> addTag = qwAddTagResult.getTag_group().getTag();
+            if (CollectionUtils.isNotEmpty(addTag)) {
+                for (InTag inTag : addTag) {
+                    QwTag addQwTag = new QwTag();
+                    addQwTag.setGroupId(qwTagGroup.getGroupId());
+                    addQwTag.setTagId(inTag.getId());
+                    addQwTag.setOrder(inTag.getOrder());
+                    addQwTag.setName(inTag.getName());
+                    addQwTag.setCorpId(qwTagGroup.getCorpId());
+                    addQwTag.setCreateTime(new Date(inTag.getCreate_time()));
+                    addQwTag.setTagFrom(1);
+                    qwTagMapper.insertQwTag(addQwTag);
+                }
+            }
+            log.info("创建标签完成 {}",JSON.toJSON(qwAddTagResult));
+        } else {
+            throw new RuntimeException(String.format("创建标签失败!%s",JSON.toJSONString(qwAddTagResult)));
+        }
+    }
+
+    private List<QwTagVO> getQwTags(QwTagGroup qwTagGroup) {
+        QwTag qwTag = new QwTag();
+        qwTag.setGroupId(qwTagGroup.getGroupId());
+        qwTag.setCompanyId(qwTagGroup.getCompanyId());
+        List<QwTagVO> qwTags = qwTagMapper.selectQwTagListVO(qwTag);
+        return qwTags;
+    }
+
+    private QwAddTagParam buildQwTagGroupAndTags(QwTagGroup qwTagGroup, String tagName01, String tagName02) {
+        QwAddTagParam qwTagGroupAddParam = new QwAddTagParam();
+        qwTagGroupAddParam.setGroup_name(qwTagGroup.getName());
+        qwTagGroupAddParam.setOrder(qwTagGroup.getOrder());
+        qwTagGroupAddParam.setStrategy_id(1);
+        qwTagGroupAddParam.setGroup_id(qwTagGroup.getGroupId());
+        List<TagData> tags = new ArrayList<>();
+
+        TagData qwTag1 = new TagData();
+        qwTag1.setName(tagName01);
+        qwTag1.setOrder(0);
+        tags.add(qwTag1);
+
+        TagData qwTag2 = new TagData();
+        qwTag2.setName(tagName02);
+        qwTag2.setOrder(0);
+        tags.add(qwTag2);
+
+        qwTagGroupAddParam.setTag(tags);
+
+        return qwTagGroupAddParam;
+    }
+
+
+    private QwTagGroupAddParam buildQwTagGroupAndTags(String tagGroupName, FsTagUpdateQueue fsTagUpdateQueue) {
+        QwTagGroupAddParam qwTagGroupAddParam = new QwTagGroupAddParam();
+        qwTagGroupAddParam.setCorpId(fsTagUpdateQueue.getCorpId());
+        qwTagGroupAddParam.setName(tagGroupName);
+        qwTagGroupAddParam.setOrder(0);
+
+
+        List<QwTag> tags = new ArrayList<>();
+        QwTag qwTag = new QwTag();
+        qwTag.setName(String.format("%d上",fsTagUpdateQueue.getVideoId()));
+        qwTag.setTagFrom(1);
+        qwTag.setOrder(0);
+        tags.add(qwTag);
+
+        QwTag qwTagFinish = new QwTag();
+        qwTagFinish.setName(String.format("%d完",fsTagUpdateQueue.getVideoId()));
+        qwTagFinish.setTagFrom(1);
+        qwTagFinish.setOrder(0);
+        tags.add(qwTagFinish);
+
+        qwTagGroupAddParam.setTag(tags);
+        return qwTagGroupAddParam;
+    }
+
+    /**
+     * 获取课程名称
+     * @param courseId 课程id
+     * @return 课程名称
+     */
+    private String getCourseName(Long courseId){
+        FsUserCourse course = fsUserCourseMapper.selectFsUserCourseByCourseId(courseId);
+        if(ObjectUtil.isNotNull(course)){
+            return course.getCourseName();
+        }
+        return null;
+    }
+}

+ 2 - 2
fs-service/src/main/resources/application-dev.yml

@@ -43,7 +43,7 @@ spring:
             druid:
                 # 主库数据源
                 master:
-                    url: jdbc:mysql://139.186.77.83:3306/ylrz_his_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://139.186.77.83:3306/ylrz_his_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
                     username: Rtroot
                     password: Rtroot
                 # 初始连接数
@@ -176,7 +176,7 @@ cloud_host:
 headerImg:
     imgUrl: https://hzyy.obs.cn-north-4.myhuaweicloud.com/fs/20250616/1750067609692.png
 ipad:
-    ipadUrl: http://139.159.133.223:8667
+    ipadUrl: http://admin.test.ylrztop.com/ipad
     aiApi: http://1.95.196.10:3000/api
 wx_miniapp_temp:
     pay_order_temp_id:

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

@@ -431,7 +431,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

+ 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>

+ 0 - 17
fs-service/src/test/java/com/fs/his/service/impl/FsUserServiceImplTest.java

@@ -1,17 +0,0 @@
-package com.fs.his.service.impl;
-
-
-import com.fs.tulin.service.ITulinInfoSyncLogService;
-
-import javax.annotation.Resource;
-
-class FsUserServiceImplTest {
-
-    @Resource
-    private ITulinInfoSyncLogService tulinInfoSyncLogService;
-
-
-    void selectFsUserPageListNew() {
-        tulinInfoSyncLogService.syncInfo();
-    }
-}