serv\web-学习历史修改

This commit is contained in:
2025-10-27 13:42:34 +08:00
parent 74880b429e
commit e50de4a277
31 changed files with 3997 additions and 64 deletions

View File

@@ -116,3 +116,78 @@ CREATE TABLE `tb_learning_statistics` (
KEY `idx_date` (`stat_date`) KEY `idx_date` (`stat_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学习统计表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学习统计表';
-- 学习观看历史表(记录每次学习行为)
DROP TABLE IF EXISTS `tb_learning_history`;
CREATE TABLE `tb_learning_history` (
`id` VARCHAR(50) NOT NULL COMMENT 'ID',
`user_id` VARCHAR(50) NOT NULL COMMENT '用户ID',
`history_id` VARCHAR(50) DEFAULT NULL COMMENT '学习历史ID',
-- 学习对象
`resource_type` INT(4) NOT NULL COMMENT '资源类型1资源/新闻 2课程 3章节 4节点',
`resource_id` VARCHAR(50) DEFAULT NULL COMMENT '资源ID',
`course_id` VARCHAR(50) DEFAULT NULL COMMENT '课程ID',
`chapter_id` VARCHAR(50) DEFAULT NULL COMMENT '章节ID',
`node_id` VARCHAR(50) DEFAULT NULL COMMENT '节点ID',
`task_id` VARCHAR(50) DEFAULT NULL COMMENT '关联任务ID',
-- 学习时间
`start_time` TIMESTAMP NOT NULL COMMENT '开始学习时间',
`end_time` TIMESTAMP NULL DEFAULT NULL COMMENT '结束学习时间',
`duration` INT(11) DEFAULT 0 COMMENT '本次学习时长(秒)',
-- 学习进度
`start_progress` DECIMAL(5,2) DEFAULT 0.00 COMMENT '开始进度0-100',
`end_progress` DECIMAL(5,2) DEFAULT 0.00 COMMENT '结束进度0-100',
-- 设备信息(可选)
`device_type` VARCHAR(20) DEFAULT NULL COMMENT '设备类型web/mobile/app',
`ip_address` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
`creator` VARCHAR(50) DEFAULT NULL COMMENT '创建者',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`delete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
KEY `idx_user_time` (`user_id`, `start_time`),
KEY `idx_user_resource` (`user_id`, `resource_type`, `resource_id`),
KEY `idx_course` (`course_id`),
KEY `idx_task` (`task_id`),
KEY `idx_history` (`history_id`),
KEY `idx_start_time` (`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学习观看历史表';
-- 学习统计明细表(按天按资源统计)
DROP TABLE IF EXISTS `tb_learning_statistics_detail`;
CREATE TABLE `tb_learning_statistics_detail` (
`id` VARCHAR(50) NOT NULL COMMENT '统计ID',
`user_id` VARCHAR(50) NOT NULL COMMENT '用户ID',
`stat_date` DATE NOT NULL COMMENT '统计日期',
-- 学习对象
`resource_type` INT(4) NOT NULL COMMENT '资源类型1资源/新闻 2课程 3章节',
`resource_id` VARCHAR(50) DEFAULT NULL COMMENT '资源ID',
`course_id` VARCHAR(50) DEFAULT NULL COMMENT '课程ID',
`chapter_id` VARCHAR(50) DEFAULT NULL COMMENT '章节ID',
-- 统计数据
`total_duration` INT(11) DEFAULT 0 COMMENT '学习时长(秒)',
`learn_count` INT(11) DEFAULT 0 COMMENT '学习次数',
`is_complete` TINYINT(1) DEFAULT 0 COMMENT '是否完成',
`complete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '完成时间',
`creator` VARCHAR(50) DEFAULT NULL COMMENT '创建者',
`updater` VARCHAR(50) DEFAULT NULL COMMENT '更新者',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_date_resource` (`user_id`, `stat_date`, `resource_type`, `resource_id`, `course_id`, `chapter_id`),
KEY `idx_user_date` (`user_id`, `stat_date`),
KEY `idx_date` (`stat_date`),
KEY `idx_resource` (`resource_type`, `resource_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='学习统计明细表';

View File

@@ -0,0 +1,59 @@
package org.xyzh.api.study.history;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.study.TbLearningHistory;
import java.util.List;
/**
* @description 学习历史API接口供其他模块调用
* @filename LearningHistoryAPI.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
public interface LearningHistoryAPI {
/**
* @description 记录学习历史
* @param learningHistory 学习历史
* @return ResultDomain<TbLearningHistory> 记录结果
* @author yslg
* @since 2025-10-27
*/
ResultDomain<TbLearningHistory> recordLearningHistory(TbLearningHistory learningHistory);
/**
* @description 批量记录学习历史
* @param historyList 学习历史列表
* @return ResultDomain<Boolean> 记录结果
* @author yslg
* @since 2025-10-27
*/
ResultDomain<Boolean> batchRecordLearningHistory(List<TbLearningHistory> historyList);
/**
* @description 简化记录方法 - 观看新闻/资源
* @param userId 用户ID
* @param resourceId 资源ID
* @param duration 学习时长(秒)
* @return ResultDomain<TbLearningHistory> 记录结果
* @author yslg
* @since 2025-10-27
*/
ResultDomain<TbLearningHistory> recordResourceView(String userId, String resourceId, Integer duration);
/**
* @description 简化记录方法 - 学习课程
* @param userId 用户ID
* @param courseId 课程ID
* @param chapterId 章节ID可选
* @param nodeId 节点ID可选
* @param duration 学习时长(秒)
* @return ResultDomain<TbLearningHistory> 记录结果
* @author yslg
* @since 2025-10-27
*/
ResultDomain<TbLearningHistory> recordCourseLearn(String userId, String courseId, String chapterId, String nodeId, Integer duration);
}

View File

@@ -0,0 +1,250 @@
package org.xyzh.common.dto.study;
import org.xyzh.common.dto.BaseDTO;
import java.math.BigDecimal;
import java.util.Date;
/**
* @description 学习观看历史表
* @filename TbLearningHistory.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
public class TbLearningHistory extends BaseDTO {
private static final long serialVersionUID = 1L;
/**
* @description 用户ID
*/
private String userID;
/**
* @description 学习会话ID
*/
private String historyID;
/**
* @description 资源类型1资源/新闻 2课程 3章节 4节点
*/
private Integer resourceType;
/**
* @description 资源ID
*/
private String resourceID;
/**
* @description 课程ID
*/
private String courseID;
/**
* @description 章节ID
*/
private String chapterID;
/**
* @description 节点ID
*/
private String nodeID;
/**
* @description 关联任务ID
*/
private String taskID;
/**
* @description 开始学习时间
*/
private Date startTime;
/**
* @description 结束学习时间
*/
private Date endTime;
/**
* @description 本次学习时长(秒)
*/
private Integer duration;
/**
* @description 开始进度0-100
*/
private BigDecimal startProgress;
/**
* @description 结束进度0-100
*/
private BigDecimal endProgress;
/**
* @description 设备类型web/mobile/app
*/
private String deviceType;
/**
* @description IP地址
*/
private String ipAddress;
/**
* @description 创建者
*/
private String creator;
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getHistoryID() {
return historyID;
}
public void setHistoryID(String historyID) {
this.historyID = historyID;
}
public Integer getResourceType() {
return resourceType;
}
public void setResourceType(Integer resourceType) {
this.resourceType = resourceType;
}
public String getResourceID() {
return resourceID;
}
public void setResourceID(String resourceID) {
this.resourceID = resourceID;
}
public String getCourseID() {
return courseID;
}
public void setCourseID(String courseID) {
this.courseID = courseID;
}
public String getChapterID() {
return chapterID;
}
public void setChapterID(String chapterID) {
this.chapterID = chapterID;
}
public String getNodeID() {
return nodeID;
}
public void setNodeID(String nodeID) {
this.nodeID = nodeID;
}
public String getTaskID() {
return taskID;
}
public void setTaskID(String taskID) {
this.taskID = taskID;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public Integer getDuration() {
return duration;
}
public void setDuration(Integer duration) {
this.duration = duration;
}
public BigDecimal getStartProgress() {
return startProgress;
}
public void setStartProgress(BigDecimal startProgress) {
this.startProgress = startProgress;
}
public BigDecimal getEndProgress() {
return endProgress;
}
public void setEndProgress(BigDecimal endProgress) {
this.endProgress = endProgress;
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
@Override
public String toString() {
return "TbLearningHistory{" +
"id=" + getID() +
", userID='" + userID + '\'' +
", historyID='" + historyID + '\'' +
", resourceType=" + resourceType +
", resourceID='" + resourceID + '\'' +
", courseID='" + courseID + '\'' +
", chapterID='" + chapterID + '\'' +
", nodeID='" + nodeID + '\'' +
", taskID='" + taskID + '\'' +
", startTime=" + startTime +
", endTime=" + endTime +
", duration=" + duration +
", startProgress=" + startProgress +
", endProgress=" + endProgress +
", deviceType='" + deviceType + '\'' +
", ipAddress='" + ipAddress + '\'' +
", creator='" + creator + '\'' +
", createTime=" + getCreateTime() +
'}';
}
}

View File

@@ -0,0 +1,194 @@
package org.xyzh.common.dto.study;
import org.xyzh.common.dto.BaseDTO;
import java.util.Date;
/**
* @description 学习统计明细表
* @filename TbLearningStatisticsDetail.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
public class TbLearningStatisticsDetail extends BaseDTO {
private static final long serialVersionUID = 1L;
/**
* @description 用户ID
*/
private String userID;
/**
* @description 统计日期
*/
private Date statDate;
/**
* @description 资源类型1资源/新闻 2课程 3章节
*/
private Integer resourceType;
/**
* @description 资源ID
*/
private String resourceID;
/**
* @description 课程ID
*/
private String courseID;
/**
* @description 章节ID
*/
private String chapterID;
/**
* @description 学习时长(秒)
*/
private Integer totalDuration;
/**
* @description 学习次数
*/
private Integer learnCount;
/**
* @description 是否完成
*/
private Boolean isComplete;
/**
* @description 完成时间
*/
private Date completeTime;
/**
* @description 创建者
*/
private String creator;
/**
* @description 更新者
*/
private String updater;
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public Date getStatDate() {
return statDate;
}
public void setStatDate(Date statDate) {
this.statDate = statDate;
}
public Integer getResourceType() {
return resourceType;
}
public void setResourceType(Integer resourceType) {
this.resourceType = resourceType;
}
public String getResourceID() {
return resourceID;
}
public void setResourceID(String resourceID) {
this.resourceID = resourceID;
}
public String getCourseID() {
return courseID;
}
public void setCourseID(String courseID) {
this.courseID = courseID;
}
public String getChapterID() {
return chapterID;
}
public void setChapterID(String chapterID) {
this.chapterID = chapterID;
}
public Integer getTotalDuration() {
return totalDuration;
}
public void setTotalDuration(Integer totalDuration) {
this.totalDuration = totalDuration;
}
public Integer getLearnCount() {
return learnCount;
}
public void setLearnCount(Integer learnCount) {
this.learnCount = learnCount;
}
public Boolean getIsComplete() {
return isComplete;
}
public void setIsComplete(Boolean isComplete) {
this.isComplete = isComplete;
}
public Date getCompleteTime() {
return completeTime;
}
public void setCompleteTime(Date completeTime) {
this.completeTime = completeTime;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public String getUpdater() {
return updater;
}
public void setUpdater(String updater) {
this.updater = updater;
}
@Override
public String toString() {
return "TbLearningStatisticsDetail{" +
"id=" + getID() +
", userID='" + userID + '\'' +
", statDate=" + statDate +
", resourceType=" + resourceType +
", resourceID='" + resourceID + '\'' +
", courseID='" + courseID + '\'' +
", chapterID='" + chapterID + '\'' +
", totalDuration=" + totalDuration +
", learnCount=" + learnCount +
", isComplete=" + isComplete +
", completeTime=" + completeTime +
", creator='" + creator + '\'' +
", updater='" + updater + '\'' +
", createTime=" + getCreateTime() +
", updateTime=" + getUpdateTime() +
'}';
}
}

View File

@@ -0,0 +1,347 @@
package org.xyzh.common.vo;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* @description 学习历史响应VO
* @filename LearningHistoryVO.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
public class LearningHistoryVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* @description 历史记录ID
*/
private String id;
/**
* @description 用户ID
*/
private String userID;
/**
* @description 用户名称(关联查询)
*/
private String userName;
/**
* @description 学习会话ID
*/
private String sessionID;
/**
* @description 资源类型1资源/新闻 2课程 3章节 4节点
*/
private Integer resourceType;
/**
* @description 资源类型名称
*/
private String resourceTypeName;
/**
* @description 资源ID
*/
private String resourceID;
/**
* @description 资源标题(关联查询)
*/
private String resourceTitle;
/**
* @description 课程ID
*/
private String courseID;
/**
* @description 课程名称(关联查询)
*/
private String courseName;
/**
* @description 章节ID
*/
private String chapterID;
/**
* @description 章节名称(关联查询)
*/
private String chapterName;
/**
* @description 节点ID
*/
private String nodeID;
/**
* @description 节点名称(关联查询)
*/
private String nodeName;
/**
* @description 关联任务ID
*/
private String taskID;
/**
* @description 任务名称(关联查询)
*/
private String taskName;
/**
* @description 开始学习时间
*/
private Date startTime;
/**
* @description 结束学习时间
*/
private Date endTime;
/**
* @description 本次学习时长(秒)
*/
private Integer duration;
/**
* @description 学习时长格式化后1小时20分钟
*/
private String durationFormatted;
/**
* @description 开始进度0-100
*/
private BigDecimal startProgress;
/**
* @description 结束进度0-100
*/
private BigDecimal endProgress;
/**
* @description 设备类型
*/
private String deviceType;
/**
* @description 创建时间
*/
private Date createTime;
// Getters and Setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSessionID() {
return sessionID;
}
public void setSessionID(String sessionID) {
this.sessionID = sessionID;
}
public Integer getResourceType() {
return resourceType;
}
public void setResourceType(Integer resourceType) {
this.resourceType = resourceType;
}
public String getResourceTypeName() {
return resourceTypeName;
}
public void setResourceTypeName(String resourceTypeName) {
this.resourceTypeName = resourceTypeName;
}
public String getResourceID() {
return resourceID;
}
public void setResourceID(String resourceID) {
this.resourceID = resourceID;
}
public String getResourceTitle() {
return resourceTitle;
}
public void setResourceTitle(String resourceTitle) {
this.resourceTitle = resourceTitle;
}
public String getCourseID() {
return courseID;
}
public void setCourseID(String courseID) {
this.courseID = courseID;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public String getChapterID() {
return chapterID;
}
public void setChapterID(String chapterID) {
this.chapterID = chapterID;
}
public String getChapterName() {
return chapterName;
}
public void setChapterName(String chapterName) {
this.chapterName = chapterName;
}
public String getNodeID() {
return nodeID;
}
public void setNodeID(String nodeID) {
this.nodeID = nodeID;
}
public String getNodeName() {
return nodeName;
}
public void setNodeName(String nodeName) {
this.nodeName = nodeName;
}
public String getTaskID() {
return taskID;
}
public void setTaskID(String taskID) {
this.taskID = taskID;
}
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public Integer getDuration() {
return duration;
}
public void setDuration(Integer duration) {
this.duration = duration;
}
public String getDurationFormatted() {
return durationFormatted;
}
public void setDurationFormatted(String durationFormatted) {
this.durationFormatted = durationFormatted;
}
public BigDecimal getStartProgress() {
return startProgress;
}
public void setStartProgress(BigDecimal startProgress) {
this.startProgress = startProgress;
}
public BigDecimal getEndProgress() {
return endProgress;
}
public void setEndProgress(BigDecimal endProgress) {
this.endProgress = endProgress;
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "LearningHistoryVO{" +
"id='" + id + '\'' +
", userID='" + userID + '\'' +
", userName='" + userName + '\'' +
", resourceType=" + resourceType +
", resourceTypeName='" + resourceTypeName + '\'' +
", resourceTitle='" + resourceTitle + '\'' +
", duration=" + duration +
", durationFormatted='" + durationFormatted + '\'' +
", startTime=" + startTime +
", endTime=" + endTime +
'}';
}
}

View File

@@ -0,0 +1,213 @@
package org.xyzh.common.vo;
import java.io.Serializable;
import java.util.Date;
/**
* @description 学习统计明细VO
* @filename LearningStatisticsDetailVO.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
public class LearningStatisticsDetailVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* @description 统计日期
*/
private Date statDate;
/**
* @description 资源类型1资源/新闻 2课程 3章节
*/
private Integer resourceType;
/**
* @description 资源类型名称
*/
private String resourceTypeName;
/**
* @description 资源ID
*/
private String resourceID;
/**
* @description 资源标题
*/
private String resourceTitle;
/**
* @description 课程ID
*/
private String courseID;
/**
* @description 课程名称
*/
private String courseName;
/**
* @description 章节ID
*/
private String chapterID;
/**
* @description 章节名称
*/
private String chapterName;
/**
* @description 学习时长(秒)
*/
private Integer totalDuration;
/**
* @description 学习时长(格式化)
*/
private String totalDurationFormatted;
/**
* @description 学习次数
*/
private Integer learnCount;
/**
* @description 是否完成
*/
private Boolean isComplete;
/**
* @description 完成时间
*/
private Date completeTime;
public Date getStatDate() {
return statDate;
}
public void setStatDate(Date statDate) {
this.statDate = statDate;
}
public Integer getResourceType() {
return resourceType;
}
public void setResourceType(Integer resourceType) {
this.resourceType = resourceType;
}
public String getResourceTypeName() {
return resourceTypeName;
}
public void setResourceTypeName(String resourceTypeName) {
this.resourceTypeName = resourceTypeName;
}
public String getResourceID() {
return resourceID;
}
public void setResourceID(String resourceID) {
this.resourceID = resourceID;
}
public String getResourceTitle() {
return resourceTitle;
}
public void setResourceTitle(String resourceTitle) {
this.resourceTitle = resourceTitle;
}
public String getCourseID() {
return courseID;
}
public void setCourseID(String courseID) {
this.courseID = courseID;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public String getChapterID() {
return chapterID;
}
public void setChapterID(String chapterID) {
this.chapterID = chapterID;
}
public String getChapterName() {
return chapterName;
}
public void setChapterName(String chapterName) {
this.chapterName = chapterName;
}
public Integer getTotalDuration() {
return totalDuration;
}
public void setTotalDuration(Integer totalDuration) {
this.totalDuration = totalDuration;
}
public String getTotalDurationFormatted() {
return totalDurationFormatted;
}
public void setTotalDurationFormatted(String totalDurationFormatted) {
this.totalDurationFormatted = totalDurationFormatted;
}
public Integer getLearnCount() {
return learnCount;
}
public void setLearnCount(Integer learnCount) {
this.learnCount = learnCount;
}
public Boolean getIsComplete() {
return isComplete;
}
public void setIsComplete(Boolean isComplete) {
this.isComplete = isComplete;
}
public Date getCompleteTime() {
return completeTime;
}
public void setCompleteTime(Date completeTime) {
this.completeTime = completeTime;
}
@Override
public String toString() {
return "LearningStatisticsDetailVO{" +
"statDate=" + statDate +
", resourceType=" + resourceType +
", resourceTypeName='" + resourceTypeName + '\'' +
", resourceTitle='" + resourceTitle + '\'' +
", totalDuration=" + totalDuration +
", totalDurationFormatted='" + totalDurationFormatted + '\'' +
", learnCount=" + learnCount +
", isComplete=" + isComplete +
'}';
}
}

View File

@@ -0,0 +1,176 @@
package org.xyzh.common.vo;
import java.io.Serializable;
import java.util.List;
/**
* @description 学习统计响应VO
* @filename LearningStatisticsVO.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
public class LearningStatisticsVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* @description 用户ID
*/
private String userID;
/**
* @description 统计周期day/week/month
*/
private String period;
/**
* @description 总学习时长(秒)
*/
private Integer totalDuration;
/**
* @description 总学习时长(格式化)
*/
private String totalDurationFormatted;
/**
* @description 学习天数
*/
private Integer learnDays;
/**
* @description 学习资源数量
*/
private Integer resourceCount;
/**
* @description 学习课程数量
*/
private Integer courseCount;
/**
* @description 学习次数
*/
private Integer learnCount;
/**
* @description 完成数量
*/
private Integer completeCount;
/**
* @description 平均每天学习时长(秒)
*/
private Integer avgDailyDuration;
/**
* @description 学习明细列表
*/
private List<LearningStatisticsDetailVO> details;
public String getUserID() {
return userID;
}
public void setUserID(String userID) {
this.userID = userID;
}
public String getPeriod() {
return period;
}
public void setPeriod(String period) {
this.period = period;
}
public Integer getTotalDuration() {
return totalDuration;
}
public void setTotalDuration(Integer totalDuration) {
this.totalDuration = totalDuration;
}
public String getTotalDurationFormatted() {
return totalDurationFormatted;
}
public void setTotalDurationFormatted(String totalDurationFormatted) {
this.totalDurationFormatted = totalDurationFormatted;
}
public Integer getLearnDays() {
return learnDays;
}
public void setLearnDays(Integer learnDays) {
this.learnDays = learnDays;
}
public Integer getResourceCount() {
return resourceCount;
}
public void setResourceCount(Integer resourceCount) {
this.resourceCount = resourceCount;
}
public Integer getCourseCount() {
return courseCount;
}
public void setCourseCount(Integer courseCount) {
this.courseCount = courseCount;
}
public Integer getLearnCount() {
return learnCount;
}
public void setLearnCount(Integer learnCount) {
this.learnCount = learnCount;
}
public Integer getCompleteCount() {
return completeCount;
}
public void setCompleteCount(Integer completeCount) {
this.completeCount = completeCount;
}
public Integer getAvgDailyDuration() {
return avgDailyDuration;
}
public void setAvgDailyDuration(Integer avgDailyDuration) {
this.avgDailyDuration = avgDailyDuration;
}
public List<LearningStatisticsDetailVO> getDetails() {
return details;
}
public void setDetails(List<LearningStatisticsDetailVO> details) {
this.details = details;
}
@Override
public String toString() {
return "LearningStatisticsVO{" +
"userID='" + userID + '\'' +
", period='" + period + '\'' +
", totalDuration=" + totalDuration +
", totalDurationFormatted='" + totalDurationFormatted + '\'' +
", learnDays=" + learnDays +
", resourceCount=" + resourceCount +
", courseCount=" + courseCount +
", learnCount=" + learnCount +
", completeCount=" + completeCount +
", avgDailyDuration=" + avgDailyDuration +
'}';
}
}

View File

@@ -73,7 +73,7 @@ public class CrontabController {
* @since 2025-10-25 * @since 2025-10-25
*/ */
@GetMapping("/task/{taskId}") @GetMapping("/task/{taskId}")
public ResultDomain<TbCrontabTask> getTaskById(@PathVariable String taskId) { public ResultDomain<TbCrontabTask> getTaskById(@PathVariable(value = "taskId") String taskId) {
return crontabService.getTaskById(taskId); return crontabService.getTaskById(taskId);
} }
@@ -111,7 +111,7 @@ public class CrontabController {
* @since 2025-10-25 * @since 2025-10-25
*/ */
@PostMapping("/task/start/{taskId}") @PostMapping("/task/start/{taskId}")
public ResultDomain<TbCrontabTask> startTask(@PathVariable String taskId) { public ResultDomain<TbCrontabTask> startTask(@PathVariable(value = "taskId") String taskId) {
return crontabService.startTask(taskId); return crontabService.startTask(taskId);
} }
@@ -123,7 +123,7 @@ public class CrontabController {
* @since 2025-10-25 * @since 2025-10-25
*/ */
@PostMapping("/task/pause/{taskId}") @PostMapping("/task/pause/{taskId}")
public ResultDomain<TbCrontabTask> pauseTask(@PathVariable String taskId) { public ResultDomain<TbCrontabTask> pauseTask(@PathVariable(value = "taskId") String taskId) {
return crontabService.pauseTask(taskId); return crontabService.pauseTask(taskId);
} }
@@ -135,7 +135,7 @@ public class CrontabController {
* @since 2025-10-25 * @since 2025-10-25
*/ */
@PostMapping("/task/execute/{taskId}") @PostMapping("/task/execute/{taskId}")
public ResultDomain<TbCrontabTask> executeTaskOnce(@PathVariable String taskId) { public ResultDomain<TbCrontabTask> executeTaskOnce(@PathVariable(value = "taskId") String taskId) {
return crontabService.executeTaskOnce(taskId); return crontabService.executeTaskOnce(taskId);
} }
@@ -161,7 +161,7 @@ public class CrontabController {
* @since 2025-10-25 * @since 2025-10-25
*/ */
@GetMapping("/log/task/{taskId}") @GetMapping("/log/task/{taskId}")
public ResultDomain<TbCrontabLog> getLogsByTaskId(@PathVariable String taskId) { public ResultDomain<TbCrontabLog> getLogsByTaskId(@PathVariable(value = "taskId") String taskId) {
return crontabService.getLogsByTaskId(taskId); return crontabService.getLogsByTaskId(taskId);
} }
@@ -199,7 +199,7 @@ public class CrontabController {
* @since 2025-10-25 * @since 2025-10-25
*/ */
@GetMapping("/log/{logId}") @GetMapping("/log/{logId}")
public ResultDomain<TbCrontabLog> getLogById(@PathVariable String logId) { public ResultDomain<TbCrontabLog> getLogById(@PathVariable(value = "logId") String logId) {
return crontabService.getLogById(logId); return crontabService.getLogById(logId);
} }
@@ -211,7 +211,7 @@ public class CrontabController {
* @since 2025-10-25 * @since 2025-10-25
*/ */
@DeleteMapping("/log/clean/{days}") @DeleteMapping("/log/clean/{days}")
public ResultDomain<Integer> cleanLogs(@PathVariable Integer days) { public ResultDomain<Integer> cleanLogs(@PathVariable(value = "days") Integer days) {
return crontabService.cleanLogs(days); return crontabService.cleanLogs(days);
} }

View File

@@ -100,5 +100,14 @@ public interface CrontabTaskMapper extends BaseMapper<TbCrontabTask> {
* @since 2025-10-25 * @since 2025-10-25
*/ */
TbCrontabTask selectTaskByBeanAndMethod(@Param("beanName") String beanName, @Param("methodName") String methodName); TbCrontabTask selectTaskByBeanAndMethod(@Param("beanName") String beanName, @Param("methodName") String methodName);
/**
* @description 查询任务总数
* @param filter 过滤条件
* @return int 任务总数
* @author yslg
* @since 2025-10-25
*/
int countSelectTask(@Param("filter") TbCrontabTask filter);
} }

View File

@@ -8,6 +8,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.xyzh.api.crontab.CrontabService; import org.xyzh.api.crontab.CrontabService;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.crontab.TbCrontabTask; import org.xyzh.common.dto.crontab.TbCrontabTask;
import org.xyzh.common.dto.crontab.TbCrontabLog; import org.xyzh.common.dto.crontab.TbCrontabLog;
@@ -215,7 +216,14 @@ public class CrontabServiceImpl implements CrontabService {
} }
List<TbCrontabTask> list = taskMapper.selectTaskPage(filter, pageParam); List<TbCrontabTask> list = taskMapper.selectTaskPage(filter, pageParam);
resultDomain.success("查询成功", list); int total = taskMapper.countSelectTask(filter);
PageDomain<TbCrontabTask> pageDomain = new PageDomain<>();
pageDomain.setDataList(list);
pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
pageDomain.setPageParam(pageParam);
resultDomain.success("查询成功", pageDomain);
} catch (Exception e) { } catch (Exception e) {
logger.error("分页查询定时任务异常: ", e); logger.error("分页查询定时任务异常: ", e);
resultDomain.fail("分页查询定时任务异常: " + e.getMessage()); resultDomain.fail("分页查询定时任务异常: " + e.getMessage());

View File

@@ -33,6 +33,33 @@
<!-- 查询条件 --> <!-- 查询条件 -->
<sql id="Base_Where_Clause"> <sql id="Base_Where_Clause">
<where>
deleted = 0
<if test="taskId != null and taskId != ''">
AND task_id = #{taskId}
</if>
<if test="taskName != null and taskName != ''">
AND task_name LIKE CONCAT('%', #{taskName}, '%')
</if>
<if test="taskGroup != null and taskGroup != ''">
AND task_group = #{taskGroup}
</if>
<if test="beanName != null and beanName != ''">
AND bean_name = #{beanName}
</if>
<if test="methodName != null and methodName != ''">
AND method_name = #{methodName}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="deleted != null">
AND deleted = #{deleted}
</if>
</where>
</sql>
<sql id="Filter_Where_Clause">
<where> <where>
<if test="filter != null"> <if test="filter != null">
<if test="filter.ID != null and filter.ID != ''"> <if test="filter.ID != null and filter.ID != ''">
@@ -144,7 +171,7 @@
SELECT SELECT
<include refid="Base_Column_List" /> <include refid="Base_Column_List" />
FROM tb_crontab_task FROM tb_crontab_task
<include refid="Base_Where_Clause" /> <include refid="Filter_Where_Clause" />
ORDER BY create_time DESC ORDER BY create_time DESC
</select> </select>
@@ -153,7 +180,7 @@
SELECT SELECT
<include refid="Base_Column_List" /> <include refid="Base_Column_List" />
FROM tb_crontab_task FROM tb_crontab_task
<include refid="Base_Where_Clause" /> <include refid="Filter_Where_Clause" />
ORDER BY create_time DESC ORDER BY create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset} LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select> </select>
@@ -185,4 +212,11 @@
AND deleted = 0 AND deleted = 0
LIMIT 1 LIMIT 1
</select> </select>
<!-- countSelectTask -->
<select id="countSelectTask">
SELECT COUNT(1) FROM tb_crontab_task
<include refid="Filter_Where_Clause" />
</select>
</mapper> </mapper>

View File

@@ -0,0 +1,218 @@
package org.xyzh.study.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.study.TbLearningHistory;
import org.xyzh.common.vo.LearningHistoryVO;
import org.xyzh.common.vo.LearningStatisticsVO;
import org.xyzh.study.service.SCLearningHistoryService;
import java.util.Date;
import java.util.List;
/**
* @description 学习历史控制器
* @filename LearningHistoryController.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
@RestController
@RequestMapping("/study/history")
public class LearningHistoryController {
private static final Logger logger = LoggerFactory.getLogger(LearningHistoryController.class);
@Autowired
private SCLearningHistoryService learningHistoryService;
/**
* 记录学习历史
* POST /study/history/record
*
* @param learningHistory 学习历史
* @return ResultDomain<TbLearningHistory> 记录结果
*/
@PostMapping("/record")
public ResultDomain<TbLearningHistory> recordLearningHistory(@RequestBody TbLearningHistory learningHistory) {
logger.info("记录学习历史,资源类型: {}, 资源ID: {}", learningHistory.getResourceType(), learningHistory.getResourceID());
return learningHistoryService.recordLearningHistory(learningHistory);
}
/**
* 批量记录学习历史
* POST /study/history/batch-record
*
* @param historyList 学习历史列表
* @return ResultDomain<Boolean> 记录结果
*/
@PostMapping("/batch-record")
public ResultDomain<Boolean> batchRecordLearningHistory(@RequestBody List<TbLearningHistory> historyList) {
logger.info("批量记录学习历史,数量: {}", historyList != null ? historyList.size() : 0);
return learningHistoryService.batchRecordLearningHistory(historyList);
}
/**
* 查询学习历史列表
* POST /study/history/list
*
* @param filter 过滤条件
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
*/
@PostMapping("/list")
public ResultDomain<List<LearningHistoryVO>> getLearningHistories(@RequestBody TbLearningHistory filter) {
logger.info("查询学习历史列表用户ID: {}", filter != null ? filter.getUserID() : null);
return learningHistoryService.getLearningHistories(filter);
}
/**
* 分页查询学习历史
* POST /study/history/page
*
* @param pageRequest 分页查询请求
* @return ResultDomain<PageDomain<LearningHistoryVO>> 分页结果
*/
@PostMapping("/page")
public ResultDomain<PageDomain<LearningHistoryVO>> getLearningHistoriesPage(@RequestBody PageRequest<TbLearningHistory> pageRequest) {
logger.info("分页查询学习历史,页码: {}, 每页数量: {}",
pageRequest.getPageParam() != null ? pageRequest.getPageParam().getPageNumber() : null,
pageRequest.getPageParam() != null ? pageRequest.getPageParam().getPageSize() : null);
return learningHistoryService.getLearningHistoriesPage(pageRequest);
}
/**
* 根据ID查询学习历史
* GET /study/history/{id}
*
* @param id 历史记录ID
* @return ResultDomain<TbLearningHistory> 学习历史
*/
@GetMapping("/{id}")
public ResultDomain<TbLearningHistory> getLearningHistoryById(@PathVariable String id) {
logger.info("查询学习历史ID: {}", id);
return learningHistoryService.getLearningHistoryById(id);
}
/**
* 获取当前用户的学习历史
* POST /study/history/my-histories
*
* @param filter 过滤条件
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
*/
@PostMapping("/my-histories")
public ResultDomain<List<LearningHistoryVO>> getCurrentUserLearningHistories(@RequestBody(required = false) TbLearningHistory filter) {
logger.info("查询当前用户学习历史");
return learningHistoryService.getCurrentUserLearningHistories(filter);
}
/**
* 获取当前用户最近的学习历史
* GET /study/history/recent
*
* @param limit 限制数量可选默认10
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
*/
@GetMapping("/recent")
public ResultDomain<List<LearningHistoryVO>> getRecentLearningHistories(@RequestParam(required = false, defaultValue = "10") Integer limit) {
logger.info("查询最近学习历史,限制数量: {}", limit);
// 从当前登录用户获取
return learningHistoryService.getRecentLearningHistories(null, limit);
}
/**
* 获取用户学习统计(按时间范围)
* GET /study/history/statistics
*
* @param userId 用户ID
* @param startTime 开始时间(时间戳,毫秒)
* @param endTime 结束时间(时间戳,毫秒)
* @return ResultDomain<LearningStatisticsVO> 学习统计
*/
@GetMapping("/statistics")
public ResultDomain<LearningStatisticsVO> getUserLearningStatistics(
@RequestParam String userId,
@RequestParam Long startTime,
@RequestParam Long endTime) {
logger.info("查询学习统计用户ID: {}, 开始时间: {}, 结束时间: {}", userId, startTime, endTime);
return learningHistoryService.getUserLearningStatistics(
userId,
new Date(startTime),
new Date(endTime)
);
}
/**
* 获取用户学习统计(按周期)
* GET /study/history/statistics/{userId}/{periodType}
*
* @param userId 用户ID
* @param periodType 周期类型day/week/month
* @return ResultDomain<LearningStatisticsVO> 学习统计
*/
@GetMapping("/statistics/{userId}/{periodType}")
public ResultDomain<LearningStatisticsVO> getUserLearningStatisticsByPeriod(
@PathVariable String userId,
@PathVariable String periodType) {
logger.info("查询学习统计用户ID: {}, 周期类型: {}", userId, periodType);
return learningHistoryService.getUserLearningStatisticsByPeriod(userId, periodType);
}
/**
* 获取当前用户的学习统计
* GET /study/history/my-statistics/{periodType}
*
* @param periodType 周期类型day/week/month
* @return ResultDomain<LearningStatisticsVO> 学习统计
*/
@GetMapping("/my-statistics/{periodType}")
public ResultDomain<LearningStatisticsVO> getCurrentUserLearningStatistics(@PathVariable String periodType) {
logger.info("查询当前用户学习统计,周期类型: {}", periodType);
return learningHistoryService.getCurrentUserLearningStatistics(periodType);
}
/**
* 删除学习历史
* DELETE /study/history/{id}
*
* @param id 历史记录ID
* @return ResultDomain<Boolean> 删除结果
*/
@DeleteMapping("/{id}")
public ResultDomain<Boolean> deleteLearningHistory(@PathVariable String id) {
logger.info("删除学习历史ID: {}", id);
return learningHistoryService.deleteLearningHistory(id);
}
/**
* 批量删除学习历史
* DELETE /study/history/batch
*
* @param ids 历史记录ID列表
* @return ResultDomain<Boolean> 删除结果
*/
@DeleteMapping("/batch")
public ResultDomain<Boolean> batchDeleteLearningHistories(@RequestBody List<String> ids) {
logger.info("批量删除学习历史,数量: {}", ids != null ? ids.size() : 0);
return learningHistoryService.batchDeleteLearningHistories(ids);
}
/**
* 健康检查
* GET /study/history/health
*
* @return ResultDomain<String> 健康状态
*/
@GetMapping("/health")
public ResultDomain<String> health() {
ResultDomain<String> resultDomain = new ResultDomain<>();
resultDomain.success("学习历史服务运行正常", "OK");
return resultDomain;
}
}

View File

@@ -0,0 +1,214 @@
package org.xyzh.study.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.dto.study.TbLearningHistory;
import org.xyzh.common.vo.LearningHistoryVO;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @description 学习观看历史数据访问层
* @filename LearningHistoryMapper.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
@Mapper
public interface LearningHistoryMapper extends BaseMapper<TbLearningHistory> {
/**
* @description 插入学习历史记录
* @param learningHistory 学习历史
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int insertLearningHistory(TbLearningHistory learningHistory);
/**
* @description 更新学习历史记录
* @param learningHistory 学习历史
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int updateLearningHistory(TbLearningHistory learningHistory);
/**
* @description 批量插入学习历史记录
* @param historyList 学习历史列表
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int batchInsertLearningHistories(@Param("historyList") List<TbLearningHistory> historyList);
/**
* @description 根据ID查询学习历史
* @param id 历史记录ID
* @return TbLearningHistory 学习历史
* @author yslg
* @since 2025-10-27
*/
TbLearningHistory selectById(@Param("id") String id);
/**
* @description 根据用户ID查询学习历史列表
* @param userId 用户ID
* @return List<TbLearningHistory> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningHistory> selectByUserId(@Param("userId") String userId);
/**
* @description 根据条件查询学习历史列表
* @param filter 过滤条件
* @return List<TbLearningHistory> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningHistory> selectLearningHistories(TbLearningHistory filter);
/**
* @description 根据条件查询学习历史VO列表带关联信息
* @param filter 过滤条件
* @return List<LearningHistoryVO> 学习历史VO列表
* @author yslg
* @since 2025-10-27
*/
List<LearningHistoryVO> selectLearningHistoriesWithDetails(TbLearningHistory filter);
/**
* @description 分页查询学习历史
* @param filter 过滤条件
* @param pageParam 分页参数
* @return List<TbLearningHistory> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningHistory> selectLearningHistoriesPage(@Param("filter") TbLearningHistory filter, @Param("pageParam") PageParam pageParam);
/**
* @description 统计学习历史总数
* @param filter 过滤条件
* @return long 总数
* @author yslg
* @since 2025-10-27
*/
long countLearningHistories(@Param("filter") TbLearningHistory filter);
/**
* @description 根据时间段查询学习历史
* @param userId 用户ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return List<TbLearningHistory> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningHistory> selectByUserIdAndTimeRange(@Param("userId") String userId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/**
* @description 根据时间段和资源类型查询学习历史
* @param userId 用户ID
* @param resourceType 资源类型
* @param startTime 开始时间
* @param endTime 结束时间
* @return List<TbLearningHistory> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningHistory> selectByUserIdAndResourceTypeAndTimeRange(
@Param("userId") String userId,
@Param("resourceType") Integer resourceType,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/**
* @description 统计用户某时间段的学习时长
* @param userId 用户ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return Map<String, Object> 统计结果totalDuration, learnCount等
* @author yslg
* @since 2025-10-27
*/
Map<String, Object> selectLearningStatisticsByTimeRange(
@Param("userId") String userId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/**
* @description 统计用户某时间段各资源类型的学习时长
* @param userId 用户ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return List<Map<String, Object>> 统计结果列表
* @author yslg
* @since 2025-10-27
*/
List<Map<String, Object>> selectLearningStatisticsByResourceType(
@Param("userId") String userId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/**
* @description 获取用户学习的资源列表(按时间段)
* @param userId 用户ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return List<Map<String, Object>> 资源列表
* @author yslg
* @since 2025-10-27
*/
List<Map<String, Object>> selectLearnedResourcesByTimeRange(
@Param("userId") String userId,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
/**
* @description 删除学习历史(软删除)
* @param id 历史记录ID
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int deleteLearningHistory(@Param("id") String id);
/**
* @description 批量删除学习历史(软删除)
* @param ids 历史记录ID列表
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int batchDeleteLearningHistories(@Param("ids") List<String> ids);
/**
* @description 根据会话ID查询学习历史
* @param sessionId 会话ID
* @return List<TbLearningHistory> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningHistory> selectBySessionId(@Param("sessionId") String sessionId);
/**
* @description 获取用户最近的学习历史
* @param userId 用户ID
* @param limit 限制数量
* @return List<TbLearningHistory> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningHistory> selectRecentByUserId(@Param("userId") String userId, @Param("limit") Integer limit);
}

View File

@@ -0,0 +1,145 @@
package org.xyzh.study.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.common.dto.study.TbLearningStatisticsDetail;
import org.xyzh.common.vo.LearningStatisticsDetailVO;
import java.util.Date;
import java.util.List;
/**
* @description 学习统计明细数据访问层
* @filename LearningStatisticsDetailMapper.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
@Mapper
public interface LearningStatisticsDetailMapper extends BaseMapper<TbLearningStatisticsDetail> {
/**
* @description 插入统计明细
* @param detail 统计明细
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int insertStatisticsDetail(TbLearningStatisticsDetail detail);
/**
* @description 更新统计明细
* @param detail 统计明细
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int updateStatisticsDetail(TbLearningStatisticsDetail detail);
/**
* @description 插入或更新统计明细ON DUPLICATE KEY UPDATE
* @param detail 统计明细
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int insertOrUpdateStatisticsDetail(TbLearningStatisticsDetail detail);
/**
* @description 根据条件查询统计明细
* @param filter 过滤条件
* @return List<TbLearningStatisticsDetail> 统计明细列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningStatisticsDetail> selectStatisticsDetails(TbLearningStatisticsDetail filter);
/**
* @description 根据用户ID和日期查询统计明细
* @param userId 用户ID
* @param statDate 统计日期
* @return List<TbLearningStatisticsDetail> 统计明细列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningStatisticsDetail> selectByUserIdAndDate(
@Param("userId") String userId,
@Param("statDate") Date statDate);
/**
* @description 根据用户ID和日期范围查询统计明细
* @param userId 用户ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return List<TbLearningStatisticsDetail> 统计明细列表
* @author yslg
* @since 2025-10-27
*/
List<TbLearningStatisticsDetail> selectByUserIdAndDateRange(
@Param("userId") String userId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* @description 根据用户ID和日期范围查询统计明细VO带关联信息
* @param userId 用户ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return List<LearningStatisticsDetailVO> 统计明细VO列表
* @author yslg
* @since 2025-10-27
*/
List<LearningStatisticsDetailVO> selectStatisticsDetailsWithInfo(
@Param("userId") String userId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
/**
* @description 根据用户ID、资源类型和日期查询统计明细
* @param userId 用户ID
* @param resourceType 资源类型
* @param statDate 统计日期
* @return TbLearningStatisticsDetail 统计明细
* @author yslg
* @since 2025-10-27
*/
TbLearningStatisticsDetail selectByUserIdAndResourceTypeAndDate(
@Param("userId") String userId,
@Param("resourceType") Integer resourceType,
@Param("resourceId") String resourceId,
@Param("statDate") Date statDate);
/**
* @description 删除统计明细(软删除)
* @param id 明细ID
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int deleteStatisticsDetail(@Param("id") String id);
/**
* @description 批量删除统计明细(软删除)
* @param ids 明细ID列表
* @return int 影响行数
* @author yslg
* @since 2025-10-27
*/
int batchDeleteStatisticsDetails(@Param("ids") List<String> ids);
/**
* @description 按日期汇总用户学习统计
* @param userId 用户ID
* @param startDate 开始日期
* @param endDate 结束日期
* @return List<TbLearningStatisticsDetail> 汇总结果
* @author yslg
* @since 2025-10-27
*/
List<TbLearningStatisticsDetail> selectDailySummary(
@Param("userId") String userId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
}

View File

@@ -0,0 +1,117 @@
package org.xyzh.study.service;
import org.xyzh.api.study.history.LearningHistoryAPI;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.study.TbLearningHistory;
import org.xyzh.common.vo.LearningHistoryVO;
import org.xyzh.common.vo.LearningStatisticsVO;
import java.util.Date;
import java.util.List;
/**
* @description 学习历史服务接口
* @filename SCLearningHistoryService.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
public interface SCLearningHistoryService extends LearningHistoryAPI {
/**
* @description 查询学习历史列表
* @param filter 过滤条件
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
ResultDomain<List<LearningHistoryVO>> getLearningHistories(TbLearningHistory filter);
/**
* @description 分页查询学习历史
* @param pageRequest 分页查询请求
* @return ResultDomain<PageDomain<LearningHistoryVO>> 分页结果
* @author yslg
* @since 2025-10-27
*/
ResultDomain<PageDomain<LearningHistoryVO>> getLearningHistoriesPage(PageRequest<TbLearningHistory> pageRequest);
/**
* @description 根据ID查询学习历史
* @param id 历史记录ID
* @return ResultDomain<TbLearningHistory> 学习历史
* @author yslg
* @since 2025-10-27
*/
ResultDomain<TbLearningHistory> getLearningHistoryById(String id);
/**
* @description 获取用户某时间段的学习统计
* @param userId 用户ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return ResultDomain<LearningStatisticsVO> 学习统计
* @author yslg
* @since 2025-10-27
*/
ResultDomain<LearningStatisticsVO> getUserLearningStatistics(String userId, Date startTime, Date endTime);
/**
* @description 获取用户某时间段的学习统计(按周期)
* @param userId 用户ID
* @param periodType 周期类型day/week/month
* @return ResultDomain<LearningStatisticsVO> 学习统计
* @author yslg
* @since 2025-10-27
*/
ResultDomain<LearningStatisticsVO> getUserLearningStatisticsByPeriod(String userId, String periodType);
/**
* @description 获取当前用户的学习历史
* @param filter 过滤条件
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
ResultDomain<List<LearningHistoryVO>> getCurrentUserLearningHistories(TbLearningHistory filter);
/**
* @description 获取当前用户的学习统计
* @param periodType 周期类型day/week/month
* @return ResultDomain<LearningStatisticsVO> 学习统计
* @author yslg
* @since 2025-10-27
*/
ResultDomain<LearningStatisticsVO> getCurrentUserLearningStatistics(String periodType);
/**
* @description 删除学习历史
* @param id 历史记录ID
* @return ResultDomain<Boolean> 删除结果
* @author yslg
* @since 2025-10-27
*/
ResultDomain<Boolean> deleteLearningHistory(String id);
/**
* @description 批量删除学习历史
* @param ids 历史记录ID列表
* @return ResultDomain<Boolean> 删除结果
* @author yslg
* @since 2025-10-27
*/
ResultDomain<Boolean> batchDeleteLearningHistories(List<String> ids);
/**
* @description 获取用户最近的学习历史
* @param userId 用户ID
* @param limit 限制数量
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
* @author yslg
* @since 2025-10-27
*/
ResultDomain<List<LearningHistoryVO>> getRecentLearningHistories(String userId, Integer limit);
}

View File

@@ -0,0 +1,681 @@
package org.xyzh.study.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.enums.AchievementEventType;
import org.xyzh.common.core.event.AchievementEvent;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.dto.study.TbLearningHistory;
import org.xyzh.common.dto.user.TbSysUser;
import org.xyzh.common.utils.IDUtils;
import org.xyzh.common.vo.LearningHistoryVO;
import org.xyzh.common.vo.LearningStatisticsDetailVO;
import org.xyzh.common.vo.LearningStatisticsVO;
import org.xyzh.study.mapper.LearningHistoryMapper;
import org.xyzh.study.service.SCLearningHistoryService;
import org.xyzh.system.utils.LoginUtil;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
* @description 学习历史服务实现类
* @filename SCLearningHistoryServiceImpl.java
* @author yslg
* @copyright xyzh
* @since 2025-10-27
*/
@Service
public class SCLearningHistoryServiceImpl implements SCLearningHistoryService {
private static final Logger logger = LoggerFactory.getLogger(SCLearningHistoryServiceImpl.class);
@Autowired
private LearningHistoryMapper learningHistoryMapper;
@Autowired
private ApplicationEventPublisher eventPublisher;
@Override
@Transactional
public ResultDomain<TbLearningHistory> recordLearningHistory(TbLearningHistory learningHistory) {
ResultDomain<TbLearningHistory> resultDomain = new ResultDomain<>();
try {
// 参数校验
if (learningHistory.getUserID() == null || learningHistory.getUserID().isEmpty()) {
TbSysUser currentUser = LoginUtil.getCurrentUser();
if (currentUser == null) {
resultDomain.fail("用户未登录");
return resultDomain;
}
learningHistory.setUserID(currentUser.getID());
}
if (learningHistory.getResourceType() == null) {
resultDomain.fail("资源类型不能为空");
return resultDomain;
}
if (learningHistory.getStartTime() == null) {
learningHistory.setStartTime(new Date());
}
// 计算时长
if (learningHistory.getDuration() == null || learningHistory.getDuration() <= 0) {
if (learningHistory.getEndTime() != null) {
long duration = (learningHistory.getEndTime().getTime() - learningHistory.getStartTime().getTime()) / 1000;
learningHistory.setDuration((int) duration);
} else {
learningHistory.setDuration(0);
}
}
// 判断是插入还是更新
boolean isUpdate = false;
if (learningHistory.getHistoryID() != null && !learningHistory.getHistoryID().isEmpty()) {
isUpdate = true;
}
int result;
if (isUpdate) {
// 更新已有记录
result = learningHistoryMapper.updateLearningHistory(learningHistory);
if (result > 0) {
resultDomain.success("更新学习历史成功", learningHistory);
logger.info("更新学习历史成功historyID: {}, 用户ID: {}, 资源类型: {}, 时长: {}秒",
learningHistory.getHistoryID(), learningHistory.getUserID(),
learningHistory.getResourceType(), learningHistory.getDuration());
// 发布成就事件 - 学习时长更新
publishLearningTimeEvent(learningHistory);
} else {
resultDomain.fail("更新学习历史失败");
}
} else {
// 插入新记录
learningHistory.setID(IDUtils.generateID());
learningHistory.setHistoryID(IDUtils.generateID());
learningHistory.setCreateTime(new Date());
result = learningHistoryMapper.insertLearningHistory(learningHistory);
if (result > 0) {
resultDomain.success("记录学习历史成功", learningHistory);
logger.info("记录学习历史成功ID: {}, 用户ID: {}, 资源类型: {}, 时长: {}秒",
learningHistory.getID(), learningHistory.getUserID(),
learningHistory.getResourceType(), learningHistory.getDuration());
} else {
resultDomain.fail("记录学习历史失败");
}
}
} catch (Exception e) {
logger.error("记录学习历史异常", e);
resultDomain.fail("记录学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional
public ResultDomain<Boolean> batchRecordLearningHistory(List<TbLearningHistory> historyList) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
if (historyList == null || historyList.isEmpty()) {
resultDomain.fail("学习历史列表不能为空");
return resultDomain;
}
TbSysUser currentUser = LoginUtil.getCurrentUser();
Date now = new Date();
for (TbLearningHistory learningHistory : historyList) {
if (learningHistory.getUserID() == null || learningHistory.getUserID().isEmpty()) {
if (currentUser == null) {
resultDomain.fail("用户未登录");
return resultDomain;
}
learningHistory.setUserID(currentUser.getID());
}
if (learningHistory.getStartTime() == null) {
learningHistory.setStartTime(now);
}
if (learningHistory.getID() == null || learningHistory.getID().isEmpty()) {
learningHistory.setID(IDUtils.generateID());
}
if (learningHistory.getCreateTime() == null) {
learningHistory.setCreateTime(now);
}
}
int result = learningHistoryMapper.batchInsertLearningHistories(historyList);
if (result > 0) {
resultDomain.success("批量记录学习历史成功", true);
logger.info("批量记录学习历史成功,数量: {}", result);
} else {
resultDomain.fail("批量记录学习历史失败");
}
} catch (Exception e) {
logger.error("批量记录学习历史异常", e);
resultDomain.fail("批量记录学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<List<LearningHistoryVO>> getLearningHistories(TbLearningHistory filter) {
ResultDomain<List<LearningHistoryVO>> resultDomain = new ResultDomain<>();
try {
List<LearningHistoryVO> historyVOList = learningHistoryMapper.selectLearningHistoriesWithDetails(filter);
// 格式化时长
formatDuration(historyVOList);
resultDomain.success("查询学习历史成功", historyVOList);
} catch (Exception e) {
logger.error("查询学习历史异常", e);
resultDomain.fail("查询学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<PageDomain<LearningHistoryVO>> getLearningHistoriesPage(PageRequest<TbLearningHistory> pageRequest) {
ResultDomain<PageDomain<LearningHistoryVO>> resultDomain = new ResultDomain<>();
try {
TbLearningHistory filter = pageRequest.getFilter();
if (filter == null) {
filter = new TbLearningHistory();
}
// 分页参数
PageParam pageParam = pageRequest.getPageParam();
pageParam.setOffset((pageParam.getPageNumber() - 1) * pageParam.getPageSize());
// 查询总数
long total = learningHistoryMapper.countLearningHistories(filter);
// 查询列表
List<TbLearningHistory> historyList = learningHistoryMapper.selectLearningHistoriesPage(filter, pageParam);
// 转换为VO
List<LearningHistoryVO> historyVOList = historyList.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
// 格式化时长
formatDuration(historyVOList);
// 构建分页结果
PageDomain<LearningHistoryVO> pageDomain = new PageDomain<>();
pageParam.setTotalElements(total);
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
pageDomain.setPageParam(pageParam);
pageDomain.setDataList(historyVOList);
resultDomain.success("分页查询学习历史成功", pageDomain);
} catch (Exception e) {
logger.error("分页查询学习历史异常", e);
resultDomain.fail("分页查询学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbLearningHistory> getLearningHistoryById(String id) {
ResultDomain<TbLearningHistory> resultDomain = new ResultDomain<>();
try {
if (id == null || id.isEmpty()) {
resultDomain.fail("历史记录ID不能为空");
return resultDomain;
}
TbLearningHistory learningHistory = learningHistoryMapper.selectById(id);
if (learningHistory != null) {
resultDomain.success("查询学习历史成功", learningHistory);
} else {
resultDomain.fail("学习历史不存在");
}
} catch (Exception e) {
logger.error("查询学习历史异常", e);
resultDomain.fail("查询学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<LearningStatisticsVO> getUserLearningStatistics(String userId, Date startTime, Date endTime) {
ResultDomain<LearningStatisticsVO> resultDomain = new ResultDomain<>();
try {
if (userId == null || userId.isEmpty()) {
resultDomain.fail("用户ID不能为空");
return resultDomain;
}
if (startTime == null || endTime == null) {
resultDomain.fail("开始时间和结束时间不能为空");
return resultDomain;
}
// 统计总体数据
Map<String, Object> statistics = learningHistoryMapper.selectLearningStatisticsByTimeRange(userId, startTime, endTime);
// 获取学习的资源列表
List<Map<String, Object>> learnedResources = learningHistoryMapper.selectLearnedResourcesByTimeRange(userId, startTime, endTime);
// 构建VO
LearningStatisticsVO statisticsVO = new LearningStatisticsVO();
statisticsVO.setUserID(userId);
if (statistics != null) {
statisticsVO.setTotalDuration(getIntValue(statistics, "totalDuration"));
statisticsVO.setLearnCount(getIntValue(statistics, "learnCount"));
statisticsVO.setLearnDays(getIntValue(statistics, "learnDays"));
statisticsVO.setResourceCount(getIntValue(statistics, "resourceCount"));
statisticsVO.setCourseCount(getIntValue(statistics, "courseCount"));
// 格式化时长
statisticsVO.setTotalDurationFormatted(formatDurationString(statisticsVO.getTotalDuration()));
// 计算平均每天学习时长
if (statisticsVO.getLearnDays() > 0) {
statisticsVO.setAvgDailyDuration(statisticsVO.getTotalDuration() / statisticsVO.getLearnDays());
}
}
resultDomain.success("查询学习统计成功", statisticsVO);
} catch (Exception e) {
logger.error("查询学习统计异常", e);
resultDomain.fail("查询学习统计异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<LearningStatisticsVO> getUserLearningStatisticsByPeriod(String userId, String periodType) {
ResultDomain<LearningStatisticsVO> resultDomain = new ResultDomain<>();
try {
if (userId == null || userId.isEmpty()) {
resultDomain.fail("用户ID不能为空");
return resultDomain;
}
// 计算时间范围
Date endTime = new Date();
Date startTime = calculateStartTime(endTime, periodType);
if (startTime == null) {
resultDomain.fail("无效的周期类型");
return resultDomain;
}
ResultDomain<LearningStatisticsVO> statistics = getUserLearningStatistics(userId, startTime, endTime);
if (statistics.isSuccess() && statistics.getData() != null) {
statistics.getData().setPeriod(periodType);
}
return statistics;
} catch (Exception e) {
logger.error("查询学习统计异常", e);
resultDomain.fail("查询学习统计异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<List<LearningHistoryVO>> getCurrentUserLearningHistories(TbLearningHistory filter) {
ResultDomain<List<LearningHistoryVO>> resultDomain = new ResultDomain<>();
try {
TbSysUser currentUser = LoginUtil.getCurrentUser();
if (currentUser == null) {
resultDomain.fail("用户未登录");
return resultDomain;
}
if (filter == null) {
filter = new TbLearningHistory();
}
filter.setUserID(currentUser.getID());
return getLearningHistories(filter);
} catch (Exception e) {
logger.error("查询当前用户学习历史异常", e);
resultDomain.fail("查询当前用户学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<LearningStatisticsVO> getCurrentUserLearningStatistics(String periodType) {
ResultDomain<LearningStatisticsVO> resultDomain = new ResultDomain<>();
try {
TbSysUser currentUser = LoginUtil.getCurrentUser();
if (currentUser == null) {
resultDomain.fail("用户未登录");
return resultDomain;
}
return getUserLearningStatisticsByPeriod(currentUser.getID(), periodType);
} catch (Exception e) {
logger.error("查询当前用户学习统计异常", e);
resultDomain.fail("查询当前用户学习统计异常: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional
public ResultDomain<Boolean> deleteLearningHistory(String id) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
if (id == null || id.isEmpty()) {
resultDomain.fail("历史记录ID不能为空");
return resultDomain;
}
int result = learningHistoryMapper.deleteLearningHistory(id);
if (result > 0) {
resultDomain.success("删除学习历史成功", true);
} else {
resultDomain.fail("删除学习历史失败");
}
} catch (Exception e) {
logger.error("删除学习历史异常", e);
resultDomain.fail("删除学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
@Transactional
public ResultDomain<Boolean> batchDeleteLearningHistories(List<String> ids) {
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
try {
if (ids == null || ids.isEmpty()) {
resultDomain.fail("历史记录ID列表不能为空");
return resultDomain;
}
int result = learningHistoryMapper.batchDeleteLearningHistories(ids);
if (result > 0) {
resultDomain.success("批量删除学习历史成功", true);
} else {
resultDomain.fail("批量删除学习历史失败");
}
} catch (Exception e) {
logger.error("批量删除学习历史异常", e);
resultDomain.fail("批量删除学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<List<LearningHistoryVO>> getRecentLearningHistories(String userId, Integer limit) {
ResultDomain<List<LearningHistoryVO>> resultDomain = new ResultDomain<>();
try {
if (userId == null || userId.isEmpty()) {
resultDomain.fail("用户ID不能为空");
return resultDomain;
}
if (limit == null || limit <= 0) {
limit = 10;
}
List<TbLearningHistory> historyList = learningHistoryMapper.selectRecentByUserId(userId, limit);
List<LearningHistoryVO> historyVOList = historyList.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
formatDuration(historyVOList);
resultDomain.success("查询最近学习历史成功", historyVOList);
} catch (Exception e) {
logger.error("查询最近学习历史异常", e);
resultDomain.fail("查询最近学习历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbLearningHistory> recordResourceView(String userId, String resourceId, Integer duration) {
ResultDomain<TbLearningHistory> resultDomain = new ResultDomain<>();
try {
TbLearningHistory learningHistory = new TbLearningHistory();
learningHistory.setUserID(userId);
learningHistory.setResourceType(1); // 1=资源/新闻
learningHistory.setResourceID(resourceId);
learningHistory.setDuration(duration);
learningHistory.setStartTime(new Date());
return recordLearningHistory(learningHistory);
} catch (Exception e) {
logger.error("记录资源观看历史异常", e);
resultDomain.fail("记录资源观看历史异常: " + e.getMessage());
}
return resultDomain;
}
@Override
public ResultDomain<TbLearningHistory> recordCourseLearn(String userId, String courseId, String chapterId, String nodeId, Integer duration) {
ResultDomain<TbLearningHistory> resultDomain = new ResultDomain<>();
try {
TbLearningHistory learningHistory = new TbLearningHistory();
learningHistory.setUserID(userId);
// 根据提供的信息确定资源类型
if (nodeId != null && !nodeId.isEmpty()) {
learningHistory.setResourceType(4); // 4=节点
learningHistory.setNodeID(nodeId);
} else if (chapterId != null && !chapterId.isEmpty()) {
learningHistory.setResourceType(3); // 3=章节
learningHistory.setChapterID(chapterId);
} else {
learningHistory.setResourceType(2); // 2=课程
}
learningHistory.setCourseID(courseId);
learningHistory.setChapterID(chapterId);
learningHistory.setNodeID(nodeId);
learningHistory.setDuration(duration);
learningHistory.setStartTime(new Date());
return recordLearningHistory(learningHistory);
} catch (Exception e) {
logger.error("记录课程学习历史异常", e);
resultDomain.fail("记录课程学习历史异常: " + e.getMessage());
}
return resultDomain;
}
// ==================== 私有辅助方法 ====================
/**
* 发布学习时长更新事件
*/
private void publishLearningTimeEvent(TbLearningHistory learningHistory) {
try {
// 构建成就事件
AchievementEvent event = AchievementEvent.builder(
learningHistory.getUserID(),
AchievementEventType.LEARNING_TIME_UPDATED
)
.value(learningHistory.getDuration()) // 学习时长(秒)
.extra("resourceType", learningHistory.getResourceType())
.extra("resourceID", learningHistory.getResourceID())
.extra("courseID", learningHistory.getCourseID())
.extra("historyID", learningHistory.getHistoryID())
.build();
// 发布事件(异步处理)
eventPublisher.publishEvent(event);
logger.debug("发布学习时长更新事件用户ID: {}, 时长: {}秒",
learningHistory.getUserID(), learningHistory.getDuration());
} catch (Exception e) {
// 成就事件发布失败不应该影响主业务
logger.error("发布学习时长事件失败", e);
}
}
/**
* 转换实体为VO
*/
private LearningHistoryVO convertToVO(TbLearningHistory entity) {
LearningHistoryVO vo = new LearningHistoryVO();
BeanUtils.copyProperties(entity, vo);
// 设置资源类型名称
if (entity.getResourceType() != null) {
vo.setResourceTypeName(getResourceTypeName(entity.getResourceType()));
}
return vo;
}
/**
* 格式化时长列表
*/
private void formatDuration(List<LearningHistoryVO> historyVOList) {
if (historyVOList != null) {
for (LearningHistoryVO vo : historyVOList) {
if (vo.getDuration() != null) {
vo.setDurationFormatted(formatDurationString(vo.getDuration()));
}
}
}
}
/**
* 格式化时长(秒转为可读字符串)
*/
private String formatDurationString(Integer seconds) {
if (seconds == null || seconds == 0) {
return "0秒";
}
int hours = seconds / 3600;
int minutes = (seconds % 3600) / 60;
int secs = seconds % 60;
StringBuilder sb = new StringBuilder();
if (hours > 0) {
sb.append(hours).append("小时");
}
if (minutes > 0) {
sb.append(minutes).append("分钟");
}
if (secs > 0 || sb.length() == 0) {
sb.append(secs).append("");
}
return sb.toString();
}
/**
* 获取资源类型名称
*/
private String getResourceTypeName(Integer resourceType) {
switch (resourceType) {
case 1:
return "资源/新闻";
case 2:
return "课程";
case 3:
return "章节";
case 4:
return "节点";
default:
return "未知";
}
}
/**
* 计算开始时间
*/
private Date calculateStartTime(Date endTime, String periodType) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(endTime);
if ("day".equalsIgnoreCase(periodType)) {
calendar.add(Calendar.DAY_OF_MONTH, -1);
} else if ("week".equalsIgnoreCase(periodType)) {
calendar.add(Calendar.WEEK_OF_YEAR, -1);
} else if ("month".equalsIgnoreCase(periodType)) {
calendar.add(Calendar.MONTH, -1);
} else {
return null;
}
return calendar.getTime();
}
/**
* 从Map中获取Integer值
*/
private Integer getIntValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return 0;
}
if (value instanceof Integer) {
return (Integer) value;
}
if (value instanceof Long) {
return ((Long) value).intValue();
}
if (value instanceof BigDecimal) {
return ((BigDecimal) value).intValue();
}
return 0;
}
}

View File

@@ -0,0 +1,334 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.study.mapper.LearningHistoryMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.study.TbLearningHistory">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="history_id" property="historyID" jdbcType="VARCHAR"/>
<result column="resource_type" property="resourceType" jdbcType="INTEGER"/>
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
<result column="course_id" property="courseID" jdbcType="VARCHAR"/>
<result column="chapter_id" property="chapterID" jdbcType="VARCHAR"/>
<result column="node_id" property="nodeID" jdbcType="VARCHAR"/>
<result column="task_id" property="taskID" jdbcType="VARCHAR"/>
<result column="start_time" property="startTime" jdbcType="TIMESTAMP"/>
<result column="end_time" property="endTime" jdbcType="TIMESTAMP"/>
<result column="duration" property="duration" jdbcType="INTEGER"/>
<result column="start_progress" property="startProgress" jdbcType="DECIMAL"/>
<result column="end_progress" property="endProgress" jdbcType="DECIMAL"/>
<result column="device_type" property="deviceType" jdbcType="VARCHAR"/>
<result column="ip_address" property="ipAddress" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<!-- VO结果映射带关联信息 -->
<resultMap id="VOResultMap" type="org.xyzh.common.vo.LearningHistoryVO">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="history_id" property="historyID" jdbcType="VARCHAR"/>
<result column="resource_type" property="resourceType" jdbcType="INTEGER"/>
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
<result column="resource_title" property="resourceTitle" jdbcType="VARCHAR"/>
<result column="course_id" property="courseID" jdbcType="VARCHAR"/>
<result column="course_name" property="courseName" jdbcType="VARCHAR"/>
<result column="chapter_id" property="chapterID" jdbcType="VARCHAR"/>
<result column="chapter_name" property="chapterName" jdbcType="VARCHAR"/>
<result column="node_id" property="nodeID" jdbcType="VARCHAR"/>
<result column="node_name" property="nodeName" jdbcType="VARCHAR"/>
<result column="task_id" property="taskID" jdbcType="VARCHAR"/>
<result column="task_name" property="taskName" jdbcType="VARCHAR"/>
<result column="start_time" property="startTime" jdbcType="TIMESTAMP"/>
<result column="end_time" property="endTime" jdbcType="TIMESTAMP"/>
<result column="duration" property="duration" jdbcType="INTEGER"/>
<result column="start_progress" property="startProgress" jdbcType="DECIMAL"/>
<result column="end_progress" property="endProgress" jdbcType="DECIMAL"/>
<result column="device_type" property="deviceType" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 基础字段 -->
<sql id="Base_Column_List">
id, user_id, history_id, resource_type, resource_id, course_id, chapter_id, node_id, task_id,
start_time, end_time, duration, start_progress, end_progress, device_type, ip_address,
creator, create_time, deleted
</sql>
<!-- 通用条件 -->
<sql id="Where_Clause">
<where>
deleted = 0
<if test="userID != null and userID != ''">
AND user_id = #{userID}
</if>
<if test="historyID != null and historyID != ''">
AND history_id = #{historyID}
</if>
<if test="resourceType != null">
AND resource_type = #{resourceType}
</if>
<if test="resourceID != null and resourceID != ''">
AND resource_id = #{resourceID}
</if>
<if test="courseID != null and courseID != ''">
AND course_id = #{courseID}
</if>
<if test="chapterID != null and chapterID != ''">
AND chapter_id = #{chapterID}
</if>
<if test="nodeID != null and nodeID != ''">
AND node_id = #{nodeID}
</if>
<if test="taskID != null and taskID != ''">
AND task_id = #{taskID}
</if>
<if test="deviceType != null and deviceType != ''">
AND device_type = #{deviceType}
</if>
</where>
</sql>
<!-- 插入学习历史 -->
<insert id="insertLearningHistory">
INSERT INTO tb_learning_history (
id, user_id, history_id, resource_type, resource_id, course_id, chapter_id, node_id, task_id,
start_time, end_time, duration, start_progress, end_progress, device_type, ip_address,
creator, create_time, deleted
) VALUES (
#{id}, #{userID}, #{historyID}, #{resourceType}, #{resourceID}, #{courseID}, #{chapterID}, #{nodeID}, #{taskID},
#{startTime}, #{endTime}, #{duration}, #{startProgress}, #{endProgress}, #{deviceType}, #{ipAddress},
#{creator}, #{createTime}, 0
)
</insert>
<!-- 更新学习历史 -->
<update id="updateLearningHistory">
UPDATE tb_learning_history
<set>
<if test="endTime != null">
end_time = #{endTime},
</if>
<if test="duration != null">
duration = #{duration},
</if>
<if test="startProgress != null">
start_progress = #{startProgress},
</if>
<if test="endProgress != null">
end_progress = #{endProgress},
</if>
<if test="deviceType != null">
device_type = #{deviceType},
</if>
<if test="ipAddress != null">
ip_address = #{ipAddress},
</if>
</set>
WHERE history_id = #{historyID} AND deleted = 0
</update>
<!-- 批量插入学习历史 -->
<insert id="batchInsertLearningHistories">
INSERT INTO tb_learning_history (
id, user_id, history_id, resource_type, resource_id, course_id, chapter_id, node_id, task_id,
start_time, end_time, duration, start_progress, end_progress, device_type, ip_address,
creator, create_time, deleted
) VALUES
<foreach collection="historyList" item="item" separator=",">
(
#{item.id}, #{item.userID}, #{item.historyID}, #{item.resourceType}, #{item.resourceID},
#{item.courseID}, #{item.chapterID}, #{item.nodeID}, #{item.taskID},
#{item.startTime}, #{item.endTime}, #{item.duration}, #{item.startProgress}, #{item.endProgress},
#{item.deviceType}, #{item.ipAddress}, #{item.creator}, #{item.createTime}, 0
)
</foreach>
</insert>
<!-- 根据ID查询 -->
<select id="selectById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
WHERE id = #{id} AND deleted = 0
</select>
<!-- 根据用户ID查询 -->
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
WHERE user_id = #{userId} AND deleted = 0
ORDER BY start_time DESC, create_time DESC
</select>
<!-- 根据条件查询学习历史列表 -->
<select id="selectLearningHistories" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
<include refid="Where_Clause"/>
ORDER BY start_time DESC, create_time DESC
</select>
<!-- 根据条件查询学习历史VO列表带关联信息 -->
<select id="selectLearningHistoriesWithDetails" resultMap="VOResultMap">
SELECT
lh.id,
lh.user_id,
u.username as user_name,
lh.history_id,
lh.resource_type,
lh.resource_id,
CASE
WHEN lh.resource_type = 1 THEN r.title
WHEN lh.resource_type = 2 THEN c.name
WHEN lh.resource_type = 3 THEN ch.name
WHEN lh.resource_type = 4 THEN n.name
END as resource_title,
lh.course_id,
c.name as course_name,
lh.chapter_id,
ch.name as chapter_name,
lh.node_id,
n.name as node_name,
lh.task_id,
t.name as task_name,
lh.start_time,
lh.end_time,
lh.duration,
lh.start_progress,
lh.end_progress,
lh.device_type,
lh.create_time
FROM tb_learning_history lh
LEFT JOIN tb_sys_user u ON lh.user_id = u.id
LEFT JOIN tb_resource r ON lh.resource_type = 1 AND lh.resource_id = r.resource_id
LEFT JOIN tb_course c ON lh.course_id = c.course_id
LEFT JOIN tb_course_chapter ch ON lh.chapter_id = ch.chapter_id
LEFT JOIN tb_course_node n ON lh.node_id = n.node_id
LEFT JOIN tb_learning_task t ON lh.task_id = t.task_id
<include refid="Where_Clause"/>
ORDER BY lh.start_time DESC, lh.create_time DESC
</select>
<!-- 分页查询学习历史 -->
<select id="selectLearningHistoriesPage" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
<include refid="Where_Clause"/>
ORDER BY start_time DESC, create_time DESC
LIMIT #{pageParam.offset}, #{pageParam.pageSize}
</select>
<!-- 统计学习历史总数 -->
<select id="countLearningHistories" resultType="long">
SELECT COUNT(*)
FROM tb_learning_history
<include refid="Where_Clause"/>
</select>
<!-- 根据时间段查询学习历史 -->
<select id="selectByUserIdAndTimeRange" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
WHERE user_id = #{userId}
AND start_time BETWEEN #{startTime} AND #{endTime}
AND deleted = 0
ORDER BY start_time DESC
</select>
<!-- 根据时间段和资源类型查询学习历史 -->
<select id="selectByUserIdAndResourceTypeAndTimeRange" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
WHERE user_id = #{userId}
AND resource_type = #{resourceType}
AND start_time BETWEEN #{startTime} AND #{endTime}
AND deleted = 0
ORDER BY start_time DESC
</select>
<!-- 统计用户某时间段的学习时长 -->
<select id="selectLearningStatisticsByTimeRange" resultType="map">
SELECT
SUM(duration) as totalDuration,
COUNT(*) as learnCount,
COUNT(DISTINCT DATE(start_time)) as learnDays,
COUNT(DISTINCT CASE WHEN resource_type = 1 THEN resource_id END) as resourceCount,
COUNT(DISTINCT CASE WHEN resource_type = 2 THEN course_id END) as courseCount
FROM tb_learning_history
WHERE user_id = #{userId}
AND start_time BETWEEN #{startTime} AND #{endTime}
AND deleted = 0
</select>
<!-- 统计用户某时间段各资源类型的学习时长 -->
<select id="selectLearningStatisticsByResourceType" resultType="map">
SELECT
resource_type as resourceType,
SUM(duration) as totalDuration,
COUNT(*) as learnCount,
COUNT(DISTINCT resource_id) as resourceCount
FROM tb_learning_history
WHERE user_id = #{userId}
AND start_time BETWEEN #{startTime} AND #{endTime}
AND deleted = 0
GROUP BY resource_type
ORDER BY resource_type
</select>
<!-- 获取用户学习的资源列表(按时间段) -->
<select id="selectLearnedResourcesByTimeRange" resultType="map">
SELECT
resource_type as resourceType,
resource_id as resourceId,
course_id as courseId,
chapter_id as chapterId,
node_id as nodeId,
SUM(duration) as totalDuration,
COUNT(*) as learnCount,
MAX(start_time) as lastLearnTime
FROM tb_learning_history
WHERE user_id = #{userId}
AND start_time BETWEEN #{startTime} AND #{endTime}
AND deleted = 0
GROUP BY resource_type, resource_id, course_id, chapter_id, node_id
ORDER BY lastLearnTime DESC
</select>
<!-- 删除学习历史(软删除) -->
<update id="deleteLearningHistory">
UPDATE tb_learning_history
SET deleted = 1, delete_time = NOW()
WHERE id = #{id}
</update>
<!-- 批量删除学习历史(软删除) -->
<update id="batchDeleteLearningHistories">
UPDATE tb_learning_history
SET deleted = 1, delete_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 根据会话ID查询学习历史 -->
<select id="selectByhistoryID" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
WHERE history_id = #{historyID} AND deleted = 0
ORDER BY start_time DESC
</select>
<!-- 获取用户最近的学习历史 -->
<select id="selectRecentByUserId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_history
WHERE user_id = #{userId} AND deleted = 0
ORDER BY start_time DESC, create_time DESC
LIMIT #{limit}
</select>
</mapper>

View File

@@ -0,0 +1,246 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.study.mapper.LearningStatisticsDetailMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.study.TbLearningStatisticsDetail">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="user_id" property="userID" jdbcType="VARCHAR"/>
<result column="stat_date" property="statDate" jdbcType="DATE"/>
<result column="resource_type" property="resourceType" jdbcType="INTEGER"/>
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
<result column="course_id" property="courseID" jdbcType="VARCHAR"/>
<result column="chapter_id" property="chapterID" jdbcType="VARCHAR"/>
<result column="total_duration" property="totalDuration" jdbcType="INTEGER"/>
<result column="learn_count" property="learnCount" jdbcType="INTEGER"/>
<result column="is_complete" property="isComplete" jdbcType="BOOLEAN"/>
<result column="complete_time" property="completeTime" jdbcType="TIMESTAMP"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<!-- VO结果映射带关联信息 -->
<resultMap id="VOResultMap" type="org.xyzh.common.vo.LearningStatisticsDetailVO">
<result column="stat_date" property="statDate" jdbcType="DATE"/>
<result column="resource_type" property="resourceType" jdbcType="INTEGER"/>
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
<result column="resource_title" property="resourceTitle" jdbcType="VARCHAR"/>
<result column="course_id" property="courseID" jdbcType="VARCHAR"/>
<result column="course_name" property="courseName" jdbcType="VARCHAR"/>
<result column="chapter_id" property="chapterID" jdbcType="VARCHAR"/>
<result column="chapter_name" property="chapterName" jdbcType="VARCHAR"/>
<result column="total_duration" property="totalDuration" jdbcType="INTEGER"/>
<result column="learn_count" property="learnCount" jdbcType="INTEGER"/>
<result column="is_complete" property="isComplete" jdbcType="BOOLEAN"/>
<result column="complete_time" property="completeTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 基础字段 -->
<sql id="Base_Column_List">
id, user_id, stat_date, resource_type, resource_id, course_id, chapter_id,
total_duration, learn_count, is_complete, complete_time,
creator, updater, create_time, update_time, deleted
</sql>
<!-- 通用条件 -->
<sql id="Where_Clause">
<where>
deleted = 0
<if test="userID != null and userID != ''">
AND user_id = #{userID}
</if>
<if test="statDate != null">
AND stat_date = #{statDate}
</if>
<if test="resourceType != null">
AND resource_type = #{resourceType}
</if>
<if test="resourceID != null and resourceID != ''">
AND resource_id = #{resourceID}
</if>
<if test="courseID != null and courseID != ''">
AND course_id = #{courseID}
</if>
<if test="chapterID != null and chapterID != ''">
AND chapter_id = #{chapterID}
</if>
<if test="isComplete != null">
AND is_complete = #{isComplete}
</if>
</where>
</sql>
<!-- 插入统计明细 -->
<insert id="insertStatisticsDetail">
INSERT INTO tb_learning_statistics_detail (
id, user_id, stat_date, resource_type, resource_id, course_id, chapter_id,
total_duration, learn_count, is_complete, complete_time,
creator, create_time, deleted
) VALUES (
#{id}, #{userID}, #{statDate}, #{resourceType}, #{resourceID}, #{courseID}, #{chapterID},
#{totalDuration}, #{learnCount}, #{isComplete}, #{completeTime},
#{creator}, #{createTime}, 0
)
</insert>
<!-- 更新统计明细 -->
<update id="updateStatisticsDetail">
UPDATE tb_learning_statistics_detail
<set>
<if test="totalDuration != null">
total_duration = #{totalDuration},
</if>
<if test="learnCount != null">
learn_count = #{learnCount},
</if>
<if test="isComplete != null">
is_complete = #{isComplete},
</if>
<if test="completeTime != null">
complete_time = #{completeTime},
</if>
<if test="updater != null">
updater = #{updater},
</if>
update_time = NOW()
</set>
WHERE id = #{id}
</update>
<!-- 插入或更新统计明细ON DUPLICATE KEY UPDATE -->
<insert id="insertOrUpdateStatisticsDetail">
INSERT INTO tb_learning_statistics_detail (
id, user_id, stat_date, resource_type, resource_id, course_id, chapter_id,
total_duration, learn_count, is_complete, complete_time,
creator, create_time, deleted
) VALUES (
#{id}, #{userID}, #{statDate}, #{resourceType}, #{resourceID}, #{courseID}, #{chapterID},
#{totalDuration}, #{learnCount}, #{isComplete}, #{completeTime},
#{creator}, NOW(), 0
)
ON DUPLICATE KEY UPDATE
total_duration = total_duration + #{totalDuration},
learn_count = learn_count + #{learnCount},
is_complete = #{isComplete},
complete_time = #{completeTime},
updater = #{updater},
update_time = NOW()
</insert>
<!-- 根据条件查询统计明细 -->
<select id="selectStatisticsDetails" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_statistics_detail
<include refid="Where_Clause"/>
ORDER BY stat_date DESC, create_time DESC
</select>
<!-- 根据用户ID和日期查询统计明细 -->
<select id="selectByUserIdAndDate" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_statistics_detail
WHERE user_id = #{userId}
AND stat_date = #{statDate}
AND deleted = 0
ORDER BY resource_type, total_duration DESC
</select>
<!-- 根据用户ID和日期范围查询统计明细 -->
<select id="selectByUserIdAndDateRange" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_statistics_detail
WHERE user_id = #{userId}
AND stat_date BETWEEN #{startDate} AND #{endDate}
AND deleted = 0
ORDER BY stat_date DESC, resource_type, total_duration DESC
</select>
<!-- 根据用户ID和日期范围查询统计明细VO带关联信息 -->
<select id="selectStatisticsDetailsWithInfo" resultMap="VOResultMap">
SELECT
sd.stat_date,
sd.resource_type,
sd.resource_id,
CASE
WHEN sd.resource_type = 1 THEN r.title
WHEN sd.resource_type = 2 THEN c.name
WHEN sd.resource_type = 3 THEN ch.name
END as resource_title,
sd.course_id,
c.name as course_name,
sd.chapter_id,
ch.name as chapter_name,
sd.total_duration,
sd.learn_count,
sd.is_complete,
sd.complete_time
FROM tb_learning_statistics_detail sd
LEFT JOIN tb_resource r ON sd.resource_type = 1 AND sd.resource_id = r.resource_id
LEFT JOIN tb_course c ON sd.course_id = c.course_id
LEFT JOIN tb_course_chapter ch ON sd.chapter_id = ch.chapter_id
WHERE sd.user_id = #{userId}
AND sd.stat_date BETWEEN #{startDate} AND #{endDate}
AND sd.deleted = 0
ORDER BY sd.stat_date DESC, sd.total_duration DESC
</select>
<!-- 根据用户ID、资源类型和日期查询统计明细 -->
<select id="selectByUserIdAndResourceTypeAndDate" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM tb_learning_statistics_detail
WHERE user_id = #{userId}
AND resource_type = #{resourceType}
AND resource_id = #{resourceId}
AND stat_date = #{statDate}
AND deleted = 0
LIMIT 1
</select>
<!-- 删除统计明细(软删除) -->
<update id="deleteStatisticsDetail">
UPDATE tb_learning_statistics_detail
SET deleted = 1, delete_time = NOW()
WHERE id = #{id}
</update>
<!-- 批量删除统计明细(软删除) -->
<update id="batchDeleteStatisticsDetails">
UPDATE tb_learning_statistics_detail
SET deleted = 1, delete_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 按日期汇总用户学习统计 -->
<select id="selectDailySummary" resultMap="BaseResultMap">
SELECT
NULL as id,
user_id,
stat_date,
NULL as resource_type,
NULL as resource_id,
NULL as course_id,
NULL as chapter_id,
SUM(total_duration) as total_duration,
SUM(learn_count) as learn_count,
NULL as is_complete,
NULL as complete_time,
NULL as creator,
NULL as updater,
MIN(create_time) as create_time,
MAX(update_time) as update_time,
0 as deleted
FROM tb_learning_statistics_detail
WHERE user_id = #{userId}
AND stat_date BETWEEN #{startDate} AND #{endDate}
AND deleted = 0
GROUP BY user_id, stat_date
ORDER BY stat_date DESC
</select>
</mapper>

View File

@@ -9,3 +9,4 @@ export { courseApi } from './course';
export { learningTaskApi } from './learning-task'; export { learningTaskApi } from './learning-task';
export { learningRecordApi } from './learning-record'; export { learningRecordApi } from './learning-record';
export { learningPlanApi } from './learning-plan'; export { learningPlanApi } from './learning-plan';
export { learningHistoryApi } from './learning-history';

View File

@@ -0,0 +1,204 @@
/**
* @description 学习历史相关API
* @author yslg
* @since 2025-10-27
*/
import { api } from '@/apis/index';
import type {
TbLearningHistory,
LearningHistoryVO,
LearningStatisticsVO,
ResultDomain,
PageDomain,
PageRequest
} from '@/types';
/**
* 学习历史API服务
*/
export const learningHistoryApi = {
/**
* 记录学习历史
* @param learningHistory 学习历史数据
* @returns Promise<ResultDomain<TbLearningHistory>>
*/
async recordLearningHistory(learningHistory: TbLearningHistory): Promise<ResultDomain<TbLearningHistory>> {
const response = await api.post<TbLearningHistory>('/study/history/record', learningHistory, {
showLoading: false // 禁用 loading 动画,避免影响用户体验
} as any);
return response.data;
},
/**
* 批量记录学习历史
* @param historyList 学习历史列表
* @returns Promise<ResultDomain<boolean>>
*/
async batchRecordLearningHistory(historyList: TbLearningHistory[]): Promise<ResultDomain<boolean>> {
const response = await api.post<boolean>('/study/history/batch-record', historyList);
return response.data;
},
/**
* 查询学习历史列表
* @param filter 过滤条件
* @returns Promise<ResultDomain<LearningHistoryVO[]>>
*/
async getLearningHistories(filter?: Partial<TbLearningHistory>): Promise<ResultDomain<LearningHistoryVO[]>> {
const response = await api.post<LearningHistoryVO[]>('/study/history/list', filter || {});
return response.data;
},
/**
* 分页查询学习历史
* @param pageRequest 分页查询请求
* @returns Promise<ResultDomain<PageDomain<LearningHistoryVO>>>
*/
async getLearningHistoriesPage(pageRequest: PageRequest<TbLearningHistory>): Promise<ResultDomain<PageDomain<LearningHistoryVO>>> {
const response = await api.post<PageDomain<LearningHistoryVO>>('/study/history/page', pageRequest);
return response.data;
},
/**
* 根据ID查询学习历史
* @param id 历史记录ID
* @returns Promise<ResultDomain<TbLearningHistory>>
*/
async getLearningHistoryById(id: string): Promise<ResultDomain<TbLearningHistory>> {
const response = await api.get<TbLearningHistory>(`/study/history/${id}`);
return response.data;
},
/**
* 获取当前用户的学习历史
* @param filter 过滤条件
* @returns Promise<ResultDomain<LearningHistoryVO[]>>
*/
async getCurrentUserLearningHistories(filter?: Partial<TbLearningHistory>): Promise<ResultDomain<LearningHistoryVO[]>> {
const response = await api.post<LearningHistoryVO[]>('/study/history/my-histories', filter || {});
return response.data;
},
/**
* 获取当前用户最近的学习历史
* @param limit 限制数量默认10
* @returns Promise<ResultDomain<LearningHistoryVO[]>>
*/
async getRecentLearningHistories(limit = 10): Promise<ResultDomain<LearningHistoryVO[]>> {
const response = await api.get<LearningHistoryVO[]>(`/study/history/recent?limit=${limit}`);
return response.data;
},
/**
* 获取用户学习统计(按时间范围)
* @param userId 用户ID
* @param startTime 开始时间Date对象
* @param endTime 结束时间Date对象
* @returns Promise<ResultDomain<LearningStatisticsVO>>
*/
async getUserLearningStatistics(userId: string, startTime: Date, endTime: Date): Promise<ResultDomain<LearningStatisticsVO>> {
const response = await api.get<LearningStatisticsVO>(
`/study/history/statistics?userId=${userId}&startTime=${startTime.getTime()}&endTime=${endTime.getTime()}`
);
return response.data;
},
/**
* 获取用户学习统计(按周期)
* @param userId 用户ID
* @param periodType 周期类型day/week/month
* @returns Promise<ResultDomain<LearningStatisticsVO>>
*/
async getUserLearningStatisticsByPeriod(userId: string, periodType: 'day' | 'week' | 'month'): Promise<ResultDomain<LearningStatisticsVO>> {
const response = await api.get<LearningStatisticsVO>(`/study/history/statistics/${userId}/${periodType}`);
return response.data;
},
/**
* 获取当前用户的学习统计
* @param periodType 周期类型day/week/month
* @returns Promise<ResultDomain<LearningStatisticsVO>>
*/
async getCurrentUserLearningStatistics(periodType: 'day' | 'week' | 'month'): Promise<ResultDomain<LearningStatisticsVO>> {
const response = await api.get<LearningStatisticsVO>(`/study/history/my-statistics/${periodType}`);
return response.data;
},
/**
* 删除学习历史
* @param id 历史记录ID
* @returns Promise<ResultDomain<boolean>>
*/
async deleteLearningHistory(id: string): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>(`/study/history/${id}`);
return response.data;
},
/**
* 批量删除学习历史
* @param ids 历史记录ID列表
* @returns Promise<ResultDomain<boolean>>
*/
async batchDeleteLearningHistories(ids: string[]): Promise<ResultDomain<boolean>> {
const response = await api.delete<boolean>('/study/history/batch', ids);
return response.data;
},
/**
* 简化记录方法 - 观看新闻/资源
* @param userId 用户ID
* @param resourceId 资源ID
* @param duration 学习时长(秒)
* @returns Promise<ResultDomain<TbLearningHistory>>
*/
async recordResourceView(userId: string, resourceId: string, duration: number): Promise<ResultDomain<TbLearningHistory>> {
const learningHistory: TbLearningHistory = {
userID: userId,
resourceType: 1, // 1资源/新闻
resourceID: resourceId,
duration: duration,
deviceType: 'web'
};
return this.recordLearningHistory(learningHistory);
},
/**
* 简化记录方法 - 学习课程
* @param userId 用户ID
* @param courseId 课程ID
* @param chapterId 章节ID可选
* @param nodeId 节点ID可选
* @param duration 学习时长(秒)
* @returns Promise<ResultDomain<TbLearningHistory>>
*/
async recordCourseLearn(
userId: string,
courseId: string,
chapterId?: string,
nodeId?: string,
duration?: number
): Promise<ResultDomain<TbLearningHistory>> {
const learningHistory: TbLearningHistory = {
userID: userId,
resourceType: nodeId ? 4 : (chapterId ? 3 : 2), // 2课程 3章节 4节点
resourceID: nodeId || chapterId || courseId,
courseID: courseId,
chapterID: chapterId,
nodeID: nodeId,
duration: duration || 0,
deviceType: 'web'
};
return this.recordLearningHistory(learningHistory);
},
/**
* 健康检查
* @returns Promise<ResultDomain<string>>
*/
async health(): Promise<ResultDomain<string>> {
const response = await api.get<string>('/study/history/health');
return response.data;
}
};

View File

@@ -25,9 +25,9 @@ export interface BaseDTO {
*/ */
export interface PageParam { export interface PageParam {
/** 当前页码 */ /** 当前页码 */
page: number; pageNumber: number;
/** 每页条数 */ /** 每页条数 */
size: number; pageSize: number;
/** 总页数 */ /** 总页数 */
totalPages?: number; totalPages?: number;

View File

@@ -331,3 +331,187 @@ export interface LearningRecordStatistics {
/** 完成任务数 */ /** 完成任务数 */
taskCount?: number; taskCount?: number;
} }
/**
* 学习观看历史表
*/
export interface TbLearningHistory extends BaseDTO {
/** 用户ID */
userID?: string;
/** 学习会话ID */
historyID?: string;
/** 资源类型1资源/新闻 2课程 3章节 4节点 */
resourceType?: number;
/** 资源ID */
resourceID?: string;
/** 课程ID */
courseID?: string;
/** 章节ID */
chapterID?: string;
/** 节点ID */
nodeID?: string;
/** 关联任务ID */
taskID?: string;
/** 开始学习时间 */
startTime?: string;
/** 结束学习时间 */
endTime?: string;
/** 本次学习时长(秒) */
duration?: number;
/** 开始进度0-100 */
startProgress?: number;
/** 结束进度0-100 */
endProgress?: number;
/** 设备类型web/mobile/app */
deviceType?: string;
/** IP地址 */
ipAddress?: string;
/** 创建者 */
creator?: string;
}
/**
* 学习统计明细表
*/
export interface TbLearningStatisticsDetail extends BaseDTO {
/** 用户ID */
userID?: string;
/** 统计日期 */
statDate?: string;
/** 资源类型1资源/新闻 2课程 3章节 */
resourceType?: number;
/** 资源ID */
resourceID?: string;
/** 课程ID */
courseID?: string;
/** 章节ID */
chapterID?: string;
/** 学习时长(秒) */
totalDuration?: number;
/** 学习次数 */
learnCount?: number;
/** 是否完成 */
isComplete?: boolean;
/** 完成时间 */
completeTime?: string;
/** 创建者 */
creator?: string;
/** 更新者 */
updater?: string;
}
/**
* 学习历史响应VO
*/
export interface LearningHistoryVO {
/** 历史记录ID */
id?: string;
/** 用户ID */
userID?: string;
/** 用户名称(关联查询) */
userName?: string;
/** 学习会话ID */
sessionID?: string;
/** 资源类型1资源/新闻 2课程 3章节 4节点 */
resourceType?: number;
/** 资源类型名称 */
resourceTypeName?: string;
/** 资源ID */
resourceID?: string;
/** 资源标题(关联查询) */
resourceTitle?: string;
/** 课程ID */
courseID?: string;
/** 课程名称(关联查询) */
courseName?: string;
/** 章节ID */
chapterID?: string;
/** 章节名称(关联查询) */
chapterName?: string;
/** 节点ID */
nodeID?: string;
/** 节点名称(关联查询) */
nodeName?: string;
/** 关联任务ID */
taskID?: string;
/** 任务名称(关联查询) */
taskName?: string;
/** 开始学习时间 */
startTime?: string;
/** 结束学习时间 */
endTime?: string;
/** 本次学习时长(秒) */
duration?: number;
/** 学习时长格式化后1小时20分钟 */
durationFormatted?: string;
/** 开始进度0-100 */
startProgress?: number;
/** 结束进度0-100 */
endProgress?: number;
/** 设备类型 */
deviceType?: string;
/** 创建时间 */
createTime?: string;
}
/**
* 学习统计明细VO
*/
export interface LearningStatisticsDetailVO {
/** 统计日期 */
statDate?: string;
/** 资源类型1资源/新闻 2课程 3章节 */
resourceType?: number;
/** 资源类型名称 */
resourceTypeName?: string;
/** 资源ID */
resourceID?: string;
/** 资源标题 */
resourceTitle?: string;
/** 课程ID */
courseID?: string;
/** 课程名称 */
courseName?: string;
/** 章节ID */
chapterID?: string;
/** 章节名称 */
chapterName?: string;
/** 学习时长(秒) */
totalDuration?: number;
/** 学习时长(格式化) */
totalDurationFormatted?: string;
/** 学习次数 */
learnCount?: number;
/** 是否完成 */
isComplete?: boolean;
/** 完成时间 */
completeTime?: string;
}
/**
* 学习统计响应VO
*/
export interface LearningStatisticsVO {
/** 用户ID */
userID?: string;
/** 统计周期day/week/month */
period?: string;
/** 总学习时长(秒) */
totalDuration?: number;
/** 总学习时长(格式化) */
totalDurationFormatted?: string;
/** 学习天数 */
learnDays?: number;
/** 学习资源数量 */
resourceCount?: number;
/** 学习课程数量 */
courseCount?: number;
/** 学习次数 */
learnCount?: number;
/** 完成数量 */
completeCount?: number;
/** 平均每天学习时长(秒) */
avgDailyDuration?: number;
/** 学习明细列表 */
details?: LearningStatisticsDetailVO[];
}

View File

@@ -492,7 +492,7 @@ async function handleViewUsers(row: Achievement) {
try { try {
achieversLoading.value = true; achieversLoading.value = true;
const result = await achievementApi.getRecentAchievers( const result = await achievementApi.getRecentAchievers(
{ page: 1, size: 100 }, { pageNumber: 1, pageSize: 100 },
{ achievementID: row.achievementID } { achievementID: row.achievementID }
); );
achieverList.value = result.dataList || []; achieverList.value = result.dataList || [];

View File

@@ -103,8 +103,8 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0"> <div class="pagination-wrapper" v-if="total > 0">
<el-pagination <el-pagination
v-model:current-page="pageParam.page" v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.size" v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]" :page-sizes="[10, 20, 50, 100]"
:total="total" :total="total"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@@ -238,8 +238,8 @@ const searchForm = reactive({
// 分页参数 // 分页参数
const pageParam = reactive<PageParam>({ const pageParam = reactive<PageParam>({
page: 1, pageNumber: 1,
size: 20 pageSize: 20
}); });
// 对话框状态 // 对话框状态
@@ -277,7 +277,7 @@ const loadLogList = async () => {
// 搜索 // 搜索
const handleSearch = () => { const handleSearch = () => {
pageParam.page = 1; pageParam.pageNumber = 1;
loadLogList(); loadLogList();
}; };
@@ -286,19 +286,19 @@ const handleReset = () => {
searchForm.taskName = ''; searchForm.taskName = '';
searchForm.taskGroup = ''; searchForm.taskGroup = '';
searchForm.executeStatus = undefined; searchForm.executeStatus = undefined;
pageParam.page = 1; pageParam.pageNumber = 1;
loadLogList(); loadLogList();
}; };
// 分页变化 // 分页变化
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
pageParam.page = page; pageParam.pageNumber = page;
loadLogList(); loadLogList();
}; };
const handleSizeChange = (size: number) => { const handleSizeChange = (size: number) => {
pageParam.size = size; pageParam.pageSize = size;
pageParam.page = 1; pageParam.pageNumber = 1;
loadLogList(); loadLogList();
}; };

View File

@@ -155,8 +155,8 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0"> <div class="pagination-wrapper" v-if="total > 0">
<el-pagination <el-pagination
v-model:current-page="pageParam.page" v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.size" v-model:page-size="pageParam.pageSize"
:page-sizes="[9, 18, 36]" :page-sizes="[9, 18, 36]"
:total="total" :total="total"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@@ -287,8 +287,8 @@ const searchForm = reactive({
// 分页参数 // 分页参数
const pageParam = reactive<PageParam>({ const pageParam = reactive<PageParam>({
page: 1, pageNumber: 1,
size: 9 pageSize: 10
}); });
// 对话框状态 // 对话框状态
@@ -341,7 +341,7 @@ const loadCrawlerList = async () => {
// 搜索 // 搜索
const handleSearch = () => { const handleSearch = () => {
pageParam.page = 1; pageParam.pageNumber = 1;
loadCrawlerList(); loadCrawlerList();
}; };
@@ -349,19 +349,19 @@ const handleSearch = () => {
const handleReset = () => { const handleReset = () => {
searchForm.taskName = ''; searchForm.taskName = '';
searchForm.status = undefined; searchForm.status = undefined;
pageParam.page = 1; pageParam.pageNumber = 1;
loadCrawlerList(); loadCrawlerList();
}; };
// 分页变化 // 分页变化
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
pageParam.page = page; pageParam.pageNumber = page;
loadCrawlerList(); loadCrawlerList();
}; };
const handleSizeChange = (size: number) => { const handleSizeChange = (size: number) => {
pageParam.size = size; pageParam.pageSize = size;
pageParam.page = 1; pageParam.pageNumber = 1;
loadCrawlerList(); loadCrawlerList();
}; };

View File

@@ -128,8 +128,8 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-wrapper" v-if="total > 0"> <div class="pagination-wrapper" v-if="total > 0">
<el-pagination <el-pagination
v-model:current-page="pageParam.page" v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.size" v-model:page-size="pageParam.pageSize"
:page-sizes="[10, 20, 50, 100]" :page-sizes="[10, 20, 50, 100]"
:total="total" :total="total"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@@ -264,8 +264,8 @@ const searchForm = reactive({
// 分页参数 // 分页参数
const pageParam = reactive<PageParam>({ const pageParam = reactive<PageParam>({
page: 1, pageNumber: 1,
size: 20 pageSize: 20
}); });
// 对话框状态 // 对话框状态
@@ -316,7 +316,7 @@ const loadTaskList = async () => {
// 搜索 // 搜索
const handleSearch = () => { const handleSearch = () => {
pageParam.page = 1; pageParam.pageNumber = 1;
loadTaskList(); loadTaskList();
}; };
@@ -325,19 +325,19 @@ const handleReset = () => {
searchForm.taskName = ''; searchForm.taskName = '';
searchForm.taskGroup = ''; searchForm.taskGroup = '';
searchForm.status = undefined; searchForm.status = undefined;
pageParam.page = 1; pageParam.pageNumber = 1;
loadTaskList(); loadTaskList();
}; };
// 分页变化 // 分页变化
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
pageParam.page = page; pageParam.pageNumber = page;
loadTaskList(); loadTaskList();
}; };
const handleSizeChange = (size: number) => { const handleSizeChange = (size: number) => {
pageParam.size = size; pageParam.pageSize = size;
pageParam.page = 1; pageParam.pageNumber = 1;
loadTaskList(); loadTaskList();
}; };

View File

@@ -41,8 +41,8 @@
</el-table> </el-table>
<el-pagination <el-pagination
v-model:current-page="pageParam.page" v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.size" v-model:page-size="pageParam.pageSize"
:total="total" :total="total"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@@ -76,8 +76,8 @@ import { ArticleStatus } from '@/types/enums';
const router = useRouter(); const router = useRouter();
const searchKeyword = ref(''); const searchKeyword = ref('');
const pageParam = ref<PageParam>({ const pageParam = ref<PageParam>({
page: 1, pageNumber: 1,
size: 10 pageSize: 10
}); });
const filter = ref<ResourceSearchParams>({ const filter = ref<ResourceSearchParams>({
keyword: searchKeyword.value keyword: searchKeyword.value
@@ -232,12 +232,12 @@ function getActionButtonText(status: number) {
} }
function handleSizeChange(val: number) { function handleSizeChange(val: number) {
pageParam.value.size = val; pageParam.value.pageSize = val;
loadArticles(); loadArticles();
} }
function handleCurrentChange(val: number) { function handleCurrentChange(val: number) {
pageParam.value.page = val; pageParam.value.pageNumber = val;
loadArticles(); loadArticles();
} }
</script> </script>

View File

@@ -98,9 +98,9 @@ import { useRouter, useRoute } from 'vue-router';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { ArrowLeft } from '@element-plus/icons-vue'; import { ArrowLeft } from '@element-plus/icons-vue';
import { resourceApi } from '@/apis/resource'; import { resourceApi } from '@/apis/resource';
import { learningRecordApi } from '@/apis/study'; import { learningRecordApi, learningHistoryApi } from '@/apis/study';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import type { ResourceCategory, Resource, ResourceVO, LearningRecord } from '@/types'; import type { ResourceCategory, Resource, ResourceVO, LearningRecord, TbLearningHistory } from '@/types';
defineOptions({ defineOptions({
name: 'ArticleShowView' name: 'ArticleShowView'
@@ -154,6 +154,11 @@ const totalVideos = ref(0); // 视频总数
const completedVideos = ref<Set<number>>(new Set()); // 已完成的视频索引 const completedVideos = ref<Set<number>>(new Set()); // 已完成的视频索引
const userInfo = computed(() => store.getters['auth/user']); const userInfo = computed(() => store.getters['auth/user']);
// 学习历史记录相关
const learningHistory = ref<TbLearningHistory | null>(null);
const historyStartTime = ref(0);
const historyTimer = ref<number | null>(null);
// 当前显示的文章数据 // 当前显示的文章数据
const currentArticleData = computed(() => { const currentArticleData = computed(() => {
// Dialog 模式使用传入的 articleData // Dialog 模式使用传入的 articleData
@@ -182,13 +187,18 @@ onMounted(() => {
// 如果传入了 articleData则不需要从路由加载 // 如果传入了 articleData则不需要从路由加载
if (props.articleData && Object.keys(props.articleData).length > 0) { if (props.articleData && Object.keys(props.articleData).length > 0) {
loadedArticleData.value = props.articleData; loadedArticleData.value = props.articleData;
// 即使传入了数据也要监听视频如果有taskId
if (taskId || route.query.taskId) { const resourceID = props.articleData.resourceID;
const resourceID = props.articleData.resourceID; if (resourceID) {
if (resourceID) { // 创建学习历史记录(每次进入都创建新记录)
createHistoryRecord(resourceID);
// 如果有taskId还要创建学习记录
if (taskId || route.query.taskId) {
loadLearningRecord(resourceID); loadLearningRecord(resourceID);
} }
} }
// 初始化视频监听 // 初始化视频监听
nextTick().then(() => { nextTick().then(() => {
setTimeout(() => { setTimeout(() => {
@@ -215,12 +225,34 @@ onBeforeUnmount(() => {
saveLearningProgress(); saveLearningProgress();
} }
stopLearningTimer(); stopLearningTimer();
// 保存学习历史记录
if (learningHistory.value) {
saveHistoryRecord();
}
stopHistoryTimer();
}); });
// 监听 articleData 变化(用于 ResourceArticle 切换文章) // 监听 articleData 变化(用于 ResourceArticle 切换文章)
watch(() => props.articleData, (newData) => { watch(() => props.articleData, async (newData, oldData) => {
if (!props.asDialog && newData && Object.keys(newData).length > 0) { if (!props.asDialog) {
loadedArticleData.value = newData; // 如果从有数据变成null或者切换到新文章都需要保存当前历史
if (learningHistory.value && (oldData && (!newData || oldData.resourceID !== newData.resourceID))) {
await saveHistoryRecord();
stopHistoryTimer();
}
// 加载新文章数据
if (newData && Object.keys(newData).length > 0) {
loadedArticleData.value = newData;
// 为新文章创建学习历史记录
const resourceID = newData.resourceID;
if (resourceID) {
await createHistoryRecord(resourceID);
}
}
// 重新初始化视频监听 // 重新初始化视频监听
nextTick().then(() => { nextTick().then(() => {
setTimeout(() => { setTimeout(() => {
@@ -243,6 +275,9 @@ async function loadArticle(resourceID: string) {
// 增加浏览次数 // 增加浏览次数
await resourceApi.incrementViewCount(resourceID); await resourceApi.incrementViewCount(resourceID);
// 创建学习历史记录(每次进入都创建新记录)
await createHistoryRecord(resourceID);
// 等待 DOM 更新后监听视频(增加延迟确保 DOM 完全渲染) // 等待 DOM 更新后监听视频(增加延迟确保 DOM 完全渲染)
await nextTick(); await nextTick();
setTimeout(() => { setTimeout(() => {
@@ -472,6 +507,81 @@ function handleVideoEnded(videoIndex: number) {
} }
} }
// ==================== 学习历史记录功能 ====================
// 创建学习历史记录
async function createHistoryRecord(resourceID: string) {
if (!userInfo.value?.id) return;
try {
const res = await learningHistoryApi.recordResourceView(
userInfo.value.id,
resourceID,
0 // 初始时长为0
);
if (res.success && res.data) {
learningHistory.value = res.data;
console.log('✅ 学习历史记录创建成功:', learningHistory.value);
// 开始计时
startHistoryTimer();
}
} catch (error) {
console.error('❌ 创建学习历史记录失败:', error);
}
}
// 开始学习历史计时
function startHistoryTimer() {
historyStartTime.value = Date.now();
// 每30秒保存一次学习历史
historyTimer.value = window.setInterval(() => {
saveHistoryRecord();
}, 30000); // 30秒
}
// 停止学习历史计时
function stopHistoryTimer() {
if (historyTimer.value) {
clearInterval(historyTimer.value);
historyTimer.value = null;
}
}
// 保存学习历史记录
async function saveHistoryRecord() {
if (!userInfo.value?.id || !learningHistory.value) return;
const currentTime = Date.now();
const duration = Math.floor((currentTime - historyStartTime.value) / 1000); // 秒
// 如果时长太短小于1秒不保存
if (duration < 1) return;
try {
const updatedHistory: TbLearningHistory = {
...learningHistory.value,
duration: (learningHistory.value.duration || 0) + duration,
endTime: new Date().toISOString()
};
// 调用API更新学习历史
const res = await learningHistoryApi.recordLearningHistory(updatedHistory);
if (res.success && res.data) {
learningHistory.value = res.data;
console.log(`💾 学习历史已保存 - 累计时长: ${learningHistory.value.duration}`);
}
// 重置开始时间
historyStartTime.value = currentTime;
} catch (error) {
console.error('❌ 保存学习历史失败:', error);
}
}
// 格式化日期简单格式YYYY-MM-DD // 格式化日期简单格式YYYY-MM-DD
function formatDateSimple(date: string | Date): string { function formatDateSimple(date: string | Date): string {
if (!date) return ''; if (!date) return '';
@@ -486,6 +596,12 @@ function formatDateSimple(date: string | Date): string {
// 关闭处理 // 关闭处理
function handleClose() { function handleClose() {
// 非Dialog模式下关闭时保存学习历史
if (!props.asDialog && learningHistory.value) {
saveHistoryRecord();
stopHistoryTimer();
}
if (props.asDialog) { if (props.asDialog) {
visible.value = false; visible.value = false;
} }

View File

@@ -221,11 +221,11 @@ import {
} from '@element-plus/icons-vue'; } from '@element-plus/icons-vue';
import { ArticleShowView } from '@/views/article'; import { ArticleShowView } from '@/views/article';
import { courseApi } from '@/apis/study'; import { courseApi } from '@/apis/study';
import { learningRecordApi } from '@/apis/study'; import { learningRecordApi, learningHistoryApi } from '@/apis/study';
import { resourceApi } from '@/apis/resource'; import { resourceApi } from '@/apis/resource';
import { useStore } from 'vuex'; import { useStore } from 'vuex';
import { FILE_DOWNLOAD_URL } from '@/config'; import { FILE_DOWNLOAD_URL } from '@/config';
import type { CourseVO, LearningRecord } from '@/types'; import type { CourseVO, LearningRecord, TbLearningHistory } from '@/types';
interface Props { interface Props {
courseId: string; courseId: string;
@@ -269,6 +269,11 @@ const previousNodeKey = ref<string | null>(null);
const totalRichTextVideos = ref(0); const totalRichTextVideos = ref(0);
const completedRichTextVideos = ref<Set<number>>(new Set()); const completedRichTextVideos = ref<Set<number>>(new Set());
// 学习历史记录
const learningHistory = ref<TbLearningHistory | null>(null);
const historyStartTime = ref<number>(0);
const historyTimer = ref<number | null>(null);
// 当前节点 // 当前节点
const currentNode = computed(() => { const currentNode = computed(() => {
if (!courseVO.value || !courseVO.value.courseChapters) return null; if (!courseVO.value || !courseVO.value.courseChapters) return null;
@@ -346,9 +351,18 @@ watch(() => [props.chapterIndex, props.nodeIndex], () => {
loadNodeContent(); loadNodeContent();
}, { immediate: true }); }, { immediate: true });
watch(currentNode, () => { watch(currentNode, async () => {
// 保存上一个节点的学习历史记录
if (learningHistory.value) {
await saveHistoryRecord();
stopHistoryTimer();
}
if (currentNode.value) { if (currentNode.value) {
loadNodeContent(); loadNodeContent();
// 为新节点创建学习历史记录
await createHistoryRecord();
} }
}); });
@@ -359,6 +373,12 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopLearningTimer(); stopLearningTimer();
saveLearningProgress(); saveLearningProgress();
// 保存学习历史记录
if (learningHistory.value) {
saveHistoryRecord();
}
stopHistoryTimer();
}); });
// 加载课程 // 加载课程
@@ -807,9 +827,97 @@ function toggleSidebar() {
} }
// 返回 // 返回
// ==================== 学习历史记录功能 ====================
// 创建学习历史记录
async function createHistoryRecord() {
if (!userInfo.value?.id || !courseVO.value || !currentNode.value) return;
try {
const chapterVO = courseVO.value.courseChapters[currentChapterIndex.value];
const chapter = chapterVO?.chapter;
const node = currentNode.value;
const res = await learningHistoryApi.recordCourseLearn(
userInfo.value.id,
props.courseId,
chapter?.chapterID,
node.nodeID,
0 // 初始时长为0
);
if (res.success && res.data) {
learningHistory.value = res.data;
console.log('✅ 课程学习历史记录创建成功:', learningHistory.value);
// 开始计时
startHistoryTimer();
}
} catch (error) {
console.error('❌ 创建课程学习历史记录失败:', error);
}
}
// 开始学习历史计时
function startHistoryTimer() {
historyStartTime.value = Date.now();
// 每30秒保存一次学习历史
historyTimer.value = window.setInterval(() => {
saveHistoryRecord();
}, 30000); // 30秒
}
// 停止学习历史计时
function stopHistoryTimer() {
if (historyTimer.value) {
clearInterval(historyTimer.value);
historyTimer.value = null;
}
}
// 保存学习历史记录
async function saveHistoryRecord() {
if (!userInfo.value?.id || !learningHistory.value) return;
const currentTime = Date.now();
const duration = Math.floor((currentTime - historyStartTime.value) / 1000); // 秒
// 如果时长太短小于1秒不保存
if (duration < 1) return;
try {
const updatedHistory: TbLearningHistory = {
...learningHistory.value,
duration: (learningHistory.value.duration || 0) + duration,
endTime: new Date().toISOString()
};
// 调用API更新学习历史
const res = await learningHistoryApi.recordLearningHistory(updatedHistory);
if (res.success && res.data) {
learningHistory.value = res.data;
console.log(`💾 课程学习历史已保存 - 累计时长: ${learningHistory.value.duration}秒 - 节点: ${currentNode.value?.name}`);
}
// 重置开始时间
historyStartTime.value = currentTime;
} catch (error) {
console.error('❌ 保存课程学习历史失败:', error);
}
}
function handleBack() { function handleBack() {
stopLearningTimer(); stopLearningTimer();
saveLearningProgress(); saveLearningProgress();
// 保存学习历史记录
if (learningHistory.value) {
saveHistoryRecord();
}
stopHistoryTimer();
emit('back'); emit('back');
} }
</script> </script>

View File

@@ -15,7 +15,7 @@
@category-change="handleCategoryChange" @category-change="handleCategoryChange"
/> />
<ResourceList <ResourceList
v-show="!showArticle" v-if="!showArticle"
ref="resourceListRef" ref="resourceListRef"
:category-id="currentCategoryId" :category-id="currentCategoryId"
:search-keyword="searchKeyword" :search-keyword="searchKeyword"
@@ -23,7 +23,7 @@
@list-updated="handleListUpdated" @list-updated="handleListUpdated"
/> />
<ResourceArticle <ResourceArticle
v-show="showArticle" v-if="showArticle"
:resource-id="currentResourceId" :resource-id="currentResourceId"
:category-id="currentCategoryId" :category-id="currentCategoryId"
:resource-list="resourceList" :resource-list="resourceList"

View File

@@ -80,8 +80,8 @@
<!-- 分页 --> <!-- 分页 -->
<div v-if="total > 0" class="pagination-container"> <div v-if="total > 0" class="pagination-container">
<el-pagination <el-pagination
v-model:current-page="pageParam.page" v-model:current-page="pageParam.pageNumber"
v-model:page-size="pageParam.size" v-model:page-size="pageParam.pageSize"
:total="total" :total="total"
:page-sizes="[6, 12, 24, 48]" :page-sizes="[6, 12, 24, 48]"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
@@ -117,8 +117,8 @@ const total = ref(0);
// 分页参数 // 分页参数
const pageParam = ref<PageParam>({ const pageParam = ref<PageParam>({
page: 1, pageNumber: 1,
size: 6 pageSize: 6
}); });
onMounted(() => { onMounted(() => {