From e50de4a2778f7ce89f27e8f86985f1b153f52108 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Mon, 27 Oct 2025 13:42:34 +0800 Subject: [PATCH] =?UTF-8?q?serv\web-=E5=AD=A6=E4=B9=A0=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.bin/mysql/sql/createTableLearning.sql | 75 ++ .../api/study/history/LearningHistoryAPI.java | 59 ++ .../common/dto/study/TbLearningHistory.java | 250 +++++++ .../dto/study/TbLearningStatisticsDetail.java | 194 +++++ .../org/xyzh/common/vo/LearningHistoryVO.java | 347 +++++++++ .../common/vo/LearningStatisticsDetailVO.java | 213 ++++++ .../xyzh/common/vo/LearningStatisticsVO.java | 176 +++++ .../crontab/controller/CrontabController.java | 14 +- .../crontab/mapper/CrontabTaskMapper.java | 9 + .../service/impl/CrontabServiceImpl.java | 10 +- .../resources/mapper/CrontabTaskMapper.xml | 38 +- .../controller/LearningHistoryController.java | 218 ++++++ .../study/mapper/LearningHistoryMapper.java | 214 ++++++ .../LearningStatisticsDetailMapper.java | 145 ++++ .../service/SCLearningHistoryService.java | 117 +++ .../impl/SCLearningHistoryServiceImpl.java | 681 ++++++++++++++++++ .../mapper/LearningHistoryMapper.xml | 334 +++++++++ .../mapper/LearningStatisticsDetailMapper.xml | 246 +++++++ schoolNewsWeb/src/apis/study/index.ts | 1 + .../src/apis/study/learning-history.ts | 204 ++++++ schoolNewsWeb/src/types/base/index.ts | 4 +- schoolNewsWeb/src/types/study/index.ts | 184 +++++ .../achievement/AchievementManagementView.vue | 2 +- .../manage/crontab/LogManagementView.vue | 18 +- .../admin/manage/crontab/NewsCrawlerView.vue | 18 +- .../manage/crontab/TaskManagementView.vue | 18 +- .../manage/resource/ArticleManagementView.vue | 12 +- .../src/views/article/ArticleShowView.vue | 134 +++- .../course/components/CourseLearning.vue | 114 ++- .../resource-center/ResourceCenterView.vue | 4 +- .../src/views/study-plan/CourseCenterView.vue | 8 +- 31 files changed, 3997 insertions(+), 64 deletions(-) create mode 100644 schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/history/LearningHistoryAPI.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningHistory.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningStatisticsDetail.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningHistoryVO.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsDetailVO.java create mode 100644 schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsVO.java create mode 100644 schoolNewsServ/study/src/main/java/org/xyzh/study/controller/LearningHistoryController.java create mode 100644 schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningHistoryMapper.java create mode 100644 schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningStatisticsDetailMapper.java create mode 100644 schoolNewsServ/study/src/main/java/org/xyzh/study/service/SCLearningHistoryService.java create mode 100644 schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningHistoryServiceImpl.java create mode 100644 schoolNewsServ/study/src/main/resources/mapper/LearningHistoryMapper.xml create mode 100644 schoolNewsServ/study/src/main/resources/mapper/LearningStatisticsDetailMapper.xml create mode 100644 schoolNewsWeb/src/apis/study/learning-history.ts diff --git a/schoolNewsServ/.bin/mysql/sql/createTableLearning.sql b/schoolNewsServ/.bin/mysql/sql/createTableLearning.sql index aa108fa..9203a1f 100644 --- a/schoolNewsServ/.bin/mysql/sql/createTableLearning.sql +++ b/schoolNewsServ/.bin/mysql/sql/createTableLearning.sql @@ -116,3 +116,78 @@ CREATE TABLE `tb_learning_statistics` ( KEY `idx_date` (`stat_date`) ) 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='学习统计明细表'; + diff --git a/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/history/LearningHistoryAPI.java b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/history/LearningHistoryAPI.java new file mode 100644 index 0000000..02c7f28 --- /dev/null +++ b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/history/LearningHistoryAPI.java @@ -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 记录结果 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain recordLearningHistory(TbLearningHistory learningHistory); + + /** + * @description 批量记录学习历史 + * @param historyList 学习历史列表 + * @return ResultDomain 记录结果 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain batchRecordLearningHistory(List historyList); + + /** + * @description 简化记录方法 - 观看新闻/资源 + * @param userId 用户ID + * @param resourceId 资源ID + * @param duration 学习时长(秒) + * @return ResultDomain 记录结果 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain recordResourceView(String userId, String resourceId, Integer duration); + + /** + * @description 简化记录方法 - 学习课程 + * @param userId 用户ID + * @param courseId 课程ID + * @param chapterId 章节ID(可选) + * @param nodeId 节点ID(可选) + * @param duration 学习时长(秒) + * @return ResultDomain 记录结果 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain recordCourseLearn(String userId, String courseId, String chapterId, String nodeId, Integer duration); +} + diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningHistory.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningHistory.java new file mode 100644 index 0000000..3e20a83 --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningHistory.java @@ -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() + + '}'; + } +} + diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningStatisticsDetail.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningStatisticsDetail.java new file mode 100644 index 0000000..a2725f8 --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/study/TbLearningStatisticsDetail.java @@ -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() + + '}'; + } +} + diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningHistoryVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningHistoryVO.java new file mode 100644 index 0000000..cff540c --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningHistoryVO.java @@ -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 + + '}'; + } +} + diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsDetailVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsDetailVO.java new file mode 100644 index 0000000..bbb8edd --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsDetailVO.java @@ -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 + + '}'; + } +} + diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsVO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsVO.java new file mode 100644 index 0000000..fcc954f --- /dev/null +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/vo/LearningStatisticsVO.java @@ -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 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 getDetails() { + return details; + } + + public void setDetails(List 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 + + '}'; + } +} + diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java index ef30cc6..59d6ab3 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/controller/CrontabController.java @@ -73,7 +73,7 @@ public class CrontabController { * @since 2025-10-25 */ @GetMapping("/task/{taskId}") - public ResultDomain getTaskById(@PathVariable String taskId) { + public ResultDomain getTaskById(@PathVariable(value = "taskId") String taskId) { return crontabService.getTaskById(taskId); } @@ -111,7 +111,7 @@ public class CrontabController { * @since 2025-10-25 */ @PostMapping("/task/start/{taskId}") - public ResultDomain startTask(@PathVariable String taskId) { + public ResultDomain startTask(@PathVariable(value = "taskId") String taskId) { return crontabService.startTask(taskId); } @@ -123,7 +123,7 @@ public class CrontabController { * @since 2025-10-25 */ @PostMapping("/task/pause/{taskId}") - public ResultDomain pauseTask(@PathVariable String taskId) { + public ResultDomain pauseTask(@PathVariable(value = "taskId") String taskId) { return crontabService.pauseTask(taskId); } @@ -135,7 +135,7 @@ public class CrontabController { * @since 2025-10-25 */ @PostMapping("/task/execute/{taskId}") - public ResultDomain executeTaskOnce(@PathVariable String taskId) { + public ResultDomain executeTaskOnce(@PathVariable(value = "taskId") String taskId) { return crontabService.executeTaskOnce(taskId); } @@ -161,7 +161,7 @@ public class CrontabController { * @since 2025-10-25 */ @GetMapping("/log/task/{taskId}") - public ResultDomain getLogsByTaskId(@PathVariable String taskId) { + public ResultDomain getLogsByTaskId(@PathVariable(value = "taskId") String taskId) { return crontabService.getLogsByTaskId(taskId); } @@ -199,7 +199,7 @@ public class CrontabController { * @since 2025-10-25 */ @GetMapping("/log/{logId}") - public ResultDomain getLogById(@PathVariable String logId) { + public ResultDomain getLogById(@PathVariable(value = "logId") String logId) { return crontabService.getLogById(logId); } @@ -211,7 +211,7 @@ public class CrontabController { * @since 2025-10-25 */ @DeleteMapping("/log/clean/{days}") - public ResultDomain cleanLogs(@PathVariable Integer days) { + public ResultDomain cleanLogs(@PathVariable(value = "days") Integer days) { return crontabService.cleanLogs(days); } diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java index 8ca15c1..33aae00 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/mapper/CrontabTaskMapper.java @@ -100,5 +100,14 @@ public interface CrontabTaskMapper extends BaseMapper { * @since 2025-10-25 */ 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); } diff --git a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java index 0ff3e44..4093881 100644 --- a/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java +++ b/schoolNewsServ/crontab/src/main/java/org/xyzh/crontab/service/impl/CrontabServiceImpl.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.xyzh.api.crontab.CrontabService; 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.dto.crontab.TbCrontabTask; import org.xyzh.common.dto.crontab.TbCrontabLog; @@ -215,7 +216,14 @@ public class CrontabServiceImpl implements CrontabService { } List list = taskMapper.selectTaskPage(filter, pageParam); - resultDomain.success("查询成功", list); + int total = taskMapper.countSelectTask(filter); + + PageDomain 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) { logger.error("分页查询定时任务异常: ", e); resultDomain.fail("分页查询定时任务异常: " + e.getMessage()); diff --git a/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml b/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml index 5cae457..684f173 100644 --- a/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml +++ b/schoolNewsServ/crontab/src/main/resources/mapper/CrontabTaskMapper.xml @@ -33,6 +33,33 @@ + + deleted = 0 + + AND task_id = #{taskId} + + + AND task_name LIKE CONCAT('%', #{taskName}, '%') + + + AND task_group = #{taskGroup} + + + AND bean_name = #{beanName} + + + AND method_name = #{methodName} + + + AND status = #{status} + + + AND deleted = #{deleted} + + + + + @@ -144,7 +171,7 @@ SELECT FROM tb_crontab_task - + ORDER BY create_time DESC @@ -153,7 +180,7 @@ SELECT FROM tb_crontab_task - + ORDER BY create_time DESC LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset} @@ -185,4 +212,11 @@ AND deleted = 0 LIMIT 1 + + + + diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/controller/LearningHistoryController.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/controller/LearningHistoryController.java new file mode 100644 index 0000000..2533ad1 --- /dev/null +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/controller/LearningHistoryController.java @@ -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 记录结果 + */ + @PostMapping("/record") + public ResultDomain recordLearningHistory(@RequestBody TbLearningHistory learningHistory) { + logger.info("记录学习历史,资源类型: {}, 资源ID: {}", learningHistory.getResourceType(), learningHistory.getResourceID()); + return learningHistoryService.recordLearningHistory(learningHistory); + } + + /** + * 批量记录学习历史 + * POST /study/history/batch-record + * + * @param historyList 学习历史列表 + * @return ResultDomain 记录结果 + */ + @PostMapping("/batch-record") + public ResultDomain batchRecordLearningHistory(@RequestBody List historyList) { + logger.info("批量记录学习历史,数量: {}", historyList != null ? historyList.size() : 0); + return learningHistoryService.batchRecordLearningHistory(historyList); + } + + /** + * 查询学习历史列表 + * POST /study/history/list + * + * @param filter 过滤条件 + * @return ResultDomain> 学习历史列表 + */ + @PostMapping("/list") + public ResultDomain> getLearningHistories(@RequestBody TbLearningHistory filter) { + logger.info("查询学习历史列表,用户ID: {}", filter != null ? filter.getUserID() : null); + return learningHistoryService.getLearningHistories(filter); + } + + /** + * 分页查询学习历史 + * POST /study/history/page + * + * @param pageRequest 分页查询请求 + * @return ResultDomain> 分页结果 + */ + @PostMapping("/page") + public ResultDomain> getLearningHistoriesPage(@RequestBody PageRequest 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 学习历史 + */ + @GetMapping("/{id}") + public ResultDomain getLearningHistoryById(@PathVariable String id) { + logger.info("查询学习历史,ID: {}", id); + return learningHistoryService.getLearningHistoryById(id); + } + + /** + * 获取当前用户的学习历史 + * POST /study/history/my-histories + * + * @param filter 过滤条件 + * @return ResultDomain> 学习历史列表 + */ + @PostMapping("/my-histories") + public ResultDomain> getCurrentUserLearningHistories(@RequestBody(required = false) TbLearningHistory filter) { + logger.info("查询当前用户学习历史"); + return learningHistoryService.getCurrentUserLearningHistories(filter); + } + + /** + * 获取当前用户最近的学习历史 + * GET /study/history/recent + * + * @param limit 限制数量(可选,默认10) + * @return ResultDomain> 学习历史列表 + */ + @GetMapping("/recent") + public ResultDomain> 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 学习统计 + */ + @GetMapping("/statistics") + public ResultDomain 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 学习统计 + */ + @GetMapping("/statistics/{userId}/{periodType}") + public ResultDomain 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 学习统计 + */ + @GetMapping("/my-statistics/{periodType}") + public ResultDomain getCurrentUserLearningStatistics(@PathVariable String periodType) { + logger.info("查询当前用户学习统计,周期类型: {}", periodType); + return learningHistoryService.getCurrentUserLearningStatistics(periodType); + } + + /** + * 删除学习历史 + * DELETE /study/history/{id} + * + * @param id 历史记录ID + * @return ResultDomain 删除结果 + */ + @DeleteMapping("/{id}") + public ResultDomain deleteLearningHistory(@PathVariable String id) { + logger.info("删除学习历史,ID: {}", id); + return learningHistoryService.deleteLearningHistory(id); + } + + /** + * 批量删除学习历史 + * DELETE /study/history/batch + * + * @param ids 历史记录ID列表 + * @return ResultDomain 删除结果 + */ + @DeleteMapping("/batch") + public ResultDomain batchDeleteLearningHistories(@RequestBody List ids) { + logger.info("批量删除学习历史,数量: {}", ids != null ? ids.size() : 0); + return learningHistoryService.batchDeleteLearningHistories(ids); + } + + /** + * 健康检查 + * GET /study/history/health + * + * @return ResultDomain 健康状态 + */ + @GetMapping("/health") + public ResultDomain health() { + ResultDomain resultDomain = new ResultDomain<>(); + resultDomain.success("学习历史服务运行正常", "OK"); + return resultDomain; + } +} + diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningHistoryMapper.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningHistoryMapper.java new file mode 100644 index 0000000..c3cd6e4 --- /dev/null +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningHistoryMapper.java @@ -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 { + + /** + * @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 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 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + List selectByUserId(@Param("userId") String userId); + + /** + * @description 根据条件查询学习历史列表 + * @param filter 过滤条件 + * @return List 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + List selectLearningHistories(TbLearningHistory filter); + + /** + * @description 根据条件查询学习历史VO列表(带关联信息) + * @param filter 过滤条件 + * @return List 学习历史VO列表 + * @author yslg + * @since 2025-10-27 + */ + List selectLearningHistoriesWithDetails(TbLearningHistory filter); + + /** + * @description 分页查询学习历史 + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return List 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + List 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 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + List 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 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + List 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 统计结果(totalDuration, learnCount等) + * @author yslg + * @since 2025-10-27 + */ + Map selectLearningStatisticsByTimeRange( + @Param("userId") String userId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * @description 统计用户某时间段各资源类型的学习时长 + * @param userId 用户ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return List> 统计结果列表 + * @author yslg + * @since 2025-10-27 + */ + List> selectLearningStatisticsByResourceType( + @Param("userId") String userId, + @Param("startTime") Date startTime, + @Param("endTime") Date endTime); + + /** + * @description 获取用户学习的资源列表(按时间段) + * @param userId 用户ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return List> 资源列表 + * @author yslg + * @since 2025-10-27 + */ + List> 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 ids); + + /** + * @description 根据会话ID查询学习历史 + * @param sessionId 会话ID + * @return List 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + List selectBySessionId(@Param("sessionId") String sessionId); + + /** + * @description 获取用户最近的学习历史 + * @param userId 用户ID + * @param limit 限制数量 + * @return List 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + List selectRecentByUserId(@Param("userId") String userId, @Param("limit") Integer limit); +} + diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningStatisticsDetailMapper.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningStatisticsDetailMapper.java new file mode 100644 index 0000000..d393db4 --- /dev/null +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/mapper/LearningStatisticsDetailMapper.java @@ -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 { + + /** + * @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 统计明细列表 + * @author yslg + * @since 2025-10-27 + */ + List selectStatisticsDetails(TbLearningStatisticsDetail filter); + + /** + * @description 根据用户ID和日期查询统计明细 + * @param userId 用户ID + * @param statDate 统计日期 + * @return List 统计明细列表 + * @author yslg + * @since 2025-10-27 + */ + List selectByUserIdAndDate( + @Param("userId") String userId, + @Param("statDate") Date statDate); + + /** + * @description 根据用户ID和日期范围查询统计明细 + * @param userId 用户ID + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return List 统计明细列表 + * @author yslg + * @since 2025-10-27 + */ + List 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 统计明细VO列表 + * @author yslg + * @since 2025-10-27 + */ + List 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 ids); + + /** + * @description 按日期汇总用户学习统计 + * @param userId 用户ID + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return List 汇总结果 + * @author yslg + * @since 2025-10-27 + */ + List selectDailySummary( + @Param("userId") String userId, + @Param("startDate") Date startDate, + @Param("endDate") Date endDate); +} + diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/SCLearningHistoryService.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/SCLearningHistoryService.java new file mode 100644 index 0000000..8cbcd69 --- /dev/null +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/SCLearningHistoryService.java @@ -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> 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain> getLearningHistories(TbLearningHistory filter); + + /** + * @description 分页查询学习历史 + * @param pageRequest 分页查询请求 + * @return ResultDomain> 分页结果 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain> getLearningHistoriesPage(PageRequest pageRequest); + + /** + * @description 根据ID查询学习历史 + * @param id 历史记录ID + * @return ResultDomain 学习历史 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain getLearningHistoryById(String id); + + /** + * @description 获取用户某时间段的学习统计 + * @param userId 用户ID + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return ResultDomain 学习统计 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain getUserLearningStatistics(String userId, Date startTime, Date endTime); + + /** + * @description 获取用户某时间段的学习统计(按周期) + * @param userId 用户ID + * @param periodType 周期类型(day/week/month) + * @return ResultDomain 学习统计 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain getUserLearningStatisticsByPeriod(String userId, String periodType); + + /** + * @description 获取当前用户的学习历史 + * @param filter 过滤条件 + * @return ResultDomain> 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain> getCurrentUserLearningHistories(TbLearningHistory filter); + + /** + * @description 获取当前用户的学习统计 + * @param periodType 周期类型(day/week/month) + * @return ResultDomain 学习统计 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain getCurrentUserLearningStatistics(String periodType); + + /** + * @description 删除学习历史 + * @param id 历史记录ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain deleteLearningHistory(String id); + + /** + * @description 批量删除学习历史 + * @param ids 历史记录ID列表 + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain batchDeleteLearningHistories(List ids); + + /** + * @description 获取用户最近的学习历史 + * @param userId 用户ID + * @param limit 限制数量 + * @return ResultDomain> 学习历史列表 + * @author yslg + * @since 2025-10-27 + */ + ResultDomain> getRecentLearningHistories(String userId, Integer limit); +} + diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningHistoryServiceImpl.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningHistoryServiceImpl.java new file mode 100644 index 0000000..315227d --- /dev/null +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningHistoryServiceImpl.java @@ -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 recordLearningHistory(TbLearningHistory learningHistory) { + ResultDomain 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 batchRecordLearningHistory(List historyList) { + ResultDomain 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> getLearningHistories(TbLearningHistory filter) { + ResultDomain> resultDomain = new ResultDomain<>(); + + try { + List historyVOList = learningHistoryMapper.selectLearningHistoriesWithDetails(filter); + + // 格式化时长 + formatDuration(historyVOList); + + resultDomain.success("查询学习历史成功", historyVOList); + + } catch (Exception e) { + logger.error("查询学习历史异常", e); + resultDomain.fail("查询学习历史异常: " + e.getMessage()); + } + + return resultDomain; + } + + @Override + public ResultDomain> getLearningHistoriesPage(PageRequest pageRequest) { + ResultDomain> 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 historyList = learningHistoryMapper.selectLearningHistoriesPage(filter, pageParam); + + // 转换为VO + List historyVOList = historyList.stream() + .map(this::convertToVO) + .collect(Collectors.toList()); + + // 格式化时长 + formatDuration(historyVOList); + + // 构建分页结果 + PageDomain 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 getLearningHistoryById(String id) { + ResultDomain 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 getUserLearningStatistics(String userId, Date startTime, Date endTime) { + ResultDomain resultDomain = new ResultDomain<>(); + + try { + if (userId == null || userId.isEmpty()) { + resultDomain.fail("用户ID不能为空"); + return resultDomain; + } + + if (startTime == null || endTime == null) { + resultDomain.fail("开始时间和结束时间不能为空"); + return resultDomain; + } + + // 统计总体数据 + Map statistics = learningHistoryMapper.selectLearningStatisticsByTimeRange(userId, startTime, endTime); + + // 获取学习的资源列表 + List> 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 getUserLearningStatisticsByPeriod(String userId, String periodType) { + ResultDomain 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 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> getCurrentUserLearningHistories(TbLearningHistory filter) { + ResultDomain> 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 getCurrentUserLearningStatistics(String periodType) { + ResultDomain 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 deleteLearningHistory(String id) { + ResultDomain 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 batchDeleteLearningHistories(List ids) { + ResultDomain 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> getRecentLearningHistories(String userId, Integer limit) { + ResultDomain> resultDomain = new ResultDomain<>(); + + try { + if (userId == null || userId.isEmpty()) { + resultDomain.fail("用户ID不能为空"); + return resultDomain; + } + + if (limit == null || limit <= 0) { + limit = 10; + } + + List historyList = learningHistoryMapper.selectRecentByUserId(userId, limit); + List 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 recordResourceView(String userId, String resourceId, Integer duration) { + ResultDomain 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 recordCourseLearn(String userId, String courseId, String chapterId, String nodeId, Integer duration) { + ResultDomain 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 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 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; + } +} + diff --git a/schoolNewsServ/study/src/main/resources/mapper/LearningHistoryMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/LearningHistoryMapper.xml new file mode 100644 index 0000000..bd17aec --- /dev/null +++ b/schoolNewsServ/study/src/main/resources/mapper/LearningHistoryMapper.xml @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + deleted = 0 + + AND user_id = #{userID} + + + AND history_id = #{historyID} + + + AND resource_type = #{resourceType} + + + AND resource_id = #{resourceID} + + + AND course_id = #{courseID} + + + AND chapter_id = #{chapterID} + + + AND node_id = #{nodeID} + + + AND task_id = #{taskID} + + + AND device_type = #{deviceType} + + + + + + + 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 + ) + + + + + UPDATE tb_learning_history + + + end_time = #{endTime}, + + + duration = #{duration}, + + + start_progress = #{startProgress}, + + + end_progress = #{endProgress}, + + + device_type = #{deviceType}, + + + ip_address = #{ipAddress}, + + + WHERE history_id = #{historyID} AND deleted = 0 + + + + + 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 + + ( + #{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 + ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE tb_learning_history + SET deleted = 1, delete_time = NOW() + WHERE id = #{id} + + + + + UPDATE tb_learning_history + SET deleted = 1, delete_time = NOW() + WHERE id IN + + #{id} + + + + + + + + + + diff --git a/schoolNewsServ/study/src/main/resources/mapper/LearningStatisticsDetailMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/LearningStatisticsDetailMapper.xml new file mode 100644 index 0000000..76c1f4c --- /dev/null +++ b/schoolNewsServ/study/src/main/resources/mapper/LearningStatisticsDetailMapper.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + deleted = 0 + + AND user_id = #{userID} + + + AND stat_date = #{statDate} + + + AND resource_type = #{resourceType} + + + AND resource_id = #{resourceID} + + + AND course_id = #{courseID} + + + AND chapter_id = #{chapterID} + + + AND is_complete = #{isComplete} + + + + + + + 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 + ) + + + + + UPDATE tb_learning_statistics_detail + + + total_duration = #{totalDuration}, + + + learn_count = #{learnCount}, + + + is_complete = #{isComplete}, + + + complete_time = #{completeTime}, + + + updater = #{updater}, + + update_time = NOW() + + WHERE id = #{id} + + + + + 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() + + + + + + + + + + + + + + + + + + + + UPDATE tb_learning_statistics_detail + SET deleted = 1, delete_time = NOW() + WHERE id = #{id} + + + + + UPDATE tb_learning_statistics_detail + SET deleted = 1, delete_time = NOW() + WHERE id IN + + #{id} + + + + + + + diff --git a/schoolNewsWeb/src/apis/study/index.ts b/schoolNewsWeb/src/apis/study/index.ts index fee0df7..483e6a2 100644 --- a/schoolNewsWeb/src/apis/study/index.ts +++ b/schoolNewsWeb/src/apis/study/index.ts @@ -9,3 +9,4 @@ export { courseApi } from './course'; export { learningTaskApi } from './learning-task'; export { learningRecordApi } from './learning-record'; export { learningPlanApi } from './learning-plan'; +export { learningHistoryApi } from './learning-history'; diff --git a/schoolNewsWeb/src/apis/study/learning-history.ts b/schoolNewsWeb/src/apis/study/learning-history.ts new file mode 100644 index 0000000..91f644f --- /dev/null +++ b/schoolNewsWeb/src/apis/study/learning-history.ts @@ -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> + */ + async recordLearningHistory(learningHistory: TbLearningHistory): Promise> { + const response = await api.post('/study/history/record', learningHistory, { + showLoading: false // 禁用 loading 动画,避免影响用户体验 + } as any); + return response.data; + }, + + /** + * 批量记录学习历史 + * @param historyList 学习历史列表 + * @returns Promise> + */ + async batchRecordLearningHistory(historyList: TbLearningHistory[]): Promise> { + const response = await api.post('/study/history/batch-record', historyList); + return response.data; + }, + + /** + * 查询学习历史列表 + * @param filter 过滤条件 + * @returns Promise> + */ + async getLearningHistories(filter?: Partial): Promise> { + const response = await api.post('/study/history/list', filter || {}); + return response.data; + }, + + /** + * 分页查询学习历史 + * @param pageRequest 分页查询请求 + * @returns Promise>> + */ + async getLearningHistoriesPage(pageRequest: PageRequest): Promise>> { + const response = await api.post>('/study/history/page', pageRequest); + return response.data; + }, + + /** + * 根据ID查询学习历史 + * @param id 历史记录ID + * @returns Promise> + */ + async getLearningHistoryById(id: string): Promise> { + const response = await api.get(`/study/history/${id}`); + return response.data; + }, + + /** + * 获取当前用户的学习历史 + * @param filter 过滤条件 + * @returns Promise> + */ + async getCurrentUserLearningHistories(filter?: Partial): Promise> { + const response = await api.post('/study/history/my-histories', filter || {}); + return response.data; + }, + + /** + * 获取当前用户最近的学习历史 + * @param limit 限制数量(默认10) + * @returns Promise> + */ + async getRecentLearningHistories(limit = 10): Promise> { + const response = await api.get(`/study/history/recent?limit=${limit}`); + return response.data; + }, + + /** + * 获取用户学习统计(按时间范围) + * @param userId 用户ID + * @param startTime 开始时间(Date对象) + * @param endTime 结束时间(Date对象) + * @returns Promise> + */ + async getUserLearningStatistics(userId: string, startTime: Date, endTime: Date): Promise> { + const response = await api.get( + `/study/history/statistics?userId=${userId}&startTime=${startTime.getTime()}&endTime=${endTime.getTime()}` + ); + return response.data; + }, + + /** + * 获取用户学习统计(按周期) + * @param userId 用户ID + * @param periodType 周期类型(day/week/month) + * @returns Promise> + */ + async getUserLearningStatisticsByPeriod(userId: string, periodType: 'day' | 'week' | 'month'): Promise> { + const response = await api.get(`/study/history/statistics/${userId}/${periodType}`); + return response.data; + }, + + /** + * 获取当前用户的学习统计 + * @param periodType 周期类型(day/week/month) + * @returns Promise> + */ + async getCurrentUserLearningStatistics(periodType: 'day' | 'week' | 'month'): Promise> { + const response = await api.get(`/study/history/my-statistics/${periodType}`); + return response.data; + }, + + /** + * 删除学习历史 + * @param id 历史记录ID + * @returns Promise> + */ + async deleteLearningHistory(id: string): Promise> { + const response = await api.delete(`/study/history/${id}`); + return response.data; + }, + + /** + * 批量删除学习历史 + * @param ids 历史记录ID列表 + * @returns Promise> + */ + async batchDeleteLearningHistories(ids: string[]): Promise> { + const response = await api.delete('/study/history/batch', ids); + return response.data; + }, + + /** + * 简化记录方法 - 观看新闻/资源 + * @param userId 用户ID + * @param resourceId 资源ID + * @param duration 学习时长(秒) + * @returns Promise> + */ + async recordResourceView(userId: string, resourceId: string, duration: number): Promise> { + 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> + */ + async recordCourseLearn( + userId: string, + courseId: string, + chapterId?: string, + nodeId?: string, + duration?: number + ): Promise> { + 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> + */ + async health(): Promise> { + const response = await api.get('/study/history/health'); + return response.data; + } +}; + diff --git a/schoolNewsWeb/src/types/base/index.ts b/schoolNewsWeb/src/types/base/index.ts index 19757a5..943af25 100644 --- a/schoolNewsWeb/src/types/base/index.ts +++ b/schoolNewsWeb/src/types/base/index.ts @@ -25,9 +25,9 @@ export interface BaseDTO { */ export interface PageParam { /** 当前页码 */ - page: number; + pageNumber: number; /** 每页条数 */ - size: number; + pageSize: number; /** 总页数 */ totalPages?: number; diff --git a/schoolNewsWeb/src/types/study/index.ts b/schoolNewsWeb/src/types/study/index.ts index 85da408..966897d 100644 --- a/schoolNewsWeb/src/types/study/index.ts +++ b/schoolNewsWeb/src/types/study/index.ts @@ -331,3 +331,187 @@ export interface LearningRecordStatistics { /** 完成任务数 */ 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[]; +} \ No newline at end of file diff --git a/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue b/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue index 615b54c..03b1d50 100644 --- a/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/achievement/AchievementManagementView.vue @@ -492,7 +492,7 @@ async function handleViewUsers(row: Achievement) { try { achieversLoading.value = true; const result = await achievementApi.getRecentAchievers( - { page: 1, size: 100 }, + { pageNumber: 1, pageSize: 100 }, { achievementID: row.achievementID } ); achieverList.value = result.dataList || []; diff --git a/schoolNewsWeb/src/views/admin/manage/crontab/LogManagementView.vue b/schoolNewsWeb/src/views/admin/manage/crontab/LogManagementView.vue index dde9f4b..584ee37 100644 --- a/schoolNewsWeb/src/views/admin/manage/crontab/LogManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/crontab/LogManagementView.vue @@ -103,8 +103,8 @@
({ - page: 1, - size: 20 + pageNumber: 1, + pageSize: 20 }); // 对话框状态 @@ -277,7 +277,7 @@ const loadLogList = async () => { // 搜索 const handleSearch = () => { - pageParam.page = 1; + pageParam.pageNumber = 1; loadLogList(); }; @@ -286,19 +286,19 @@ const handleReset = () => { searchForm.taskName = ''; searchForm.taskGroup = ''; searchForm.executeStatus = undefined; - pageParam.page = 1; + pageParam.pageNumber = 1; loadLogList(); }; // 分页变化 const handlePageChange = (page: number) => { - pageParam.page = page; + pageParam.pageNumber = page; loadLogList(); }; const handleSizeChange = (size: number) => { - pageParam.size = size; - pageParam.page = 1; + pageParam.pageSize = size; + pageParam.pageNumber = 1; loadLogList(); }; diff --git a/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue b/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue index 5ee324a..df910e5 100644 --- a/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue +++ b/schoolNewsWeb/src/views/admin/manage/crontab/NewsCrawlerView.vue @@ -155,8 +155,8 @@
({ - page: 1, - size: 9 + pageNumber: 1, + pageSize: 10 }); // 对话框状态 @@ -341,7 +341,7 @@ const loadCrawlerList = async () => { // 搜索 const handleSearch = () => { - pageParam.page = 1; + pageParam.pageNumber = 1; loadCrawlerList(); }; @@ -349,19 +349,19 @@ const handleSearch = () => { const handleReset = () => { searchForm.taskName = ''; searchForm.status = undefined; - pageParam.page = 1; + pageParam.pageNumber = 1; loadCrawlerList(); }; // 分页变化 const handlePageChange = (page: number) => { - pageParam.page = page; + pageParam.pageNumber = page; loadCrawlerList(); }; const handleSizeChange = (size: number) => { - pageParam.size = size; - pageParam.page = 1; + pageParam.pageSize = size; + pageParam.pageNumber = 1; loadCrawlerList(); }; diff --git a/schoolNewsWeb/src/views/admin/manage/crontab/TaskManagementView.vue b/schoolNewsWeb/src/views/admin/manage/crontab/TaskManagementView.vue index 34c6386..beafd66 100644 --- a/schoolNewsWeb/src/views/admin/manage/crontab/TaskManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/crontab/TaskManagementView.vue @@ -128,8 +128,8 @@
({ - page: 1, - size: 20 + pageNumber: 1, + pageSize: 20 }); // 对话框状态 @@ -316,7 +316,7 @@ const loadTaskList = async () => { // 搜索 const handleSearch = () => { - pageParam.page = 1; + pageParam.pageNumber = 1; loadTaskList(); }; @@ -325,19 +325,19 @@ const handleReset = () => { searchForm.taskName = ''; searchForm.taskGroup = ''; searchForm.status = undefined; - pageParam.page = 1; + pageParam.pageNumber = 1; loadTaskList(); }; // 分页变化 const handlePageChange = (page: number) => { - pageParam.page = page; + pageParam.pageNumber = page; loadTaskList(); }; const handleSizeChange = (size: number) => { - pageParam.size = size; - pageParam.page = 1; + pageParam.pageSize = size; + pageParam.pageNumber = 1; loadTaskList(); }; diff --git a/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue b/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue index b4cc75f..e479ce7 100644 --- a/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue +++ b/schoolNewsWeb/src/views/admin/manage/resource/ArticleManagementView.vue @@ -41,8 +41,8 @@ ({ - page: 1, - size: 10 + pageNumber: 1, + pageSize: 10 }); const filter = ref({ keyword: searchKeyword.value @@ -232,12 +232,12 @@ function getActionButtonText(status: number) { } function handleSizeChange(val: number) { - pageParam.value.size = val; + pageParam.value.pageSize = val; loadArticles(); } function handleCurrentChange(val: number) { - pageParam.value.page = val; + pageParam.value.pageNumber = val; loadArticles(); } diff --git a/schoolNewsWeb/src/views/article/ArticleShowView.vue b/schoolNewsWeb/src/views/article/ArticleShowView.vue index 22592d2..a2eb67f 100644 --- a/schoolNewsWeb/src/views/article/ArticleShowView.vue +++ b/schoolNewsWeb/src/views/article/ArticleShowView.vue @@ -98,9 +98,9 @@ import { useRouter, useRoute } from 'vue-router'; import { ElMessage } from 'element-plus'; import { ArrowLeft } from '@element-plus/icons-vue'; import { resourceApi } from '@/apis/resource'; -import { learningRecordApi } from '@/apis/study'; +import { learningRecordApi, learningHistoryApi } from '@/apis/study'; import { useStore } from 'vuex'; -import type { ResourceCategory, Resource, ResourceVO, LearningRecord } from '@/types'; +import type { ResourceCategory, Resource, ResourceVO, LearningRecord, TbLearningHistory } from '@/types'; defineOptions({ name: 'ArticleShowView' @@ -154,6 +154,11 @@ const totalVideos = ref(0); // 视频总数 const completedVideos = ref>(new Set()); // 已完成的视频索引 const userInfo = computed(() => store.getters['auth/user']); +// 学习历史记录相关 +const learningHistory = ref(null); +const historyStartTime = ref(0); +const historyTimer = ref(null); + // 当前显示的文章数据 const currentArticleData = computed(() => { // Dialog 模式使用传入的 articleData @@ -182,13 +187,18 @@ onMounted(() => { // 如果传入了 articleData,则不需要从路由加载 if (props.articleData && Object.keys(props.articleData).length > 0) { loadedArticleData.value = props.articleData; - // 即使传入了数据,也要监听视频(如果有taskId) - if (taskId || route.query.taskId) { - const resourceID = props.articleData.resourceID; - if (resourceID) { + + const resourceID = props.articleData.resourceID; + if (resourceID) { + // 创建学习历史记录(每次进入都创建新记录) + createHistoryRecord(resourceID); + + // 如果有taskId,还要创建学习记录 + if (taskId || route.query.taskId) { loadLearningRecord(resourceID); } } + // 初始化视频监听 nextTick().then(() => { setTimeout(() => { @@ -215,12 +225,34 @@ onBeforeUnmount(() => { saveLearningProgress(); } stopLearningTimer(); + + // 保存学习历史记录 + if (learningHistory.value) { + saveHistoryRecord(); + } + stopHistoryTimer(); }); // 监听 articleData 变化(用于 ResourceArticle 切换文章) -watch(() => props.articleData, (newData) => { - if (!props.asDialog && newData && Object.keys(newData).length > 0) { - loadedArticleData.value = newData; +watch(() => props.articleData, async (newData, oldData) => { + if (!props.asDialog) { + // 如果从有数据变成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(() => { setTimeout(() => { @@ -243,6 +275,9 @@ async function loadArticle(resourceID: string) { // 增加浏览次数 await resourceApi.incrementViewCount(resourceID); + // 创建学习历史记录(每次进入都创建新记录) + await createHistoryRecord(resourceID); + // 等待 DOM 更新后监听视频(增加延迟确保 DOM 完全渲染) await nextTick(); 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) function formatDateSimple(date: string | Date): string { if (!date) return ''; @@ -486,6 +596,12 @@ function formatDateSimple(date: string | Date): string { // 关闭处理 function handleClose() { + // 非Dialog模式下关闭时保存学习历史 + if (!props.asDialog && learningHistory.value) { + saveHistoryRecord(); + stopHistoryTimer(); + } + if (props.asDialog) { visible.value = false; } diff --git a/schoolNewsWeb/src/views/course/components/CourseLearning.vue b/schoolNewsWeb/src/views/course/components/CourseLearning.vue index 1bcabf1..fe26992 100644 --- a/schoolNewsWeb/src/views/course/components/CourseLearning.vue +++ b/schoolNewsWeb/src/views/course/components/CourseLearning.vue @@ -221,11 +221,11 @@ import { } from '@element-plus/icons-vue'; import { ArticleShowView } from '@/views/article'; import { courseApi } from '@/apis/study'; -import { learningRecordApi } from '@/apis/study'; +import { learningRecordApi, learningHistoryApi } from '@/apis/study'; import { resourceApi } from '@/apis/resource'; import { useStore } from 'vuex'; import { FILE_DOWNLOAD_URL } from '@/config'; -import type { CourseVO, LearningRecord } from '@/types'; +import type { CourseVO, LearningRecord, TbLearningHistory } from '@/types'; interface Props { courseId: string; @@ -269,6 +269,11 @@ const previousNodeKey = ref(null); const totalRichTextVideos = ref(0); const completedRichTextVideos = ref>(new Set()); +// 学习历史记录 +const learningHistory = ref(null); +const historyStartTime = ref(0); +const historyTimer = ref(null); + // 当前节点 const currentNode = computed(() => { if (!courseVO.value || !courseVO.value.courseChapters) return null; @@ -346,9 +351,18 @@ watch(() => [props.chapterIndex, props.nodeIndex], () => { loadNodeContent(); }, { immediate: true }); -watch(currentNode, () => { +watch(currentNode, async () => { + // 保存上一个节点的学习历史记录 + if (learningHistory.value) { + await saveHistoryRecord(); + stopHistoryTimer(); + } + if (currentNode.value) { loadNodeContent(); + + // 为新节点创建学习历史记录 + await createHistoryRecord(); } }); @@ -359,6 +373,12 @@ onMounted(() => { onBeforeUnmount(() => { stopLearningTimer(); 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() { stopLearningTimer(); saveLearningProgress(); + + // 保存学习历史记录 + if (learningHistory.value) { + saveHistoryRecord(); + } + stopHistoryTimer(); + emit('back'); } diff --git a/schoolNewsWeb/src/views/resource-center/ResourceCenterView.vue b/schoolNewsWeb/src/views/resource-center/ResourceCenterView.vue index 56ff613..7a47f4d 100644 --- a/schoolNewsWeb/src/views/resource-center/ResourceCenterView.vue +++ b/schoolNewsWeb/src/views/resource-center/ResourceCenterView.vue @@ -15,7 +15,7 @@ @category-change="handleCategoryChange" />
({ - page: 1, - size: 6 + pageNumber: 1, + pageSize: 6 }); onMounted(() => {