天機學堂(學習計劃和進度)

經過前面的努力,我們已經完成了《我的課程表》相關的功能的基礎部分,不過還有功能實現的并不完善。還記得昨天給大家的練習題嗎?《查詢我正在學習的課程》,在原型圖中有這樣的一個需求:

我們需要在查詢結果中返回已學習課時數、正在學習的章節名稱。雖然我們在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
繁體地址,請注明出處:http://hk.pswp.cn/news/908364.shtml
英文地址,請注明出處:http://en.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 接口時,常常會產生一個疑問: ?? 這個接口到底是 “智能差異更新” 還是 “純覆蓋更新”? 網上的資料并不多,很多人根據接口名誤以為是增量更新。今天我結合官方源碼 …