天机学堂(学习计划和进度)

经过前面的努力,我们已经完成了《我的课程表》相关的功能的基础部分,不过还有功能实现的并不完善。还记得昨天给大家的练习题吗?《查询我正在学习的课程》,在原型图中有这样的一个需求:

我们需要在查询结果中返回已学习课时数、正在学习的章节名称。虽然我们在learning_lesson表中设计了两个字段:

  • learned_sections:已学习章节数

  • latest_learn_time:最近学习时间

但是,这几个字段默认都是空或0,我们该如何得知用户到底学习了几节?最近一次学习是什么时候?最近一次学习的是第几章节呢?

以上的问题归纳下来,就是一个学习进度统计问题,这在在线教育、视频播放领域是一个非常常见的问题。因此,学会了解决这套解决方案,你就能游刃有余的应对相关行业的类似问题了。

大家在学习这套解决方案的同时,也可以增强下面的能力:

  • 需求分析和表设计能力

  • 复杂SQL的编写能力

  • 处理高并发写数据库的能力

1.分析产品原型

大部分人的学习自律性是比较差的,属于“买了就算会了”的状态。如果学员学习积极性下降,学习结果也会不尽人意,从而产生挫败感。导致购买课程的欲望也会随之下降,形成恶性循环,不利于我们卖课。

所以,我们推出学习计划的功能,让学员制定一套学习计划,每周要学几节课。系统会做数据统计,每一周计划是否达标,达标后给予奖励,未达标则提醒用户,达到督促用户持续学习的目的。

用户学习效果好了,产生了好的结果,就会有继续学习、购买课程的欲望,形成良性循环。

因此,学习计划、学习进度统计其实是学习辅助中必不可少的环节。

1.1.分析业务流程

我们从两个业务点来分析:

  • 学习计划

  • 学习进度统计

1.1.1.学习计划

在我的课程页面,可以对有效的课程添加学习计划:

学习计划就是简单设置一下用户每周计划学习几节课:

这个在昨天的数据库设计中已经有对应的字段了,只不过功能尚未完成。

有了计划以后,我们就可以在我的课程页面展示用户计划的完成情况,提醒用户尽快学习:

可以看到,在学习计划中是需要统计用户“已经学习的课时数量”的。那么我们该如何统计用户学了多少课时呢?

1.1.2.学习进度统计

要统计学习进度,需要先弄清楚用户学习的方式,学习的内容。在原型图《课程学习页-录播课-课程学习页-目录》中,可以看到学习课程的原型图:

一个课程往往包含很多个章(chapter),每一章下又包含了很多小节(section)。章本身没有课程内容,只是划分课程的一个概念,因此统计学习进度就是看用户学了多少个小节。

小节也分两种,一种是视频;一种是每章最后的阶段考试。用户学完一个视频,或者参加了最终的考试都算学完了一个小节。

考试只要提交了就算学完了,比较容易判断是否学完。但是视频该如何统计呢?达到什么样的标准才算这一小节的视频学完了呢?

这里我们不能要求用户一定要播放进度到100%,太苛刻了。所以,天机学堂的产品是这样设计的:

因此,只要视频播放进度达到50%就算是完成本节学习了。所以用户在播放视频的过程中,需要不断提交视频的播放进度,当我们发现视频进度超过50%时就可以标记这一小节为已学完

当然,我们不能仅仅记录视频是否学完,还应该记录用户具体播放的进度到了第几秒。只有这样在用户关闭视频,再次播放时我们才能实现视频自动续播功能,用户体验会比较好。

也就是说,要记录用户学习进度,需要记录下列核心信息:

  • 小节的基础信息(id、关联的课程id等)

  • 当前的播放进度(第几秒)

  • 当前小节是否已学完(播放进度是否超50%)

用户每学习一个小节,就会新增一条学习记录,当该课程的全部小节学习完毕,则该课程就从学习中进入已学完状态了。整体流程如图:

1.2.业务接口统计

接下来我们分析一下这部分功能相关的接口有哪些,按照用户的学习顺序,依次有下面几个接口:

  • 创建学习计划

  • 查询学习记录

  • 提交学习记录

  • 查询我的计划

1.2.1.创建学习计划

在个人中心的我的课表列表中,没有学习计划的课程都会有一个创建学习计划的按钮,在原型图就能看到:

创建学习计划,本质就是让用户设定自己每周的学习频率:

而学习频率我们在设计learning_lesson表的时候已经有两个字段来表示了:

CREATE TABLE `learning_lesson`  (`id` bigint NOT NULL COMMENT '主键',`user_id` bigint NOT NULL COMMENT '学员id',`course_id` bigint NOT NULL COMMENT '课程id',`status` tinyint NULL DEFAULT 0 COMMENT '课程状态,0-未学习,1-学习中,2-已学完,3-已失效',`week_freq` tinyint NULL DEFAULT NULL COMMENT '每周学习频率,每周3天,每天2节,则频率为6',`plan_status` tinyint NOT NULL DEFAULT 0 COMMENT '学习计划状态,0-没有计划,1-计划进行中',`learned_sections` int NOT NULL DEFAULT 0 COMMENT '已学习小节数量',`latest_section_id` bigint NULL DEFAULT NULL COMMENT '最近一次学习的小节id',`latest_learn_time` datetime NULL DEFAULT NULL COMMENT '最近一次学习的时间',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`expire_time` datetime NOT NULL COMMENT '过期时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `idx_user_id`(`user_id`, `course_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生课程表' ROW_FORMAT = Dynamic;

当我们创建学习计划时,就是更新learning_lesson表,写入week_freq并更新plan_status为计划进行中即可。因此请求参数就是课程的id、每周学习频率。

再按照Restful风格,最终接口如下:

参数

说明

请求方式

POST

请求路径

/lessons/plans

请求参数

参数名

类型

说明

courseId

Long

课程id

weekFreq

Integer

计划每周学习频率

返回值

1.2.2.查询学习记录

用户创建完计划自然要开始学习课程,在用户学习视频的页面,首先要展示课程的一些基础信息。例如课程信息、章节目录以及每个小节的学习进度:

其中,课程、章节、目录信息等数据都在课程微服务,而学习进度肯定是在学习微服务。课程信息是必备的,而学习进度却不一定存在

因此,查询这个接口的请求肯定是请求到课程微服务,查询课程、章节信息,再由课程微服务向学习微服务查询学习进度,合并后一起返回给前端即可。

所以,学习中心要提供一个查询章节学习进度的Feign接口,事实上这个接口已经在tj-api模块的LearningClient中定义好了:


/*** 查询当前用户指定课程的学习进度* @param courseId 课程id* @return 课表信息、学习记录及进度信息*/
@GetMapping("/learning-records/course/{courseId}")
LearningLessonDTO queryLearningRecordByCourse(@PathVariable("courseId") Long courseId);

对应的DTO也都在tj-api模块定义好了,因此整个接口规范如下:

参数

说明

请求方式

GET

请求路径

/learning-records/course/{courseId}

请求参数

路径占位符参数,courseId:课表关联的课程id

返回值

参数名

类型

说明

id

Long

课表id

latestSectionid

Long

最近学习的小节id

records

array

参数名

类型

说明

sectionId

Long

小节id

moment

int

视频播放进度,第几秒

finished

boolean

是否学完

1.2.3.提交学习记录

之前分析业务流程的时候已经聊过,学习记录就是用户当前学了哪些小节,以及学习到该小节的进度如何。而小节类型分为考试、视频两种。

  • 考试比较简单,只要提交了就说明这一节学完了。

  • 视频比较麻烦,需要记录用户的播放进度,进度超过50%才算学完。因此视频播放的过程中需要不断提交播放进度到服务端,而服务端则需要保存学习记录到数据库。

只要记录了用户学过的每一个小节,以及小节对应的学习进度、是否学完。无论是视频续播、还是统计学习计划进度,都可以轻松实现了。

因此,提交学习记录就是提交小节的信息和小节的学习进度信息。考试提交一次即可,视频则是播放中频繁提交。提交的信息包括两大部分:

  • 小节的基本信息

    • 小节id

    • lessonId

    • 小节类型:可能是视频,也可能是考试。考试无需提供播放进度信息

    • 提交时间

  • 播放进度信息

    • 视频时长:时长结合播放进度可以判断有没有超过50%

    • 视频播放进度:也就是第几秒

综上,提交学习记录的接口信息如下:

参数

说明

请求方式

POST

请求路径

/learning-records

请求参数

参数名

类型

说明

lessonId

long

课表id

sectionId

long

小节id

sectionType

int

小节类型:1-视频,2-考试

commitTime

LocalDateTime

提交时间

duration

int

视频总时长,单位秒

moment

int

视频播放进度,单位秒

返回值

接口描述

  • 视频播放:当播放进度超过50%则判定为本节学完

  • 考试:考试结束时提交记录,直接判定为本节学完

1.2.4.查询我的学习计划

在个人中心的我的课程页面,会展示用户的学习计划及本周的学习进度,原型如图:

需要注意的是这个查询其实是一个分页查询,因为页面最多展示10行,而学员同时在学的课程可能会超过10个,这个时候就会分页展示,当然这个分页可能是滚动分页,所以没有进度条。另外,查询的是我的学习计划,隐含的查询条件就是当前登录用户,这个无需传递,通过请求头即可获得。

因此查询参数只需要分页参数即可。

查询结果中有很多对于已经学习的小节数量的统计,因此将来我们一定要保存用户对于每一个课程的学习记录,哪些小节已经学习了,哪些已经学完了。只有这样才能统计出学习进度。

查询的结果如页面所示,分上下两部分。:

总的统计信息:

  • 本周已完成总章节数:需要对学习记录做统计

  • 课程总计划学习数量:累加课程的总计划学习频率即可

  • 本周学习积分:积分暂不实现

正在学习的N个课程信息的集合,其中每个课程包含下列字段:

  • 该课程本周学了几节:统计学习记录

  • 计划学习频率:在learning_lesson表中有对应字段

  • 该课程总共学了几节:在learning_lesson表中有对应字段

  • 课程总章节数:查询课程微服务

  • 该课程最近一次学习时间:在learning_lesson表中有对应字段

综上,查询学习计划进度的接口信息如下:

参数

说明

请求方式

GET

请求路径

/lessons/plans

请求参数

分页参数:PageQuery

返回值

参数名

类型

说明

weekPoints

int

本周学习积分

weekFinished

int

本周已学完小节数量

weekTotalPlan

int

本周计划学习小节数量

list

Array

参数

类型

说明

courseId

Long

课程id

courseName

String

课程名称

weekLearnedSections

int

本周学习的小节数量

weekFreq

int

本周计划学习数量

learnedSections

int

总已学习小节数量

sections

int

总小节数量

latestLearnTime

LocalDateTime

最近一次学习时间

1.3.设计数据库

数据表的设计要满足学习计划、学习进度的功能需求。学习计划信息在learning_lesson表中已经设计,因此我们关键是设计学习进度记录表即可。

按照之前的分析,用户学习的课程包含多个小节,小节的类型包含两种:

  • 视频:视频播放进度超过50%就算当节学完

  • 考试:考完就算一节学完

学习进度除了要记录哪些小节学完,还要记录学过的小节、每小节的播放的进度(方便续播)。因此,需要记录的数据就包含以下部分:

  • 学过的小节的基础信息

    • 小节id

    • 小节对应的lessonId

    • 用户id:学习课程的人

  • 小节的播放进度信息

    • 视频播放进度:也就是播放到了第几秒

    • 是否已经学完:播放进度有没有超过50%

    • 第一次学完的时间:用户可能重复学习,第一次从未学完到学完的时间要记录下来

再加上一些表基础字段,整张表结构就出来了:

CREATE TABLE IF NOT EXISTS `learning_record` (`id` bigint NOT NULL COMMENT '学习记录的id',`lesson_id` bigint NOT NULL COMMENT '对应课表的id',`section_id` bigint NOT NULL COMMENT '对应小节的id',`user_id` bigint NOT NULL COMMENT '用户id',`moment` int DEFAULT '0' COMMENT '视频的当前观看时间点,单位秒',`finished` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否完成学习,默认false',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '第一次观看时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(最近一次观看时间)',PRIMARY KEY (`id`) USING BTREE,KEY `idx_update_time` (`update_time`) USING BTREE,KEY `idx_user_id` (`user_id`) USING BTREE,KEY `idx_lesson_id` (`lesson_id`,`section_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学习记录表';

课前资料也提供了对应的SQL语句:

1.4.生成基础代码

接下来我们就可以生成数据库实体对应的基础代码了。

1.4.1.创建新分支

动手之前,不要忘了开发新功能需要创建新的分支。这里我们依然在DEV分支基础上,创建一个新的feature类型分支:feature-learning-records

我们可以选择用命令:

git checkout -b feature-learning-records

也可以选择图形界面方式:

1.4.2.代码生成

同样是使用MybatisPlus插件,这里不再赘述。效果如下:

需要注意的是,我们同样需要把生成的实体类的ID策略改成雪花算法:

另外,按照Restful风格, 把controller的路径做修改:

1.4.3.类型枚举

在昨天学习的课表中,有一种状态枚举,就是把课程的状态通过枚举定义出来,避免出现错误。而在学习记录中,有一个section_type字段,代表记录的小节有两种类型:

  • 1,视频类型

  • 2,考试类型

为了方便我们也定义为枚举,称为类型枚举:

具体代码:

package com.tianji.learning.enums;import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.tianji.common.enums.BaseEnum;
import lombok.Getter;@Getter
public enum SectionType implements BaseEnum {VIDEO(1, "视频"),EXAM(2, "考试"),;@JsonValue@EnumValueint value;String desc;SectionType(int value, String desc) {this.value = value;this.desc = desc;}@JsonCreator(mode = JsonCreator.Mode.DELEGATING)public static SectionType of(Integer value){if (value == null) {return null;}for (SectionType status : values()) {if (status.equalsValue(value)) {return status;}}return null;}
}

2.实现接口

2.1.查询学习记录

首先回顾一下接口基本信息:

参数

说明

请求方式

GET

请求路径

/learning-records/course/{courseId}

请求参数

路径占位符参数,courseId:课表关联的课程id

返回值

参数名

类型

说明

id

Long

课表id

latestSectionid

Long

最近学习的小节id

records

array

参数名

类型

说明

sectionId

Long

小节id

moment

int

视频播放进度,第几秒

finished

boolean

是否学完

2.1.1.思路分析

做个接口是给课程微服务调用的,因此在tj-api模块的LearningClient中定义好了:


/*** 查询当前用户指定课程的学习进度* @param courseId 课程id* @return 课表信息、学习记录及进度信息*/
@GetMapping("/learning-records/course/{courseId}")
LearningLessonDTO queryLearningRecordByCourse(@PathVariable("courseId") Long courseId);

对应的DTO也都在tj-api模块定义好了。我们直接实现接口即可。

由于请求参数是courseId,而返回值中包含lessonIdlatestSectionid都在learning_lesson表中,因此我们需要根据courseId和userId查询出lesson信息。然后再根据lessonId查询学习记录。整体流程如下:

  • 获取当前登录用户id

  • 根据courseId和userId查询LearningLesson

  • 判断是否存在或者是否过期

    • 如果不存在或过期直接返回空

    • 如果存在并且未过期,则继续

  • 查询lesson对应的所有学习记录

2.1.2.代码实现

首先在tj-learning模块下的com.tianji.learning.controller.LearningRecordController下定义接口:

package com.tianji.learning.controller;import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.learning.service.ILearningRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;/*** <p>* 学习记录表 前端控制器* </p>*/
@RestController
@RequestMapping("/learning-records")
@Api(tags = "学习记录的相关接口")
@RequiredArgsConstructor
public class LearningRecordController {private final ILearningRecordService recordService;@ApiOperation("查询指定课程的学习记录")@GetMapping("/course/{courseId}")public LearningLessonDTO queryLearningRecordByCourse(@ApiParam(value = "课程id", example = "2") @PathVariable("courseId") Long courseId){return recordService.queryLearningRecordByCourse(courseId);}
}

然后在com.tianji.learning.service.ILearningRecordService中定义方法:

package com.tianji.learning.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.learning.domain.po.LearningRecord;/*** <p>* 学习记录表 服务类* </p>*/
public interface ILearningRecordService extends IService<LearningRecord> {LearningLessonDTO queryLearningRecordByCourse(Long courseId);
}

最后在com.tianji.learning.service.impl.LearningRecordServiceImpl中定义实现类:

package com.tianji.learning.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.api.dto.leanring.LearningRecordDTO;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.mapper.LearningRecordMapper;
import com.tianji.learning.service.ILearningLessonService;
import com.tianji.learning.service.ILearningRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;import java.util.List;/*** <p>* 学习记录表 服务实现类* </p>** @author 虎哥* @since 2022-12-10*/
@Service
@RequiredArgsConstructor
public class LearningRecordServiceImpl extends ServiceImpl<LearningRecordMapper, LearningRecord> implements ILearningRecordService {private final ILearningLessonService lessonService;@Overridepublic LearningLessonDTO queryLearningRecordByCourse(Long courseId) {// 1.获取登录用户Long userId = UserContext.getUser();// 2.查询课表LearningLesson lesson = lessonService.queryByUserAndCourseId(userId, courseId);// 3.查询学习记录// select * from xx where lesson_id = #{lessonId}List<LearningRecord> records = lambdaQuery().eq(LearningRecord::getLessonId, lesson.getId()).list();// 4.封装结果LearningLessonDTO dto = new LearningLessonDTO();dto.setId(lesson.getId());dto.setLatestSectionId(lesson.getLatestSectionId());dto.setRecords(BeanUtils.copyList(records, LearningRecordDTO.class));return dto;}
}

其中查询课表的时候,需要调用ILessonService中的queryByUserAndCourseId()方法,该方法代码如下:

@Override
public LearningLesson queryByUserAndCourseId(Long userId, Long courseId) {return getOne(buildUserIdAndCourseIdWrapper(userId, courseId));
}private LambdaQueryWrapper<LearningLesson> buildUserIdAndCourseIdWrapper(Long userId, Long courseId) {LambdaQueryWrapper<LearningLesson> queryWrapper = new QueryWrapper<LearningLesson>().lambda().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getCourseId, courseId);return queryWrapper;
}

2.2.提交学习记录

回顾一下接口信息:

参数

说明

请求方式

POST

请求路径

/learning-records

请求参数

参数名

类型

说明

lessonId

long

课表id

sectionId

long

小节id

sectionType

int

小节类型:1-视频,2-考试

commitTime

LocalDateTime

提交时间

duration

int

视频总时长,单位秒

moment

int

视频播放进度,单位秒

返回值

接口描述

  • 视频播放:当播放进度超过50%则判定为本节学完

  • 考试:考试结束时提交记录,直接判定为本节学完

2.2.1.思路分析

学习记录就是用户当前学了哪些小节,以及学习到该小节的进度如何。而小节类型分为考试、视频两种。

  • 考试比较简单,只要提交了就说明这一节学完了。

  • 视频比较麻烦,需要记录用户的播放进度,进度超过50%才算学完。因此视频播放的过程中需要不断提交播放进度到服务端,而服务端则需要保存学习记录到数据库。

以上信息都需要保存到learning_record表中。

特别需要注意的是,学习记录learning_record表记录的是每一个小节的学习进度。而在learning_lesson表也需要记录一些学习进度相关字段:

这些字段是整个课程的进度统计:

  • learned_sections:已学习小节数量

  • latest_section_id:最近一次学习的小节id

  • latest_learn_time:最近一次学习时间

每当有一个小节被学习,都应该更新latest_section_idlatest_learn_time;每当有一个小节学习完后,learned_sections都应该累加1。不过这里有一点容易出错的地方:

  • 考试只会被参加一次,考试提交则小节学完,learned_sections累加1

  • 视频可以被重复播放,只有在第一次学完一个视频时,learned_sections才需要累加1

那么问题来了,如何判断视频是否是第一次学完?我认为应该同时满足两个条件:

  • 视频播放进度超过50%

  • 之前学习记录的状态为未学完

另外,随着learned_sections字段不断累加,最终会到达课程的最大小节数,这就意味着当前课程被全部学完了。那么课程状态需要从“学习中”变更为“已学完”。

综上,最终的提交学习记录处理流程如图:

2.2.2.表单实体

请求参数比较多,所以需要定义一个表单DTO实体,这个在课前资料已经提供好了:

具体代码如下:

package com.tianji.learning.domain.dto;import com.tianji.common.validate.annotations.EnumValid;
import com.tianji.learning.enums.SectionType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;@Data
@ApiModel(description = "学习记录")
public class LearningRecordFormDTO {@ApiModelProperty("小节类型:1-视频,2-考试")@NotNull(message = "小节类型不能为空")@EnumValid(enumeration = {1, 2}, message = "小节类型错误,只能是:1-视频,2-考试")private SectionType sectionType;@ApiModelProperty("课表id")@NotNull(message = "课表id不能为空")private Long lessonId;@ApiModelProperty("对应节的id")@NotNull(message = "节的id不能为空")private Long sectionId;@ApiModelProperty("视频总时长,单位秒")private Integer duration;@ApiModelProperty("视频的当前观看时长,单位秒,第一次提交填0")private Integer moment;@ApiModelProperty("提交时间")private LocalDateTime commitTime;
}

2.2.3.代码实现

首先在tj-learning模块下的com.tianji.learning.controller.LearningRecordController下定义接口:

package com.tianji.learning.controller;import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.learning.domain.dto.LearningRecordFormDTO;
import com.tianji.learning.service.ILearningRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;/*** <p>* 学习记录表 前端控制器* </p>** @author 虎哥* @since 2022-12-10*/
@RestController
@RequestMapping("/learning-records")
@Api(tags = "学习记录的相关接口")
@RequiredArgsConstructor
public class LearningRecordController {private final ILearningRecordService recordService;@ApiOperation("查询指定课程的学习记录")@GetMapping("/course/{courseId}")public LearningLessonDTO queryLearningRecordByCourse(@ApiParam(value = "课程id", example = "2") @PathVariable("courseId") Long courseId){return recordService.queryLearningRecordByCourse(courseId);}@ApiOperation("提交学习记录")@PostMappingpublic void addLearningRecord(@RequestBody LearningRecordFormDTO formDTO){recordService.addLearningRecord(formDTO);}
}

然后在com.tianji.learning.service.ILearningRecordService中定义方法:

package com.tianji.learning.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.learning.domain.dto.LearningRecordFormDTO;
import com.tianji.learning.domain.po.LearningRecord;/*** <p>* 学习记录表 服务类* </p>** @author 虎哥* @since 2022-12-10*/
public interface ILearningRecordService extends IService<LearningRecord> {LearningLessonDTO queryLearningRecordByCourse(Long courseId);void addLearningRecord(LearningRecordFormDTO formDTO);
}

最后在com.tianji.learning.service.impl.LearningRecordServiceImpl中定义实现类:

package com.tianji.learning.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.client.course.CourseClient;
import com.tianji.api.dto.course.CourseFullInfoDTO;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.api.dto.leanring.LearningRecordDTO;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.exceptions.DbException;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.domain.dto.LearningRecordFormDTO;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.enums.LessonStatus;
import com.tianji.learning.enums.SectionType;
import com.tianji.learning.mapper.LearningRecordMapper;
import com.tianji.learning.service.ILearningLessonService;
import com.tianji.learning.service.ILearningRecordService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;/*** <p>* 学习记录表 服务实现类* </p>*/
@Service
@RequiredArgsConstructor
public class LearningRecordServiceImpl extends ServiceImpl<LearningRecordMapper, LearningRecord> implements ILearningRecordService {private final ILearningLessonService lessonService;private final CourseClient courseClient;// 。。。略@Override@Transactionalpublic void addLearningRecord(LearningRecordFormDTO recordDTO) {// 1.获取登录用户Long userId = UserContext.getUser();// 2.处理学习记录boolean finished = false;if (recordDTO.getSectionType() == SectionType.VIDEO) {// 2.1.处理视频finished = handleVideoRecord(userId, recordDTO);}else{// 2.2.处理考试finished = handleExamRecord(userId, recordDTO);}// 3.处理课表数据handleLearningLessonsChanges(recordDTO, finished);}private void handleLearningLessonsChanges(LearningRecordFormDTO recordDTO, boolean finished) {// 1.查询课表LearningLesson lesson = lessonService.getById(recordDTO.getLessonId());if (lesson == null) {throw new BizIllegalException("课程不存在,无法更新数据!");}// 2.判断是否有新的完成小节boolean allLearned = false;if(finished){// 3.如果有新完成的小节,则需要查询课程数据CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);if (cInfo == null) {throw new BizIllegalException("课程不存在,无法更新数据!");}// 4.比较课程是否全部学完:已学习小节 >= 课程总小节allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();     }// 5.更新课表lessonService.lambdaUpdate().set(lesson.getLearnedSections() == 0, LearningLesson::getStatus, LessonStatus.LEARNING.getValue()).set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED.getValue()).set(!finished, LearningLesson::getLatestSectionId, recordDTO.getSectionId()).set(!finished, LearningLesson::getLatestLearnTime, recordDTO.getCommitTime()).setSql(finished, "learned_sections = learned_sections + 1").eq(LearningLesson::getId, lesson.getId()).update();}private boolean handleVideoRecord(Long userId, LearningRecordFormDTO recordDTO) {// 1.查询旧的学习记录LearningRecord old = queryOldRecord(recordDTO.getLessonId(), recordDTO.getSectionId());// 2.判断是否存在if (old == null) {// 3.不存在,则新增// 3.1.转换POLearningRecord record = BeanUtils.copyBean(recordDTO, LearningRecord.class);// 3.2.填充数据record.setUserId(userId);// 3.3.写入数据库boolean success = save(record);if (!success) {throw new DbException("新增学习记录失败!");}return false;}// 4.存在,则更新// 4.1.判断是否是第一次完成boolean finished = !old.getFinished() && recordDTO.getMoment() * 2 >= recordDTO.getDuration();// 4.2.更新数据boolean success = lambdaUpdate().set(LearningRecord::getMoment, recordDTO.getMoment()).set(finished, LearningRecord::getFinished, true).set(finished, LearningRecord::getFinishTime, recordDTO.getCommitTime()).eq(LearningRecord::getId, old.getId()).update();if(!success){throw new DbException("更新学习记录失败!");}return finished ;}private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {return lambdaQuery().eq(LearningRecord::getLessonId, lessonId).eq(LearningRecord::getSectionId, sectionId).one();}private boolean handleExamRecord(Long userId, LearningRecordFormDTO recordDTO) {// 1.转换DTO为POLearningRecord record = BeanUtils.copyBean(recordDTO, LearningRecord.class);// 2.填充数据record.setUserId(userId);record.setFinished(true);record.setFinishTime(recordDTO.getCommitTime());// 3.写入数据库boolean success = save(record);if (!success) {throw new DbException("新增考试记录失败!");}return true;}
}

2.3.创建学习计划

回顾下接口信息:

参数

说明

请求方式

POST

请求路径

/lessons/plans

请求参数

参数名

类型

说明

courseId

Long

课程id

weekFreq

Integer

计划每周学习频率

返回值

2.3.1.思路分析

创建学习计划,本质就是让用户设定自己每周的学习频率:

虽说接口是创建学习计划,但本质这是一个更新的接口。因为学习计划字段都保存在learning_lesson表中。

CREATE TABLE `learning_lesson`  (`id` bigint NOT NULL COMMENT '主键',`user_id` bigint NOT NULL COMMENT '学员id',`course_id` bigint NOT NULL COMMENT '课程id',`status` tinyint NULL DEFAULT 0 COMMENT '课程状态,0-未学习,1-学习中,2-已学完,3-已失效',`week_freq` tinyint NULL DEFAULT NULL COMMENT '每周学习频率,每周3天,每天2节,则频率为6',`plan_status` tinyint NOT NULL DEFAULT 0 COMMENT '学习计划状态,0-没有计划,1-计划进行中',`learned_sections` int NOT NULL DEFAULT 0 COMMENT '已学习小节数量',`latest_section_id` bigint NULL DEFAULT NULL COMMENT '最近一次学习的小节id',`latest_learn_time` datetime NULL DEFAULT NULL COMMENT '最近一次学习的时间',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`expire_time` datetime NOT NULL COMMENT '过期时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `idx_user_id`(`user_id`, `course_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生课程表' ROW_FORMAT = Dynamic;

当我们创建学习计划时,就是更新learning_lesson表,写入week_freq并更新plan_status为计划进行中即可。

2.3.2.表单实体

表单包含两个字段:

  • courseId

  • weekFreq

前端是以JSON方式提交,我们需要定义一个表单DTO实体。在课前资料中已经提供给大家了:

具体代码:

package com.tianji.learning.domain.dto;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Range;import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;@Data
@ApiModel(description = "学习计划表单实体")
public class LearningPlanDTO {@NotNull@ApiModelProperty("课程表id")@Min(1)private Long courseId;@NotNull@Range(min = 1, max = 50)@ApiModelProperty("每周学习频率")private Integer freq;
}

2.3.3.代码实现

首先,在com.tianji.learning.controller.LearningLessonController中添加一个接口:

package com.tianji.learning.controller;import com.tianji.learning.domain.dto.LearningPlanDTO;
import com.tianji.learning.service.ILearningLessonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;import javax.validation.Valid;/*** <p>* 学生课程表 前端控制器* </p>** @author 虎哥* @since 2022-12-02*/
@RestController
@RequestMapping("/lessons")
@Api(tags = "我的课表相关接口")
@RequiredArgsConstructor
public class LearningLessonController {private final ILearningLessonService lessonService;// 略。。。@ApiOperation("创建学习计划")@PostMapping("/plans")public void createLearningPlans(@Valid @RequestBody LearningPlanDTO planDTO){lessonService.createLearningPlan(planDTO.getCourseId(), planDTO.getFreq());}
}

然后,在com.tianji.learning.service.ILearningLessonService中定义service方法:

package com.tianji.learning.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.tianji.learning.domain.po.LearningLesson;import java.util.List;/*** <p>* 学生课程表 服务类* </p>*/
public interface ILearningLessonService extends IService<LearningLesson> {// ... 略void createLearningPlan(Long courseId, Integer freq);
}

最后,在com.tianji.learning.service.impl.LearningLessonServiceImpl中实现方法:

// ... 略@Override
public void createLearningPlan(Long courseId, Integer freq) {// 1.获取当前登录的用户Long userId = UserContext.getUser();// 2.查询课表中的指定课程有关的数据LearningLesson lesson = queryByUserAndCourseId(userId, courseId);AssertUtils.isNotNull(lesson, "课程信息不存在!");// 3.修改数据LearningLesson l = new LearningLesson();l.setId(lesson.getId());l.setWeekFreq(freq);if(lesson.getPlanStatus() == PlanStatus.NO_PLAN) {l.setPlanStatus(PlanStatus.PLAN_RUNNING);}updateById(l);
}// ... 略

2.4.查询学习计划进度

页面原型如图:

接口回顾:

参数

说明

请求方式

GET

请求路径

/lessons/plans

请求参数

分页参数:PageQuery

返回值

参数名

类型

说明

weekPoints

int

本周学习积分

weekFinished

int

本周已学完小节数量

weekTotalPlan

int

本周计划学习小节数量

list

Array

参数

类型

说明

courseId

Long

课程id

courseName

String

课程名称

weekLearnedSections

int

本周学习的小节数量

weekFreq

int

本周计划学习数量

learnedSections

int

总已学习小节数量

sections

int

总小节数量

latestLearnTime

LocalDateTime

最近一次学习时间

2.4.1.思路分析

要查询的数据分为两部分:

  • 本周计划学习的每个课程的学习进度

  • 本周计划学习的课程总的学习进度

对于本周计划学习的每个课程的学习进度,首先需要查询出学习中的LearningLesson的信息,查询条件包括:

  • 属于当前登录用户

  • 学习计划进行中

查询到的LearningLesson可能有多个,而且查询到的PO数据跟最终的VO相比还有差距:

PO:

VO:

具体来说,PO中缺少了courseName和weekSections两个字段。其中courseName可以通过courseId去课程微服务查询。weekSections只能对学习记录做统计得到。

因此,我们需要搜集查询到的课表中的courseId,查询出对应的课程信息;还需要搜集查询到的课表的id,去learning_record中统计每个课表本周已学习的小节数量。

最终遍历处理每个PO,转换为VO格式。

除了本周每个课程的学习进度以外,我们还要统计本周计划学习的课程总的学习进度。其中的积分数据暂时不管,剩下的两个需要分别对两张表统计:

  • weekTotalPlan:对learning_lesson表统计,查询计划学习的课程的weekFreq字段做累加即可

  • weekFinished:对learning_record表,对已学完的小节记录做count即可

注意

虽然这里是分页查询,但是每个用户购买的课程其实是有限的,为了便于数据统计,建议采用查询全部数据,然后手动逻辑分页的方式。这样在统计全部课程学习进度的时候会方便很多。

2.4.2.实体

VO实体已经在课前资料中给出:

2.4.3.代码实现

首先在tj-learning模块的com.tianji.learning.controller.LearningLessonController中定义controller接口:

@ApiOperation("查询我的学习计划")
@GetMapping("/plans")
public LearningPlanPageVO queryMyPlans(PageQuery query){return lessonService.queryMyPlans(query);
}

然后在com.tianji.learning.service.ILearningLessonService中定义service方法:

LearningPlanPageVO queryMyPlans(PageQuery query);

最后在com.tianji.learning.service.impl.LearningLessonServiceImpl中实现该方法:

版本1:物理分页,分别统计


@Override
public LearningPlanPageVO queryMyPlans(PageQuery query) {LearningPlanPageVO result = new LearningPlanPageVO();// 1.获取当前登录用户Long userId = UserContext.getUser();// 2.获取本周起始时间LocalDate now = LocalDate.now();LocalDateTime begin = DateUtils.getWeekBeginTime(now);LocalDateTime end = DateUtils.getWeekEndTime(now);// 3.查询总的统计数据// 3.1.本周总的已学习小节数量Integer weekFinished = recordMapper.selectCount(new LambdaQueryWrapper<LearningRecord>().eq(LearningRecord::getUserId, userId).eq(LearningRecord::getFinished, true).gt(LearningRecord::getFinishTime, begin).lt(LearningRecord::getFinishTime, end));result.setWeekFinished(weekFinished);// 3.2.本周总的计划学习小节数量Integer weekTotalPlan = getBaseMapper().queryTotalPlan(userId);result.setWeekTotalPlan(weekTotalPlan);// TODO 3.3.本周学习积分// 4.查询分页数据// 4.1.分页查询课表信息以及学习计划信息Page<LearningLesson> p = lambdaQuery().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING).in(LearningLesson::getStatus, LessonStatus.NOT_BEGIN, LessonStatus.LEARNING).page(query.toMpPage("latest_learn_time", false));List<LearningLesson> records = p.getRecords();if (CollUtils.isEmpty(records)) {return result.emptyPage(p);}// 4.2.查询课表对应的课程信息Map<Long, CourseSimpleInfoDTO> cMap = queryCourseSimpleInfoList(records);// 4.3.统计每一个课程本周已学习小节数量List<IdAndNumDTO> list = recordMapper.countLearnedSections(userId, begin, end);Map<Long, Integer> countMap = IdAndNumDTO.toMap(list);// 4.4.组装数据VOList<LearningPlanVO> voList = new ArrayList<>(records.size());for (LearningLesson r : records) {// 4.4.1.拷贝基础属性到voLearningPlanVO vo = BeanUtils.copyBean(r, LearningPlanVO.class);// 4.4.2.填充课程详细信息CourseSimpleInfoDTO cInfo = cMap.get(r.getCourseId());if (cInfo != null) {vo.setCourseName(cInfo.getName());vo.setSections(cInfo.getSectionNum());}// 4.4.3.每个课程的本周已学习小节数量vo.setWeekLearnedSections(countMap.getOrDefault(r.getId(), 0));voList.add(vo);}return result.pageInfo(p.getTotal(), p.getPages(), voList);
}private Map<Long, CourseSimpleInfoDTO> queryCourseSimpleInfoList(List<LearningLesson> records) {// 3.1.获取课程idSet<Long> cIds = records.stream().map(LearningLesson::getCourseId).collect(Collectors.toSet());// 3.2.查询课程信息List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(cIds);if (CollUtils.isEmpty(cInfoList)) {// 课程不存在,无法添加throw new BadRequestException("课程信息不存在!");}// 3.3.把课程集合处理成Map,key是courseId,值是course本身Map<Long, CourseSimpleInfoDTO> cMap = cInfoList.stream().collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c));return cMap;
}

其中需要调用LearningRecordMapper实现对本周每个课程的已学习小节的统计,对应实现如下:

public interface LearningRecordMapper extends BaseMapper<LearningRecord> {List<IdAndNumDTO> countLearnedSections(@Param("userId") Long userId,@Param("begin") LocalDateTime begin,@Param("end") LocalDateTime end);
}

对应的SQL如下:

<select id="countLearnedSections" resultType="com.tianji.api.dto.IdAndNumDTO">SELECT lesson_id AS id, COUNT(1) AS numFROM learning_recordWHERE user_id = #{userId}AND finished = 1AND finish_time &gt; #{begin} AND finish_time &lt; #{end}GROUP BY lesson_id;
</select>

版本2,不分页,stream流统计:

@Override
public LearningPlanPageVO queryMyPlans(PageQuery query) {LearningPlanPageVO result = new LearningPlanPageVO();// 1.获取当前登录用户Long userId = UserContext.getUser();// 2.获取本周起始时间LocalDate now = LocalDate.now();LocalDateTime begin = DateUtils.getWeekBeginTime(now);LocalDateTime end = DateUtils.getWeekEndTime(now);// 3.查询本周计划学习的所有课程,满足三个条件:属于当前用户、有学习计划、学习中List<LearningLesson> lessons = lambdaQuery().eq(LearningLesson::getUserId, userId).eq(LearningLesson::getPlanStatus, PlanStatus.PLAN_RUNNING).in(LearningLesson::getStatus, LessonStatus.NOT_BEGIN, LessonStatus.LEARNING).list();if (CollUtils.isEmpty(lessons)) {return null;}// 4.统计当前用户每个课程的已学习小节数量List<LearningRecord> learnedRecords = recordMapper.selectList(new QueryWrapper<LearningRecord>().lambda().eq(LearningRecord::getUserId, userId).eq(LearningRecord::getFinished, true).gt(LearningRecord::getFinishTime, begin).lt(LearningRecord::getFinishTime, end));Map<Long, Long> countMap = learnedRecords.stream().collect(Collectors.groupingBy(LearningRecord::getLessonId, Collectors.counting()));// 5.查询总的统计数据// 5.1.本周总的已学习小节数量int weekFinished = learnedRecords.size();result.setWeekFinished(weekFinished);// 5.2.本周总的计划学习小节数量int weekTotalPlan = lessons.stream().mapToInt(LearningLesson::getWeekFreq).sum();result.setWeekTotalPlan(weekTotalPlan);// TODO 5.3.本周学习积分// 6.处理分页数据// 6.1.分页查询课表信息以及学习计划信息Page<LearningLesson> p = new Page<>(query.getPageNo(), query.getPageSize(), lessons.size());List<LearningLesson> records = CollUtils.sub(lessons, query.from(), query.from() + query.getPageSize());if (CollUtils.isEmpty(records)) {return result;}// 6.2.查询课表对应的课程信息Map<Long, CourseSimpleInfoDTO> cMap = queryCourseInfo(records);// 6.3.组装数据VOList<LearningPlanVO> voList = new ArrayList<>(records.size());for (LearningLesson r : records) {// 6.4.1.拷贝基础属性到voLearningPlanVO vo = BeanUtils.copyBean(r, LearningPlanVO.class);// 6.4.2.填充课程详细信息CourseSimpleInfoDTO cInfo = cMap.get(r.getCourseId());if (cInfo != null) {vo.setCourseName(cInfo.getName());vo.setSections(cInfo.getSectionNum());}// 6.4.3.每个课程的本周已学习小节数量vo.setWeekLearnedSections(countMap.getOrDefault(r.getId(), 0L).intValue());voList.add(vo);}return result.pageInfo(p.getTotal(), p.getPages(), voList);
}

3.练习

3.1.课程过期

编写一个SpringTask定时任务,定期检查learning_lesson表中的课程是否过期,如果过期则将课程状态修改为已过期。

3.2.方案思考

思考题:思考一下目前提交学习记录功能可能存在哪些问题?有哪些可以改进的方向?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/908364.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

软件项目管理(3) 软件项目任务分解

一、相关概念 1.任务分解的方法和步骤 &#xff08;1&#xff09;方法 模板参照方法&#xff1a;参照有标准或半标准的任分解结构图类比方法&#xff1a;任务分解结构图经常被重复使用&#xff0c;具有相似性自顶向下方法&#xff1a;一般->特殊&#xff0c;演绎推理从大…

Vite 双引擎架构 —— Esbuild 概念篇

Vite 底层采用 双引擎架构&#xff0c;核心构建引擎是 Esbuild 和 Rollup&#xff0c;二者在开发和生产环境中分工协作&#xff0c;共同实现高性能构建。不可否认&#xff0c;作为 Vite 的双引擎之一&#xff0c;Esbuild 在很多关键的构建阶段(如依赖预编译、TS 语法转译、代码…

leetcode hot100 链表(二)

书接上回&#xff1a; leetcode hot100 链表&#xff08;一&#xff09;-CSDN博客 8.删除链表的倒数第N个结点 class Solution { public:ListNode* removeNthFromEnd(ListNode* head, int n) {ListNode* currhead;int len0;while(curr){currcurr->next;len;}int poslen-n…

Compose Multiplatform 实现自定义的系统托盘,解决托盘乱码问题

Compose Multiplatform是 JetBrains 开发的声明式 UI 框架&#xff0c;可让您为 Android、iOS、桌面和 Web 开发共享 UI。将 Compose Multiplatform 集成到您的 Kotlin Multiplatform 项目中&#xff0c;即可更快地交付您的应用和功能&#xff0c;而无需维护多个 UI 实现。 在…

C++11 Move Constructors and Move Assignment Operators 从入门到精通

文章目录 一、引言二、基本概念2.1 右值引用&#xff08;Rvalue References&#xff09;2.2 移动语义&#xff08;Move Semantics&#xff09; 三、移动构造函数&#xff08;Move Constructors&#xff09;3.1 定义和语法3.2 示例代码3.3 使用场景 四、移动赋值运算符&#xff…

Linux配置yum 时间同步服务 关闭防火墙 关闭ESlinux

1、配置yum 1.1、Could not resolve host: mirrorlist.centos.org; 未知的错误 https://blog.csdn.net/fansfi/article/details/146369946?fromshareblogdetail&sharetypeblogdetail&sharerId146369946&sharereferPC&sharesourceRockandrollman&sharefr…

使用 uv 工具快速部署并管理 vLLM 推理环境

uv&#xff1a;现代 Python 项目管理的高效助手 uv&#xff1a;Rust 驱动的 Python 包管理新时代 在部署大语言模型&#xff08;LLM&#xff09;推理服务时&#xff0c;vLLM 是一个备受关注的方案&#xff0c;具备高吞吐、低延迟和对 OpenAI API 的良好兼容性。为了提高部署效…

基于sqlite的任务锁(支持多进程/多线程)

前言 介绍 任务锁,在多进程服务间控制耗时任务的锁,确保相同id的耗时任务同时只有一个在执行 依赖 SqliteOp,参考这篇文章 https://blog.csdn.net/weixin_43721000/article/details/137019125 实现方式 utils/taskLock.py import timefrom utils.SqliteOp import Sqli…

html表格转换为markdown

文章目录 工具功能亮点1.核心实现解析1. 剪贴板交互2. HTML检测与提取3. 转换规则设计 2. 完整代码 在日常工作中&#xff0c;我们经常遇到需要将网页表格快速转换为Markdown格式的场景。无论是文档编写、知识整理还是数据迁移&#xff0c;手动转换既耗时又容易出错。本文将介绍…

IDEA 中 Undo Commit,Revert Commit,Drop Commit区别

一、Undo Commit 适用情况&#xff1a;代码修改完了&#xff0c;已经Commit了&#xff0c;但是还未push&#xff0c;然后发现还有地方需要修改&#xff0c;但是又不想增加一个新的Commit记录。这时可以进行Undo Commit&#xff0c;修改后再重新Commit。如果已经进行了Push&…

【Linux】Linux 进程间通讯-管道

参考博客&#xff1a;https://blog.csdn.net/sjsjnsjnn/article/details/125864580 一、进程间通讯介绍 1.1 进程间通讯的概念 进程通信&#xff08;Interprocess communication&#xff09;&#xff0c;简称&#xff1a;IPC 本来进程之间是相互独立的。但是由于不同的进程…

深度剖析 DeepSeek 开源模型部署与应用:策略、权衡与未来走向

在人工智能技术呈指数级发展的当下&#xff0c;大模型已然成为推动各行业变革的核心驱动力。DeepSeek 开源模型以其卓越的性能和灵活的开源特性&#xff0c;吸引了众多企业与开发者的目光。如何高效且合理地部署与运用 DeepSeek 模型&#xff0c;成为释放其巨大潜力的关键所在&…

第34次CCF-CSP认证真题解析(目标300分做法)

第34次CCF-CSP认证 矩阵重塑&#xff08;其一&#xff09;AC代码及解析矩阵重塑&#xff08;其二&#xff09;AC代码及解析货物调度AC代码及解析 矩阵重塑&#xff08;其一&#xff09; 输入输出及样例&#xff1a; AC代码及解析 1.线性化原矩阵 &#xff1a;由于cin的特性我们…

智能制造数字孪生全要素交付一张网:智造中枢,孪生领航,共建智造生态共同体

在制造业转型升级的浪潮中&#xff0c;数字孪生技术正成为推动行业变革的核心引擎。从特斯拉通过数字孪生体实现车辆全生命周期优化&#xff0c;到海尔卡奥斯工业互联网平台赋能千行百业&#xff0c;数字孪生技术已从概念验证走向规模化落地。通过构建覆盖全国的交付网络&#…

【技术】跨设备链路聚合的技术——M-LAG

原创&#xff1a;厦门微思网络 M-LAG&#xff08;Multichassis Link Aggregation Group&#xff09;提供一种跨设备链路聚合的技术。M-LAG通过将两台接入交换机以同一个状态和用户侧设备或服务器进行跨设备的链路聚合&#xff0c;把链路的可靠性从单板级提升到设备级。同时&…

AI健康小屋+微高压氧舱:科技如何重构我们的健康防线?

目前&#xff0c;随着科技和社会的不断发展&#xff0c;人们的生活水平和方式有了翻天覆地的变化。 从吃饱穿暖到吃好喝好再到健康生活&#xff0c;观念也在逐渐发生改变。 尤其是在21世纪&#xff0c;大家对健康越来越重视&#xff0c;这就不得不提AI健康小屋和氧舱。 一、A…

Python训练营---Day44

DAY 44 预训练模型 知识点回顾&#xff1a; 预训练的概念常见的分类预训练模型图像预训练模型的发展史预训练的策略预训练代码实战&#xff1a;resnet18 作业&#xff1a; 尝试在cifar10对比如下其他的预训练模型&#xff0c;观察差异&#xff0c;尽可能和他人选择的不同尝试通…

1.文件操作相关的库

一、filesystem(C17) 和 fstream 1.std::filesystem::path - cppreference.cn - C参考手册 std::filesystem::path 表示路径 构造函数&#xff1a; path( string_type&& source, format fmt auto_format ); 可以用string进行构造&#xff0c;也可以用string进行隐式类…

【 java 集合知识 第二篇 】

目录 1.Map集合 1.1.快速遍历Map 1.2.HashMap实现原理 1.3.HashMap的扩容机制 1.4.HashMap在多线程下的问题 1.5.解决哈希冲突的方法 1.6.HashMap的put过程 1.7.HashMap的key使用什么类型 1.8.HashMapkey可以为null的原因 1.9.HashMap为什么不采用平衡二叉树 1.10.Hash…

【Dify 知识库 API】“根据文本更新文档” 真的是差异更新吗?一文讲透真实机制!

在使用 Dify 知识库 API 过程中,很多开发者在调用 /datasets/{dataset_id}/document/update-by-text 接口时,常常会产生一个疑问: 👉 这个接口到底是 “智能差异更新” 还是 “纯覆盖更新”? 网上的资料并不多,很多人根据接口名误以为是增量更新。今天我结合官方源码 …