Преглед на файлове

1、企微多次登录企微问题处理
2、结束发送消息给app端进行强制退出
3、同日重开后复用当天记录(避开 uk_live_user_date),并重置为可再领

yys преди 5 дни
родител
ревизия
715549b948

+ 14 - 0
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -286,6 +286,8 @@ public class QwMsgController {
                 }
                 if (!qu.getCorpId().equals(sCorpId)){
                     redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),22,10, TimeUnit.MINUTES);
+                    // 企业不匹配后作废本次二维码key,避免继续拿旧验证码去CheckCode
+                    redisCache.deleteObject("qrCode:qwUserId:" + id);
                     log.info("id:{}, 公司不匹配不给登录", id);
                     WxWorkGetQrCodeDTO wxWorkGetQrCodeDTO = new WxWorkGetQrCodeDTO();
                     wxWorkGetQrCodeDTO.setUuid(wxWorkMsgResp.getUuid());
@@ -295,6 +297,7 @@ public class QwMsgController {
                 }
                 if (!qu.getQwUserId().equals(jsonObject.get("acctid"))){
                     redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),23,10, TimeUnit.MINUTES);
+                    redisCache.deleteObject("qrCode:qwUserId:" + id);
                     log.info("id:{}, 账号不匹配不给登录", id);
                     WxWorkGetQrCodeDTO wxWorkGetQrCodeDTO = new WxWorkGetQrCodeDTO();
                     wxWorkGetQrCodeDTO.setUuid(wxWorkMsgResp.getUuid());
@@ -316,6 +319,17 @@ public class QwMsgController {
                 break;
             case 100004:
                 log.info("id:{}, 需要验证二维码消息", id);
+                // 已提交验证码 / 已有终态 / 二维码已作废时,忽略迟到或重复的 100004,避免误弹验证码
+                Object curStatusObj = redisCache.getCacheObject("qrCodeUid:" + wxWorkMsgResp.getUuid());
+                Integer curStatus = curStatusObj instanceof Number ? ((Number) curStatusObj).intValue() : null;
+                if (curStatus != null && (curStatus == 22 || curStatus == 23 || curStatus == 104001 || curStatus == 100050)) {
+                    log.info("id:{}, 忽略100004, curStatus={}", id, curStatus);
+                    break;
+                }
+                if (redisCache.getCacheObject("qrCode:qwUserId:" + id) == null) {
+                    log.info("id:{}, 无有效二维码key,忽略100004", id);
+                    break;
+                }
                 redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),100004,10, TimeUnit.MINUTES);
                 break;
             case 100012:

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

@@ -23,6 +23,11 @@ public interface LiveCompletionPointsRecordMapper {
      */
     int updateRecord(LiveCompletionPointsRecord record);
 
+    /**
+     * 同日重开后复用当天记录(避开 uk_live_user_date),并重置为可再领
+     */
+    int reuseRecordForNewSession(LiveCompletionPointsRecord record);
+
     /**
      * 查询用户某天的完课记录
      */

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

@@ -22,10 +22,12 @@ import org.springframework.transaction.annotation.Transactional;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
 import java.util.Date;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * 直播完课积分记录Service业务层处理
@@ -66,17 +68,17 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
 
             // 2. 从数据库获取完课积分配置
             CompletionPointsConfig config = getCompletionPointsConfig(live);
-            
+
             // 检查是否开启完课积分功能
             if (!config.isEnabled()) {
 
                 return null;
             }
-            
+
             // 检查配置完整性
             Integer completionRate = config.getCompletionRate();
             int[] pointsConfig = config.getPointsConfig();
-            
+
             if (completionRate == null || pointsConfig == null || pointsConfig.length == 0) {
 
                 return null;
@@ -104,7 +106,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
             BigDecimal watchRate = BigDecimal.valueOf(actualWatchDuration)
                     .multiply(BigDecimal.valueOf(100))
                     .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
-            
+
             // 限制完课比例最大值为100.00%(防止数据库字段溢出)
             if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
                 watchRate = BigDecimal.valueOf(100);
@@ -116,51 +118,40 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
                 return null;
             }
 
-            // 7. 本场直播(按 startTime)是否已创建过完课记录;结束后重开可再领
-            LiveCompletionPointsRecord sessionRecord = recordMapper.selectLatestByUserAndLiveId(liveId, userId);
-            if (sessionRecord != null && live.getStartTime() != null) {
-                Date recordTime = sessionRecord.getCreateTime() != null
-                        ? sessionRecord.getCreateTime()
-                        : sessionRecord.getCurrentCompletionDate();
-                if (recordTime != null) {
-                    LocalDateTime recordLdt = recordTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
-                    if (!recordLdt.isBefore(live.getStartTime())) {
-                        return null;
-                    }
-                }
-            } else if (sessionRecord != null && live.getStartTime() == null) {
-                LocalDate today = LocalDate.now();
-                Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
-                LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
-                if (todayRecord != null) {
+            LocalDate today = LocalDate.now();
+            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            // 7. 按唯一键 uk_live_user_date(live+user+当天)先查主库当天记录,避免从库延迟/并发重复插入
+            LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
+            if (todayRecord != null) {
+                if (isRecordInCurrentSession(todayRecord, live.getStartTime())) {
+                    // 本场已创建过,不再插入
                     return null;
                 }
+                // 同日上一场残留:复用更新,避开唯一键冲突,重开后可再领
+                LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(userId);
+                int continuousDays = calcContinuousDays(latestRecord, today);
+                int points = calculatePoints(continuousDays, pointsConfig);
+
+                todayRecord.setWatchDuration(actualWatchDuration);
+                todayRecord.setVideoDuration(videoDuration);
+                todayRecord.setCompletionRate(watchRate);
+                todayRecord.setContinuousDays(continuousDays);
+                todayRecord.setPointsAwarded(points);
+                if (latestRecord != null && !Objects.equals(latestRecord.getId(), todayRecord.getId())) {
+                    todayRecord.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
+                }
+                recordMapper.reuseRecordForNewSession(todayRecord);
+                todayRecord.setReceiveStatus(0);
+                todayRecord.setReceiveTime(null);
+                todayRecord.setCreateTime(new Date());
+                log.info("同日重开复用完课记录, liveId={}, userId={}, recordId={}", liveId, userId, todayRecord.getId());
+                return todayRecord;
             }
 
-            LocalDate today = LocalDate.now();
-            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
-
             // 8. 查询最近一次完课记录(不限直播间),计算连续天数
             LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(userId);
-            int continuousDays = 1;
-
-            if (latestRecord != null) {
-                LocalDate lastDate = latestRecord.getCurrentCompletionDate()
-                        .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
-
-                long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
-
-                if (daysBetween == 0) {
-                    continuousDays = latestRecord.getContinuousDays();
-
-                } else if (daysBetween == 1) {
-                    // 昨天完课了,连续天数+1
-                    continuousDays = latestRecord.getContinuousDays() + 1;
-                } else {
-                    // 中断了,重新开始
-                    continuousDays = 1;
-                }
-            }
+            int continuousDays = calcContinuousDays(latestRecord, today);
 
             // 9. 计算积分
             int points = calculatePoints(continuousDays, pointsConfig);
@@ -181,16 +172,58 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
                 record.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
             }
 
-            recordMapper.insertRecord(record);
+            try {
+                recordMapper.insertRecord(record);
+            } catch (org.springframework.dao.DuplicateKeyException e) {
+                // 实时推送与定时任务并发时可能撞唯一键,视为已存在
+                log.info("完课记录已存在(并发), liveId={}, userId={}", liveId, userId);
+                return null;
+            }
 
             return record;
 
+        } catch (org.springframework.dao.DuplicateKeyException e) {
+            log.info("完课记录已存在(并发), liveId={}, userId={}", liveId, userId);
+            return null;
         } catch (Exception e) {
             log.error("检查并创建完课记录失败, liveId={}, userId={}", liveId, userId, e);
             throw e;
         }
     }
 
+    /** 记录是否属于当前开播场次(create_time >= startTime) */
+    private boolean isRecordInCurrentSession(LiveCompletionPointsRecord record, LocalDateTime startTime) {
+        if (record == null) {
+            return false;
+        }
+        if (startTime == null) {
+            // 无开播时间时,当天有记录即视为已处理
+            return true;
+        }
+        Date recordTime = record.getCreateTime() != null ? record.getCreateTime() : record.getCurrentCompletionDate();
+        if (recordTime == null) {
+            return true;
+        }
+        LocalDateTime recordLdt = recordTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        return !recordLdt.isBefore(startTime);
+    }
+
+    private int calcContinuousDays(LiveCompletionPointsRecord latestRecord, LocalDate today) {
+        int continuousDays = 1;
+        if (latestRecord == null || latestRecord.getCurrentCompletionDate() == null) {
+            return continuousDays;
+        }
+        LocalDate lastDate = latestRecord.getCurrentCompletionDate()
+                .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
+        if (daysBetween == 0) {
+            continuousDays = latestRecord.getContinuousDays() != null ? latestRecord.getContinuousDays() : 1;
+        } else if (daysBetween == 1) {
+            continuousDays = (latestRecord.getContinuousDays() != null ? latestRecord.getContinuousDays() : 0) + 1;
+        }
+        return continuousDays;
+    }
+
     /**
      * 用户领取完课积分
      */
@@ -214,10 +247,12 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         }
 
         // 4. 发放积分并写入 fs_user_integral_logs(logType=27 直播完课积分)
+        // businessId 带上 createTime,同日重开复用同一 recordId 时可再领
         FsUserAddIntegralTemplateParam integralParam = new FsUserAddIntegralTemplateParam();
         integralParam.setUserId(userId);
         integralParam.setLogType(FsUserIntegralLogTypeEnum.TYPE_27);
-        integralParam.setBusinessId("live_completion_" + recordId);
+        long sessionTs = record.getCreateTime() != null ? record.getCreateTime().getTime() : System.currentTimeMillis();
+        integralParam.setBusinessId("live_completion_" + recordId + "_" + sessionTs);
         integralParam.setPoints(Long.valueOf(record.getPointsAwarded()));
         R integralResult = fsUserIntegralLogsService.addIntegralTemplate(integralParam);
         if (integralResult == null || !"200".equals(String.valueOf(integralResult.get("code")))) {
@@ -389,12 +424,12 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         config.setEnabled(false);
         config.setCompletionRate(null);
         config.setPointsConfig(null);
-        
+
         String configJson = live.getConfigJson();
         if (configJson == null || configJson.isEmpty()) {
             return config;
         }
-        
+
         try {
             JSONObject jsonConfig = JSON.parseObject(configJson);
 
@@ -416,10 +451,10 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         } catch (Exception e) {
             log.warn("解析完课积分配置失败, liveId={}, 配置未生效", live.getLiveId(), e);
         }
-        
+
         return config;
     }
-    
+
     /**
      * 计算积分
      * 根据连续天数和积分配置计算应得积分
@@ -437,7 +472,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         }
         return pointsConfig[continuousDays - 1];
     }
-    
+
     /**
      * 完课积分配置内部类
      */
@@ -445,27 +480,27 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         private boolean enabled;
         private Integer completionRate;
         private int[] pointsConfig;
-        
+
         public boolean isEnabled() {
             return enabled;
         }
-        
+
         public void setEnabled(boolean enabled) {
             this.enabled = enabled;
         }
-        
+
         public Integer getCompletionRate() {
             return completionRate;
         }
-        
+
         public void setCompletionRate(Integer completionRate) {
             this.completionRate = completionRate;
         }
-        
+
         public int[] getPointsConfig() {
             return pointsConfig;
         }
-        
+
         public void setPointsConfig(int[] pointsConfig) {
             this.pointsConfig = pointsConfig;
         }

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

@@ -657,6 +657,19 @@ public class LiveServiceImpl implements ILiveService
         }
     }
 
+    private void publishLiveEndWsMessage(Long liveId) {
+        try {
+            JSONObject payload = new JSONObject();
+            payload.put("liveId", liveId);
+            payload.put("cmd", "live_end");
+            payload.put("msg", "结束直播");
+            payload.put("status", 3);
+            redisCache.publish(LiveKeysConstant.LIVE_WS_BROADCAST_CHANNEL, payload.toJSONString());
+        } catch (Exception e) {
+            log.warn("发布直播结束 WebSocket 广播失败, liveId={}", liveId, e);
+        }
+    }
+
     @Override
     public String getGotoWxAppLiveLink(String linkStr, String appId) {
         CloseableHttpClient client = null;
@@ -1103,6 +1116,8 @@ public class LiveServiceImpl implements ILiveService
         clearLiveCache(live.getLiveId());
         // 结束直播清理看课时长,避免结束后继续累计
         liveWatchUserService.resetWatchDurationOnLiveEnd(live.getLiveId());
+        // 通知 App 端直播已结束
+        publishLiveEndWsMessage(live.getLiveId());
 
         return R.ok();
     }

+ 61 - 9
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -1218,13 +1218,21 @@ public class QwUserServiceImpl implements IQwUserService
 //                return R.ok("登录成功");
 //            }
 //        }
+        // 二次登录时 uuid 会复用,必须清掉上次扫码残留状态(如 100004),否则会误弹验证码框
+        if (qwUser.getUid() != null) {
+            redisCache.deleteObject("qrCodeUid:" + qwUser.getUid());
+            redisCache.deleteObject("qrCode:uuid:" + qwUser.getUid());
+        }
+        redisCache.deleteObject("qrCode:qwUserId:" + loginParam.getQwUserId());
+        redisCache.deleteObject("qrCodeUid:qwUserId:" + loginParam.getQwUserId());
+
         //自动登录失败
         WxWorkInitDTO wxWorkInitDTO = new WxWorkInitDTO();
         if (qwUser.getVid()!=null){
             wxWorkInitDTO.setVid(qwUser.getVid());
         }
         WxWorkResponseDTO<WxWorkInitRespDTO> init = wxWorkService.init(wxWorkInitDTO,serverId);
-        if (init.getErrcode()!=0){
+        if (init == null || init.getErrcode() == null || init.getErrcode() != 0 || init.getData() == null){
             //vid登录失败
             return R.error("初始化失败");
         }
@@ -1235,7 +1243,9 @@ public class QwUserServiceImpl implements IQwUserService
         u.setUid(data.getUuid());
 
         qwUserMapper.updateQwUser(u);
-        if (data.getIs_login().equals("true")) {
+        // 新 uuid(或复用 uuid)下的旧状态一并清掉,避免轮询读到过期 100004
+        redisCache.deleteObject("qrCodeUid:" + data.getUuid());
+        if ("true".equals(data.getIs_login())) {
             updateIpadStatus(qwUser.getId(),1);
             redisCache.setCacheObject("qrCode:uuid:"+data.getUuid(),loginParam.getQwUserId());
             return R.ok("登录成功");
@@ -1249,6 +1259,10 @@ public class QwUserServiceImpl implements IQwUserService
         WxWorkGetQrCodeDTO wxWorkGetQrCodeDTO = new WxWorkGetQrCodeDTO();
         wxWorkGetQrCodeDTO.setUuid(data.getUuid());
         WxWorkResponseDTO<WxWorkGetQrCodeRespDTO> qrCode = wxWorkService.getQrCode(wxWorkGetQrCodeDTO,serverId);
+        if (qrCode == null || qrCode.getErrcode() == null || qrCode.getErrcode() != 0 || qrCode.getData() == null
+                || qrCode.getData().getKey() == null) {
+            return R.error("获取登录二维码失败,请重试");
+        }
         WxWorkGetQrCodeRespDTO qrData = qrCode.getData();
 
 
@@ -1276,8 +1290,9 @@ public class QwUserServiceImpl implements IQwUserService
         if (uid == null) {
             return R.error("未获取到Uid");
         }
-        Integer status = redisCache.getCacheObject("qrCodeUid:" + uid);
-        if (status==null){
+        Object statusObj = redisCache.getCacheObject("qrCodeUid:" + uid);
+        Integer status = statusObj instanceof Number ? ((Number) statusObj).intValue() : null;
+        if (status == null){
             return R.ok("100000");
         }
 
@@ -1291,19 +1306,56 @@ public class QwUserServiceImpl implements IQwUserService
         String key = redisCache.getCacheObject("qrCode:qwUserId:" + loginParam.getQwUserId());
         String uid = redisCache.getCacheObject("qrCodeUid:qwUserId:" + loginParam.getQwUserId());
         if (key==null||uid==null){
-            return R.error("初始化错误请重新登录");
+            return R.error("初始化错误,请重新点击登录后再扫码");
+        }
+        if (loginParam.getCode() == null || loginParam.getCode().trim().isEmpty()) {
+            return R.error("请输入验证码");
+        }
+        Object statusObj = redisCache.getCacheObject("qrCodeUid:" + uid);
+        Integer status = statusObj instanceof Number ? ((Number) statusObj).intValue() : null;
+        if (status != null && (status == 22 || status == 23)) {
+            return R.error(status == 22
+                    ? "扫码企微与员工所属企业不一致,请重新点击登录,使用正确企微扫码"
+                    : "扫码账号与员工不一致,请重新点击登录后再扫码");
+        }
+        if (status != null && status == 104001) {
+            return R.ok();
+        }
+        if (status != null && status == 100050) {
+            // 已提交过验证码,等待主机回调结果
+            return R.ok("NOT_NEED_VERIFY");
+        }
+        if (status == null || status != 100004) {
+            return R.error("当前不在验证码校验状态,请重新点击登录后再扫码");
         }
         QwUser qwUser = qwUserMapper.selectQwUserById(loginParam.getQwUserId());
+        if (qwUser == null || qwUser.getServerId() == null) {
+            return R.error("账号或主机信息不存在,请重新扫码登录");
+        }
         WxWorkCheckCodeDTO wxWorkCheckCodeDTO = new WxWorkCheckCodeDTO();
         wxWorkCheckCodeDTO.setUuid(uid);
-        wxWorkCheckCodeDTO.setCode(loginParam.getCode());
+        wxWorkCheckCodeDTO.setCode(loginParam.getCode().trim());
         wxWorkCheckCodeDTO.setQrcodeKey(key);
-        System.out.println("登录参数:"+wxWorkCheckCodeDTO);
         WxWorkResponseDTO wxWorkResponseDTO = wxWorkService.CheckCode(wxWorkCheckCodeDTO,qwUser.getServerId());
-        if (wxWorkResponseDTO.getErrcode()==0){
+        if (wxWorkResponseDTO != null && wxWorkResponseDTO.getErrcode() != null && wxWorkResponseDTO.getErrcode() == 0){
+            // 100050=已提交验证码,等待 104001/22/23;避免清空后被迟到的 100004 再次误弹
+            redisCache.setCacheObject("qrCodeUid:" + uid, 100050, 10, TimeUnit.MINUTES);
             return R.ok();
         }
-        return R.error("验证码错误重新输入验证码或重新扫码登录");
+        String rawMsg = wxWorkResponseDTO != null ? wxWorkResponseDTO.getErrmsg() : null;
+        // 主机侧已不需要验证码:进入等待终态,前端继续轮询
+        if (rawMsg != null && rawMsg.toLowerCase().contains("qrcode_not_need_verify")) {
+            redisCache.setCacheObject("qrCodeUid:" + uid, 100050, 10, TimeUnit.MINUTES);
+            return R.ok("NOT_NEED_VERIFY");
+        }
+        if (rawMsg != null && rawMsg.toLowerCase().contains("qr_code_expire")) {
+            redisCache.deleteObject("qrCode:qwUserId:" + loginParam.getQwUserId());
+            return R.error("二维码已过期,请关闭后重新点击登录再扫码");
+        }
+        String errMsg = (rawMsg != null && !rawMsg.isEmpty())
+                ? rawMsg
+                : "验证码错误,请重新输入或重新扫码登录";
+        return R.error(errMsg);
     }
 
     @Override

+ 10 - 2
fs-service/src/main/java/com/fs/wxwork/service/WxWorkServiceImpl.java

@@ -99,8 +99,16 @@ public class WxWorkServiceImpl implements WxWorkService {
     @Override
     public WxWorkResponseDTO CheckCode(WxWorkCheckCodeDTO param,Long serverId) {
         String url = getUrl(serverId) + "/CheckCode";
-        return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO>() {
-        });
+        try {
+            return WxWorkHttpUtil.postWithType(url, param, new TypeReference<WxWorkResponseDTO>() {
+            });
+        } catch (Exception e) {
+            log.error("CheckCode 调用失败, serverId={}, uuid={}, err={}", serverId, param != null ? param.getUuid() : null, e.getMessage());
+            WxWorkResponseDTO resp = new WxWorkResponseDTO();
+            resp.setErrcode(-1);
+            resp.setErrmsg("验证码校验服务异常,请重新点击登录后再扫码");
+            return resp;
+        }
     }
 
     @Override

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

@@ -72,6 +72,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         WHERE id = #{id}
     </update>
 
+    <!-- 同日重开复用:刷新本场数据并重置领取状态,更新 create_time 作为本场标记 -->
+    <update id="reuseRecordForNewSession" parameterType="com.fs.live.domain.LiveCompletionPointsRecord">
+        UPDATE live_completion_points_record
+        SET watch_duration = #{watchDuration},
+            video_duration = #{videoDuration},
+            completion_rate = #{completionRate},
+            continuous_days = #{continuousDays},
+            points_awarded = #{pointsAwarded},
+            last_completion_date = #{lastCompletionDate},
+            receive_status = 0,
+            receive_time = NULL,
+            create_time = NOW(),
+            update_time = NOW()
+        WHERE id = #{id}
+    </update>
+
     <!-- 查询用户某天的完课记录 -->
     <select id="selectByUserAndDate" resultMap="LiveCompletionPointsRecordResult">
         SELECT * FROM live_completion_points_record