xw 5 дней назад
Родитель
Сommit
2d27c5de55

+ 66 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCompanyQw.java

@@ -0,0 +1,66 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 用户项目销售绑定快照对象 fs_user_company_qw
+ * 用于企微自动发课(send_type=2)的重粉判断优化
+ * 
+ * @author fs
+ * @date 2026-01-07
+ */
+@Data
+@TableName("fs_user_company_qw")
+public class FsUserCompanyQw implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 用户ID(fs_user表的user_id) */
+    private Long fsUserId;
+
+    /** 项目ID(课程所属项目) */
+    private Long projectId;
+
+    /** 企微用户ID(销售,qw_user表的id) */
+    private Long qwUserId;
+
+    /** 公司用户ID(销售,company_user表的user_id) */
+    private Long companyUserId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 企微外部联系人ID */
+    private Long qwExternalContactId;
+
+    /** 课程ID */
+    private Long courseId;
+
+    /** 视频ID */
+    private Long videoId;
+
+    /** 首次绑定时间 */
+    private Date firstBindTime;
+
+    /** 最后绑定时间 */
+    private Date lastBindTime;
+
+    /** 绑定次数(更新次数) */
+    private Integer bindCount;
+
+    /** 创建时间 */
+    private Date createTime;
+
+    /** 更新时间 */
+    private Date updateTime;
+}

+ 48 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCompanyQwMapper.java

@@ -0,0 +1,48 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FsUserCompanyQw;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 用户项目销售绑定快照Mapper接口
+ * 用于企微自动发课(send_type=2)的重粉判断优化
+ *
+ * @author fs
+ * @date 2026-01-07
+ */
+@Mapper
+public interface FsUserCompanyQwMapper extends BaseMapper<FsUserCompanyQw> {
+
+    /**
+     * 根据用户ID和项目ID查询绑定的销售信息(用于重粉判断)
+     *
+     * @param fsUserId 用户ID
+     * @param projectId 项目ID
+     * @return 绑定记录,如果不存在则返回null
+     */
+    FsUserCompanyQw selectByUserAndProject(@Param("fsUserId") Long fsUserId,
+                                            @Param("projectId") Long projectId);
+
+    /**
+     *
+     * @param fsUserId 用户ID
+     * @param projectId 项目ID
+     * @param qwUserId 企微用户ID(销售)
+     * @param companyUserId 公司用户ID(销售)
+     * @param companyId 公司ID
+     * @param qwExternalContactId 企微外部联系人ID
+     * @param courseId 课程ID
+     * @param videoId 视频ID
+     * @return 影响的行数
+     */
+    int insertOrUpdate(@Param("fsUserId") Long fsUserId,
+                       @Param("projectId") Long projectId,
+                       @Param("qwUserId") Long qwUserId,
+                       @Param("companyUserId") Long companyUserId,
+                       @Param("companyId") Long companyId,
+                       @Param("qwExternalContactId") Long qwExternalContactId,
+                       @Param("courseId") Long courseId,
+                       @Param("videoId") Long videoId);
+}

+ 118 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -250,6 +250,12 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     @Autowired
     private IFsUserCompanyBindService fsUserCompanyBindService;
     private IFsUserCompanyBindService fsUserCompanyBindService;
 
 
+    @Autowired
+    private FsUserCompanyQwMapper fsUserCompanyQwMapper;
+
+    @Autowired
+    private SysDictDataMapper sysDictDataMapper;
+
     @Autowired
     @Autowired
     private BalanceRollbackErrorMapper balanceRollbackErrorMapper;
     private BalanceRollbackErrorMapper balanceRollbackErrorMapper;
     @Autowired
     @Autowired
@@ -601,6 +607,55 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             }
             }
         }
         }
 
 
+        // ========== 【新增】企微自动发课(send_type=2)重粉判断逻辑 - 在所有场景处理之前判断 ==========
+        // 获取当前视频所属的课程和项目
+        if (param.getVideoId() != null && param.getCompanyId() != null) {
+            FsUserCourseVideo currentVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+            if (currentVideo != null && currentVideo.getCourseId() != null) {
+                FsUserCourse course = fsUserCourseMapper.selectFsUserCourseByCourseId(currentVideo.getCourseId());
+                Long projectId = course != null && course.getProject() != null ? course.getProject() : 0L;
+
+                // 只对有项目ID的课程进行重粉判断
+                if (projectId != 0L) {
+                        // 查询该用户在这个项目下是否已经绑定了其他销售
+                        FsUserCompanyQw existBind = fsUserCompanyQwMapper.selectByUserAndProject(
+                            param.getUserId(), projectId
+                        );
+
+                        // 如果存在绑定记录,且绑定的销售不是当前销售,说明是重粉
+                        if (existBind != null && param.getQwUserId() != null
+                            && !existBind.getQwUserId().equals(Long.parseLong(param.getQwUserId()))) {
+                            // 查询项目名称
+                            String projectName = "未知项目";
+                            List<SysDictData> courseProjects = sysDictDataMapper.selectDictDataByType("sys_course_project");
+                            for (SysDictData dictData : courseProjects) {
+                                if (dictData.getDictValue().equals(String.valueOf(projectId))) {
+                                    projectName = dictData.getDictLabel();
+                                    break;
+                                }
+                            }
+
+                            // 查询销售名称
+                            String salesName = "其他销售";
+                            QwUser existQwUser = qwUserMapper.selectById(existBind.getQwUserId());
+                            if (existQwUser != null && existQwUser.getCompanyUserId() != null) {
+                                CompanyUser companyUser = companyUserMapper.selectCompanyUserById(existQwUser.getCompanyUserId());
+                                if (companyUser != null && StringUtils.isNotEmpty(companyUser.getUserName())) {
+                                    salesName = companyUser.getUserName();
+                                }
+                            }
+
+                            // 返回重粉提示
+                            String errorMsg = String.format("您已在销售【%s】处观看过【%s】项目的课程", salesName, projectName);
+                            logger.info("【重粉检测-提前拦截】用户ID: {}, 项目ID: {}, 原销售: {}, 当前销售: {}",
+                                param.getUserId(), projectId, existBind.getQwUserId(), param.getQwUserId());
+                            return R.error(errorMsg);
+                        }
+                }
+            }
+        }
+        // ========== 【重粉判断结束】 ==========
+
         Integer isRoom = param.getIsRoom();
         Integer isRoom = param.getIsRoom();
 
 
         // 处理逻辑
         // 处理逻辑
@@ -711,6 +766,10 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
         }
         //重粉逻辑
         //重粉逻辑
         fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());
         fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());
+
+        // 记录用户-项目-销售绑定关系
+        recordUserProjectBind(param, qwExternalId, "handleQwRoom");
+
         return R.error(567, "群聊通用链接").put("qwExternalId", qwExternalContact.getId());
         return R.error(567, "群聊通用链接").put("qwExternalId", qwExternalContact.getId());
     }
     }
     private R handleRoom(FsUserCourseVideoAddKfUParam param,FsUser user,String noRegisterMsg) {
     private R handleRoom(FsUserCourseVideoAddKfUParam param,FsUser user,String noRegisterMsg) {
@@ -795,6 +854,12 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             }
             }
         }
         }
 
 
+        // ========== 【handleRoom-记录用户-项目-销售绑定关系】 ==========
+        if (param.getQwExternalId() != null) {
+            recordUserProjectBind(param, param.getQwExternalId(), "handleRoom");
+        }
+        // ========== 【绑定关系记录结束】 ==========
+
 
 
      /*   String msg = "<div style=\"color: red;margin-bottom: 15px;font-weight: bold;\">本课程为群会员独享<br>请长按二维码</div>\n" +
      /*   String msg = "<div style=\"color: red;margin-bottom: 15px;font-weight: bold;\">本课程为群会员独享<br>请长按二维码</div>\n" +
                 "\t\t\t\t\t<div style=\"color: #999;font-size: 14px;font-weight: bold;\">添加伴学助手免费领取会员权限</div>";*/
                 "\t\t\t\t\t<div style=\"color: #999;font-size: 14px;font-weight: bold;\">添加伴学助手免费领取会员权限</div>";*/
@@ -907,6 +972,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             //重粉逻辑
             //重粉逻辑
             fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());
             fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());
 
 
+            // 记录用户-项目-销售绑定关系
+            recordUserProjectBind(param, qwExternalId, "handleExt");
+
 
 
             if (param.getLinkType() != null && param.getLinkType() == 5) {
             if (param.getLinkType() != null && param.getLinkType() == 5) {
                 FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
                 FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
@@ -945,6 +1013,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             //重粉逻辑
             //重粉逻辑
             fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());
             fsUserCompanyBindService.bindFsUser(param.getUserId(), qwExternalId, log.getLogId());
 
 
+            // 记录用户-项目-销售绑定关系
+            recordUserProjectBind(param, qwExternalId, "handleExt");
+
 
 
             if (param.getLinkType() != null && param.getLinkType() == 5) {
             if (param.getLinkType() != null && param.getLinkType() == 5) {
                 FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
                 FsCourseLink fsCourseLink = fsCourseLinkMapper.selectExpireLinkByQwExternalId(param.getQwUserId(), param.getVideoId(), qwExternalId);
@@ -955,6 +1026,53 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
         }
     }
     }
 
 
+    /**
+     * 记录用户-项目-销售绑定关系到 fs_user_company_qw 表
+     * 用于企微自动发课(send_type=2)的重粉判断优化
+     *
+     * @param param 看课参数
+     * @param qwExternalId 企微外部联系人id
+     * @param sceneName 场景名称(用于日志追踪)
+     */
+    private void recordUserProjectBind(FsUserCourseVideoAddKfUParam param, Long qwExternalId, String sceneName) {
+        try {
+            // 查询视频所属的课程和项目
+            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+            if (video == null || video.getCourseId() == null) {
+                logger.warn("【{}-记录绑定关系】视频信息不存在,视频ID: {}", sceneName, param.getVideoId());
+                return;
+            }
+
+            FsUserCourse course = fsUserCourseMapper.selectFsUserCourseByCourseId(video.getCourseId());
+            Long projectId = course != null && course.getProject() != null ? course.getProject() : 0L;
+
+            // 只对有项目ID的课程记录绑定关系
+            if (projectId == 0L) {
+                logger.debug("【{}-记录绑定关系】课程未关联项目,跳过记录,课程ID: {}", sceneName, video.getCourseId());
+                return;
+            }
+
+            Long qwUserId = param.getQwUserId() != null ? Long.parseLong(param.getQwUserId()) : null;
+            fsUserCompanyQwMapper.insertOrUpdate(
+                param.getUserId(),
+                projectId,
+                qwUserId,
+                param.getCompanyUserId(),
+                param.getCompanyId(),
+                qwExternalId,
+                video.getCourseId(),
+                param.getVideoId()
+            );
+
+            logger.info("【{}-记录绑定关系成功】用户ID: {}, 项目ID: {}, 销售ID: {}, 外部联系人id: {}",
+                sceneName, param.getUserId(), projectId, qwUserId, qwExternalId);
+
+        } catch (Exception e) {
+            logger.error("【{}-记录绑定关系失败】用户ID: {}, 视频ID: {}, 错误: {}",
+                sceneName, param.getUserId(), param.getVideoId(), e.getMessage(), e);
+        }
+    }
+
     private R addCustomerService(String qwUserById,String msg){
     private R addCustomerService(String qwUserById,String msg){
         if ("济南联志健康".equals(signProjectName)){
         if ("济南联志健康".equals(signProjectName)){
             return R.error(400,msg).put("qrcode", "");
             return R.error(400,msg).put("qrcode", "");

+ 56 - 0
fs-service/src/main/resources/mapper/course/FsUserCompanyQwMapper.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.course.mapper.FsUserCompanyQwMapper">
+
+    <resultMap id="FsUserCompanyQwResult" type="com.fs.course.domain.FsUserCompanyQw">
+        <id property="id" column="id"/>
+        <result property="fsUserId" column="fs_user_id"/>
+        <result property="projectId" column="project_id"/>
+        <result property="qwUserId" column="qw_user_id"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="qwExternalContactId" column="qw_external_contact_id"/>
+        <result property="courseId" column="course_id"/>
+        <result property="videoId" column="video_id"/>
+        <result property="firstBindTime" column="first_bind_time"/>
+        <result property="lastBindTime" column="last_bind_time"/>
+        <result property="bindCount" column="bind_count"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <!-- 根据用户ID和项目ID查询绑定记录(用于重粉判断) -->
+    <select id="selectByUserAndProject" resultMap="FsUserCompanyQwResult">
+        SELECT 
+            id, fs_user_id, project_id, qw_user_id, company_user_id, company_id,
+            qw_external_contact_id, course_id, video_id,
+            first_bind_time, last_bind_time, bind_count,
+            create_time, update_time
+        FROM fs_user_company_qw
+        WHERE fs_user_id = #{fsUserId}
+          AND project_id = #{projectId}
+        LIMIT 1
+    </select>
+
+    <!-- 插入或更新用户-项目-销售绑定关系(upsert) -->
+    <insert id="insertOrUpdate">
+        INSERT INTO fs_user_company_qw (
+            fs_user_id, project_id, qw_user_id, company_user_id, company_id,
+            qw_external_contact_id, course_id, video_id,
+            first_bind_time, last_bind_time, bind_count
+        ) VALUES (
+            #{fsUserId}, #{projectId}, #{qwUserId}, #{companyUserId}, #{companyId},
+            #{qwExternalContactId}, #{courseId}, #{videoId},
+            NOW(), NOW(), 1
+        )
+        ON DUPLICATE KEY UPDATE
+            qw_external_contact_id = VALUES(qw_external_contact_id),
+            course_id = VALUES(course_id),
+            video_id = VALUES(video_id),
+            last_bind_time = NOW(),
+            bind_count = bind_count + 1
+    </insert>
+
+</mapper>