From 9adc0c20582dd2614c6fe66c51497c37f58df186 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Fri, 14 Nov 2025 18:31:39 +0800 Subject: [PATCH] =?UTF-8?q?overview=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 +- .../api/news/resource/ResourceService.java | 3 + .../xyzh/api/study/course/CourseService.java | 9 + .../api/study/task/LearningTaskService.java | 9 + .../auth/filter/JwtAuthenticationFilter.java | 18 +- .../java/org/xyzh/common/dto/BaseDTO.java | 20 + .../common/redis/service/RedisService.java | 12 + .../java/org/xyzh/common/utils/TimeUtils.java | 25 ++ .../service/impl/NCResourceServiceImpl.java | 15 + .../main/resources/mapper/ResourceMapper.xml | 6 + .../service/impl/SCCourseServiceImpl.java | 29 +- .../impl/SCLearningTaskServiceImpl.java | 21 +- schoolNewsServ/system/pom.xml | 10 + .../controller/SystemOverviewController.java | 345 +++++++++++++++--- .../org/xyzh/system/mapper/UserMapper.java | 2 +- .../user/service/impl/SysUserServiceImpl.java | 4 +- .../src/main/resources/mapper/UserMapper.xml | 22 +- schoolNewsWeb/src/apis/system/overview.ts | 22 +- schoolNewsWeb/src/assets/imgs/overview-pv.svg | 5 + .../src/assets/imgs/overview-resource.svg | 5 + .../src/assets/imgs/overview-user.svg | 5 + schoolNewsWeb/src/assets/imgs/overview-uv.svg | 6 + schoolNewsWeb/src/utils/permission.ts | 9 +- .../admin/overview/SystemOverviewView.vue | 296 +++++++++------ 24 files changed, 723 insertions(+), 178 deletions(-) create mode 100644 schoolNewsWeb/src/assets/imgs/overview-pv.svg create mode 100644 schoolNewsWeb/src/assets/imgs/overview-resource.svg create mode 100644 schoolNewsWeb/src/assets/imgs/overview-user.svg create mode 100644 schoolNewsWeb/src/assets/imgs/overview-uv.svg diff --git a/.vscode/settings.json b/.vscode/settings.json index 3a8ebfb..e736c01 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,5 +22,6 @@ "path": "cmd.exe", "args": ["/k", "chcp 65001"] } - } + }, + "java.debug.settings.onBuildFailureProceed": true } \ No newline at end of file diff --git a/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java b/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java index f7312ac..d887108 100644 --- a/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java +++ b/schoolNewsServ/api/api-news/src/main/java/org/xyzh/api/news/resource/ResourceService.java @@ -183,4 +183,7 @@ public interface ResourceService { * @since 2025-10-15 */ ResultDomain searchResources(String keyword, String tagID, Integer status); + + ResultDomain getResourceCount(TbResource filter); + } diff --git a/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java index 0ee31fa..05c036b 100644 --- a/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java +++ b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/course/CourseService.java @@ -190,4 +190,13 @@ public interface CourseService { * @since 2025-10-28 */ ResultDomain getCourseProgress(String courseID); + + /** + * @description 获取课程数量 + * @param filter 过滤条件 + * @return ResultDomain 课程数量 + * @author yslg + * @since 2025-11-14 + */ + ResultDomain getCourseCount(TbCourse filter); } diff --git a/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/task/LearningTaskService.java b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/task/LearningTaskService.java index 7e97e3b..63eb5a1 100644 --- a/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/task/LearningTaskService.java +++ b/schoolNewsServ/api/api-study/src/main/java/org/xyzh/api/study/task/LearningTaskService.java @@ -237,4 +237,13 @@ public interface LearningTaskService { * @since 2025-10-30 */ ResultDomain> getTaskStatisticsRankings(String taskID); + + /** + * @description 获取学习任务数量 + * @param filter 过滤条件 + * @return ResultDomain 学习任务数量 + * @author yslg + * @since 2025-11-14 + */ + ResultDomain getLearningTaskCount(TbLearningTask filter); } diff --git a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/filter/JwtAuthenticationFilter.java b/schoolNewsServ/auth/src/main/java/org/xyzh/auth/filter/JwtAuthenticationFilter.java index 388260a..cbf45e3 100644 --- a/schoolNewsServ/auth/src/main/java/org/xyzh/auth/filter/JwtAuthenticationFilter.java +++ b/schoolNewsServ/auth/src/main/java/org/xyzh/auth/filter/JwtAuthenticationFilter.java @@ -18,6 +18,8 @@ import org.xyzh.common.core.domain.LoginDomain; import org.xyzh.common.redis.service.RedisService; import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,6 +36,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String REDIS_LOGIN_PREFIX = "login:token:"; + /** + * @description UV统计key前缀,例如 stat:uv:20251114 + */ + private static final String REDIS_UV_PREFIX = "stat:uv:"; + private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class); @Autowired @@ -89,8 +96,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); - + logger.debug("用户认证成功(从缓存),userId: {}", userId); + + // 记录UV:以用户ID为维度,按天去重 + try { + String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); + String uvKey = REDIS_UV_PREFIX + today; + redisService.sAdd(uvKey, userId); + } catch (Exception e) { + logger.warn("记录UV失败, userId={}: {}", userId, e.getMessage()); + } } else { logger.warn("Redis缓存中未找到用户登录信息,userId: {}, 可能已过期或未登录", userId); } diff --git a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java index a654949..96940e2 100644 --- a/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java +++ b/schoolNewsServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java @@ -28,6 +28,10 @@ public class BaseDTO implements Serializable{ */ private Date createTime; + private Date startTime; + + private Date endTime; + /** * @description 更新时间 * @author yslg @@ -94,6 +98,22 @@ public class BaseDTO implements Serializable{ this.createTime = createTime; } + 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; + } + /** * @description 获取更新时间 * @return Date 更新时间 diff --git a/schoolNewsServ/common/common-redis/src/main/java/org/xyzh/common/redis/service/RedisService.java b/schoolNewsServ/common/common-redis/src/main/java/org/xyzh/common/redis/service/RedisService.java index ef844a1..4c6bd66 100644 --- a/schoolNewsServ/common/common-redis/src/main/java/org/xyzh/common/redis/service/RedisService.java +++ b/schoolNewsServ/common/common-redis/src/main/java/org/xyzh/common/redis/service/RedisService.java @@ -283,6 +283,18 @@ public class RedisService { return redisTemplate.opsForSet().members(key); } + /** + * @description 获取Set集合大小(等价于Redis的SCARD) + * @param key String 键 + * @return long 集合元素数量 + * @author yslg + * @since 2025-11-14 + */ + public long sCard(String key) { + Long size = redisTemplate.opsForSet().size(key); + return size != null ? size : 0L; + } + /** * @description ZSet操作-添加元素 * @param key String 键 diff --git a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/TimeUtils.java b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/TimeUtils.java index b3a2e37..2266a5e 100644 --- a/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/TimeUtils.java +++ b/schoolNewsServ/common/common-util/src/main/java/org/xyzh/common/utils/TimeUtils.java @@ -294,4 +294,29 @@ public class TimeUtils { return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); } + /** + * @description 获取昨天的开始时间 + * @return java.util.Date对象 + * @author yslg + * @since 2025-11-14 + */ + public static Date getStartTimeOfYesterday() { + LocalDate yesterday = LocalDate.now().minusDays(1); + return Date.from( + yesterday.atStartOfDay(ZoneId.systemDefault()).toInstant() + ); + } + + /** + * @description 获取昨天的结束时间 + * @return java.util.Date对象 + * @author yslg + * @since 2025-11-14 + */ + public static Date getEndTimeOfYesterday() { + LocalDate today = LocalDate.now(); + return Date.from( + today.atStartOfDay(ZoneId.systemDefault()).minusNanos(1).toInstant() + ); + } } diff --git a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java index c638d3b..6a222e3 100644 --- a/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java +++ b/schoolNewsServ/news/src/main/java/org/xyzh/news/service/impl/NCResourceServiceImpl.java @@ -754,4 +754,19 @@ public class NCResourceServiceImpl implements ResourceService { return resultDomain; } } + + @Override + public ResultDomain getResourceCount(TbResource filter) { + ResultDomain resultDomain = new ResultDomain<>(); + try { + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + long count = resourceMapper.countResources(filter, userDeptRoles); + resultDomain.success("获取资源总数成功", (int)count); + return resultDomain; + } catch (Exception e) { + logger.error("获取资源总数异常: {}", e.getMessage(), e); + resultDomain.fail("获取资源总数失败: " + e.getMessage()); + return resultDomain; + } + } } diff --git a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml index 805cb5e..9ff64a0 100644 --- a/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml +++ b/schoolNewsServ/news/src/main/resources/mapper/ResourceMapper.xml @@ -387,6 +387,12 @@ AND r.is_banner = #{filter.isBanner} + + AND r.publish_time >= #{filter.startTime} + + + AND r.publish_time <= #{filter.endTime} + diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java index a617d25..8391c0e 100644 --- a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCCourseServiceImpl.java @@ -550,14 +550,14 @@ public class SCCourseServiceImpl implements SCCourseService { @Override public ResultDomain getCourseProgress(String courseID) { ResultDomain resultDomain = new ResultDomain<>(); - + // 获取当前用户 TbSysUser user = LoginUtil.getCurrentUser(); if (user == null) { resultDomain.fail("用户未登录"); return resultDomain; } - + // 查询课程 TbCourse course = courseMapper.selectByCourseId(courseID); if (course == null) { @@ -572,25 +572,25 @@ public class SCCourseServiceImpl implements SCCourseService { TbCourseChapter filter = new TbCourseChapter(); filter.setCourseID(courseID); List chapters = courseChapterMapper.selectCourseChapters(filter); - + // 查询并构建章节及节点结构(带进度) if (!chapters.isEmpty()) { List chapterIDs = chapters.stream() .map(TbCourseChapter::getChapterID) .collect(Collectors.toList()); - + // 查询带进度的节点(传入用户ID) List nodesWithProgress = courseNodeMapper.selectNodesProgress(chapterIDs, user.getID()); - + // 转换章节为CourseItemVO列表 List chapterVOs = chapters.stream() .map(CourseItemVO::fromChapter) .collect(Collectors.toList()); - + // 按章节ID分组节点 Map> nodesMap = nodesWithProgress.stream() .collect(Collectors.groupingBy(CourseItemVO::getChapterID)); - + // 设置章节列表和章节节点映射 courseItemVO.setChapters(chapterVOs); courseItemVO.setChapterNodes(nodesMap); @@ -599,4 +599,19 @@ public class SCCourseServiceImpl implements SCCourseService { resultDomain.success("获取课程进度成功", courseItemVO); return resultDomain; } + + @Override + public ResultDomain getCourseCount(TbCourse filter) { + ResultDomain resultDomain = new ResultDomain<>(); + try { + // 获取当前用户的部门角色 + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + long count = courseMapper.countCourses(filter, userDeptRoles); + resultDomain.success("获取课程数量成功", (int) count); + } catch (Exception e) { + logger.error("获取课程数量失败", e); + resultDomain.fail("获取课程数量失败: " + e.getMessage()); + } + return resultDomain; + } } diff --git a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningTaskServiceImpl.java b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningTaskServiceImpl.java index 56e27d1..09d715d 100644 --- a/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningTaskServiceImpl.java +++ b/schoolNewsServ/study/src/main/java/org/xyzh/study/service/impl/SCLearningTaskServiceImpl.java @@ -796,15 +796,15 @@ public class SCLearningTaskServiceImpl implements LearningTaskService { ResultDomain> resultDomain = new ResultDomain<>(); try { Map rankingsData = new HashMap<>(); - + // 获取完成时间排行榜(前10名) List> completionTimeRanking = taskUserMapper.getCompletionTimeRanking(taskID); rankingsData.put("completionTimeRanking", completionTimeRanking); - + // 获取学习时长排行榜(前10名) List> durationRanking = taskUserMapper.getStudyDurationRanking(taskID); rankingsData.put("durationRanking", durationRanking); - + resultDomain.success("获取任务排行榜数据成功", rankingsData); } catch (Exception e) { logger.error("获取任务排行榜数据失败", e); @@ -812,4 +812,19 @@ public class SCLearningTaskServiceImpl implements LearningTaskService { } return resultDomain; } + + @Override + public ResultDomain getLearningTaskCount(TbLearningTask filter) { + ResultDomain resultDomain = new ResultDomain<>(); + try { + // 获取当前用户的部门角色 + List userDeptRoles = LoginUtil.getCurrentDeptRole(); + long count = learningTaskMapper.countLearningTasks(filter, userDeptRoles); + resultDomain.success("获取学习任务数量成功", (int) count); + } catch (Exception e) { + logger.error("获取学习任务数量失败", e); + resultDomain.fail("获取学习任务数量失败: " + e.getMessage()); + } + return resultDomain; + } } diff --git a/schoolNewsServ/system/pom.xml b/schoolNewsServ/system/pom.xml index c09f407..23055be 100644 --- a/schoolNewsServ/system/pom.xml +++ b/schoolNewsServ/system/pom.xml @@ -25,6 +25,16 @@ api-system 1.0.0 + + org.xyzh + api-news + 1.0.0 + + + org.xyzh + api-study + 1.0.0 + diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/SystemOverviewController.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/SystemOverviewController.java index 5a2e819..856cb2a 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/SystemOverviewController.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/controller/SystemOverviewController.java @@ -2,17 +2,34 @@ package org.xyzh.system.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.xyzh.api.news.resource.ResourceService; +import org.xyzh.api.study.course.CourseService; +import org.xyzh.api.study.task.LearningTaskService; import org.xyzh.common.core.domain.ResultDomain; - -import java.time.LocalDate; +import org.xyzh.common.dto.resource.TbResource; +import org.xyzh.common.dto.study.TbCourse; +import org.xyzh.common.dto.study.TbLearningTask; +import org.xyzh.common.dto.system.TbSysVisitStatistics; +import org.xyzh.common.dto.user.TbSysUser; +import org.xyzh.common.utils.TimeUtils; +import org.xyzh.common.vo.UserDeptRoleVO; +import org.xyzh.common.redis.service.RedisService; +import org.xyzh.system.mapper.SysVisitStatisticsMapper; +import org.xyzh.system.mapper.UserMapper; +import org.xyzh.system.utils.LoginUtil; import java.util.HashMap; import java.util.List; import java.util.Map; - +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Date; /** * @description 系统总览控制器 * @filename SystemOverviewController.java @@ -25,53 +42,193 @@ import java.util.Map; public class SystemOverviewController { private static final Logger logger = LoggerFactory.getLogger(SystemOverviewController.class); + @Autowired + private UserMapper userMapper; + + @Autowired + private SysVisitStatisticsMapper sysVisitStatisticsMapper; + + @Autowired + private ResourceService resourceService; + + @Autowired + private RedisService redisService; + + @Autowired + private CourseService courseService; + + @Autowired + private LearningTaskService learningTaskService; + /** * 获取系统总览数据统计 */ @GetMapping("/statistics") public ResultDomain> getSystemStatistics() { - // TODO: 后续接入真实统计数据(用户表、资源表、访问统计表等) ResultDomain> result = new ResultDomain<>(); Map data = new HashMap<>(); + List voList = LoginUtil.getCurrentDeptRole(); + // 顶部统计卡片:总用户数、总资源数、今日PV、今日UV + // 1. 总用户数:直接统计用户表数量(后续可增加状态/逻辑删除过滤) + try { + TbSysUser filterUser = new TbSysUser(); - // 顶部统计卡片:总用户数、总资源数、今日访问量、活跃用户 - data.put("totalUsers", 1234); - data.put("totalUsersChange", "+12%"); + long totalUsers = userMapper.countDeptUser(voList.get(0).getDeptID(), filterUser); + data.put("totalUsers", totalUsers); + filterUser.setEndTime(TimeUtils.getEndTimeOfYesterday()); + long yesterdayUsers = userMapper.countDeptUser(voList.get(0).getDeptID(), filterUser); + // 目前未记录按天的用户总量,这里先返回"+0%",后续可根据用户注册表统计 + data.put("totalUsersChange", formatPercentChange(totalUsers, yesterdayUsers)); - data.put("totalResources", 5678); - data.put("totalResourcesChange", "+8%"); + // 2. 总资源数:当前总量 & 与昨日总量对比 + long totalResources = 0L; + long yesterdayTotalResources = 0L; - data.put("todayVisits", 892); - data.put("todayVisitsChange", "+15%"); + // 今天总资源数(不带时间过滤) + ResultDomain totalResResult = resourceService.getResourceCount(new TbResource()); + if (totalResResult.isSuccess() && totalResResult.getData() != null) { + totalResources = totalResResult.getData(); + } - data.put("activeUsers", 456); - data.put("activeUsersChange", "+5%"); + // 昨日结束时的资源总数:publish_time <= 昨日结束时间 + TbResource yesterdayFilter = new TbResource(); + yesterdayFilter.setStartTime(TimeUtils.getStartTimeOfYesterday()); + yesterdayFilter.setEndTime(TimeUtils.getEndTimeOfYesterday()); + ResultDomain yesterdayResResult = resourceService.getResourceCount(yesterdayFilter); + if (yesterdayResResult.isSuccess() && yesterdayResResult.getData() != null) { + yesterdayTotalResources = yesterdayResResult.getData(); + } + + data.put("totalResources", totalResources); + data.put("totalResourcesChange", formatPercentChange(totalResources, yesterdayTotalResources)); + } catch (Exception e) { + logger.error("统计总用户数失败", e); + data.put("totalUsers", 0); + } + + // 3. 今日访问量 & 活跃用户:优先从Redis读取今日数据,必要时从系统访问统计表回退 + try { + LocalDate todayDate = LocalDate.now(); + LocalDate yesterdayDate = todayDate.minusDays(1); + + String todayKeySuffix = todayDate.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE); + String pvKey = "stat:pv:" + todayKeySuffix; + String uvKey = "stat:uv:" + todayKeySuffix; + + String yesterdayKeySuffix = yesterdayDate.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE); + String yesterdayPvKey = "stat:pv:" + yesterdayKeySuffix; + String yesterdayUvKey = "stat:uv:" + yesterdayKeySuffix; + + long todayPv = 0L; + long todayUv = 0L; + long yesterdayPv = 0L; + long yesterdayUv = 0L; + + try { + Object pvObj = redisService.get(pvKey); + if (pvObj instanceof Number) { + todayPv = ((Number) pvObj).longValue(); + } + } catch (Exception e) { + logger.warn("从Redis读取PV失败: {}", e.getMessage()); + } + + try { + todayUv = redisService.sCard(uvKey); + } catch (Exception e) { + logger.warn("从Redis读取UV失败: {}", e.getMessage()); + } + + try { + Object yesterdayPvObj = redisService.get(yesterdayPvKey); + if (yesterdayPvObj instanceof Number) { + yesterdayPv = ((Number) yesterdayPvObj).longValue(); + } + } catch (Exception e) { + logger.warn("从Redis读取昨日PV失败: {}", e.getMessage()); + } + + try { + yesterdayUv = redisService.sCard(yesterdayUvKey); + } catch (Exception e) { + logger.warn("从Redis读取昨日UV失败: {}", e.getMessage()); + } + + // 从统计表中获取活跃用户,作为顶部卡片展示 + // 顶部卡片:今日PV / 今日UV + data.put("totalPv", todayPv); + data.put("totalPvChange", formatPercentChange(todayPv, yesterdayPv)); + + data.put("totalUv", todayUv); + data.put("totalUvChange", formatPercentChange(todayUv, yesterdayUv)); + } catch (Exception e) { + logger.error("统计访问量/活跃用户失败", e); + data.put("totalPv", 0L); + data.put("totalPvChange", "+0%"); + data.put("totalUv", 0L); + data.put("totalUvChange", "+0%"); + } result.success("获取系统总览统计成功", data); return result; } /** - * 获取活跃用户图表数据 + * 获取活跃用户图表数据(PV和UV) */ @GetMapping("/active-users") public ResultDomain> getActiveUsersChart( @RequestParam(required = true, name = "start") String start, @RequestParam(required = true, name = "end") String end) { - // TODO: 后续根据days参数(7/30天)查询真实活跃用户统计 ResultDomain> result = new ResultDomain<>(); Map data = new HashMap<>(); - // 默认展示7天 LocalDate startDate = LocalDate.parse(start); LocalDate endDate = LocalDate.parse(end); - long days = startDate.until(endDate).getDays(); + LocalDate today = LocalDate.now(); - // X轴:周一 ~ 周日(示例) - data.put("labels", List.of("周一", "周二", "周三", "周四", "周五", "周六", "周日")); - // Y轴数据:活跃用户数量(示例数据) - data.put("values", List.of(120, 200, 150, 80, 70, 110, 130)); + try { + // 按天遍历统计区间内的PV和UV数据 + List labels = new ArrayList<>(); + List pvValues = new ArrayList<>(); + List uvValues = new ArrayList<>(); - result.success("获取活跃用户图表数据成功", data); + LocalDate cursor = startDate; + while (!cursor.isAfter(endDate)) { + labels.add(formatDayLabel(cursor)); + + int pv = 0; + int uv = 0; + try { + String keySuffix = cursor.format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE); + String pvKey = "stat:pv:" + keySuffix; + String uvKey = "stat:uv:" + keySuffix; + + Object pvObj = redisService.get(pvKey); + if (pvObj instanceof Number) { + pv = ((Number) pvObj).intValue(); + } + uv = (int) redisService.sCard(uvKey); + } catch (Exception e) { + logger.warn("从Redis读取今日PV/UV失败,尝试从数据库读取: {}", e.getMessage()); + } + + pvValues.add(pv); + uvValues.add(uv); + + cursor = cursor.plusDays(1); + } + + data.put("labels", labels); + data.put("pvValues", pvValues); + data.put("uvValues", uvValues); + } catch (Exception e) { + logger.error("获取PV/UV图表数据失败", e); + data.put("labels", List.of()); + data.put("pvValues", List.of()); + data.put("uvValues", List.of()); + } + + result.success("获取PV/UV图表数据成功", data); return result; } @@ -80,21 +237,36 @@ public class SystemOverviewController { */ @GetMapping("/resource-category-stats") public ResultDomain> getResourceCategoryStats() { - // TODO: 后续从资源表统计各类型资源数量 ResultDomain> result = new ResultDomain<>(); - Map data = new HashMap<>(); + try { + Map data = new HashMap<>(); - // 饼图数据:名称 + 数量 - List> categories = List.of( - createCategory("文章", 1048), - createCategory("视频", 735), - createCategory("音频", 580), - createCategory("课程", 484), - createCategory("其他", 300) - ); + // 获取各类型资源数量 + ResultDomain resourceCountResult = resourceService.getResourceCount(new TbResource()); + ResultDomain courseCountResult = courseService.getCourseCount(new TbCourse()); + ResultDomain taskCountResult = learningTaskService.getLearningTaskCount(new TbLearningTask()); - data.put("items", categories); - result.success("获取资源分类统计成功", data); + // 获取数量,失败时默认为0 + int resourceCount = (resourceCountResult.isSuccess() && resourceCountResult.getData() != null) + ? resourceCountResult.getData() : 0; + int courseCount = (courseCountResult.isSuccess() && courseCountResult.getData() != null) + ? courseCountResult.getData() : 0; + int taskCount = (taskCountResult.isSuccess() && taskCountResult.getData() != null) + ? taskCountResult.getData() : 0; + + // 饼图数据:名称 + 数量 + List> categories = List.of( + createCategory("文章", resourceCount), + createCategory("课程", courseCount), + createCategory("学习任务", taskCount) + ); + + data.put("items", categories); + result.success("获取资源分类统计成功", data); + } catch (Exception e) { + logger.error("获取资源分类统计失败", e); + result.fail("获取资源分类统计失败: " + e.getMessage()); + } return result; } @@ -103,16 +275,76 @@ public class SystemOverviewController { */ @GetMapping("/today-visits") public ResultDomain> getTodayVisits() { - // TODO: 后续接入访问日志/统计表,计算今日指标 ResultDomain> result = new ResultDomain<>(); Map data = new HashMap<>(); + try { + String todayKeySuffix = LocalDate.now().format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE); + String pvKey = "stat:pv:" + todayKeySuffix; + String uvKey = "stat:uv:" + todayKeySuffix; - data.put("uv", 892); - data.put("pv", 3456); - data.put("avgVisitDuration", "5分32秒"); - data.put("bounceRate", "35.6%"); + long uvFromRedis = 0L; + long pvFromRedis = 0L; - result.success("获取今日访问量统计成功", data); + try { + pvFromRedis = 0L; + Object pvObj = redisService.get(pvKey); + if (pvObj instanceof Number) { + pvFromRedis = ((Number) pvObj).longValue(); + } + } catch (Exception e) { + logger.warn("从Redis读取PV失败: {}", e.getMessage()); + } + + try { + uvFromRedis = redisService.sCard(uvKey); + } catch (Exception e) { + logger.warn("从Redis读取UV失败: {}", e.getMessage()); + } + + TbSysVisitStatistics filter = new TbSysVisitStatistics(); + filter.setStatDate(new Date()); + List list = sysVisitStatisticsMapper.selectSysVisitStatistics(filter); + TbSysVisitStatistics todayStat = (list != null && !list.isEmpty()) ? list.get(0) : null; + + int uvFromDb = todayStat != null && todayStat.getUniqueVisitors() != null ? todayStat.getUniqueVisitors() : 0; + int pvFromDb = todayStat != null && todayStat.getPageViews() != null ? todayStat.getPageViews() : 0; + int avgDurationSec = todayStat != null && todayStat.getAvgVisitDuration() != null ? todayStat.getAvgVisitDuration() : 0; + + // 优先使用Redis中的实时数据,若没有则回退到统计表 + data.put("uv", uvFromRedis > 0 ? uvFromRedis : uvFromDb); + data.put("pv", pvFromRedis > 0 ? pvFromRedis : pvFromDb); + data.put("avgVisitDuration", formatDuration(avgDurationSec)); + // 当前表中没有跳出率字段,这里先返回"-",后续有字段后再计算 + data.put("bounceRate", "-"); + + result.success("获取今日访问量统计成功", data); + return result; + } catch (Exception e) { + logger.error("获取今日访问量统计失败", e); + data.put("uv", 0); + data.put("pv", 0); + data.put("avgVisitDuration", "0秒"); + data.put("bounceRate", "-"); + result.success("获取今日访问量统计成功", data); + return result; + } + } + + /** + * 记录页面访问(PV),由前端在进入系统总览页面时主动调用 + */ + @GetMapping("/track-visit") + public ResultDomain trackVisit() { + ResultDomain result = new ResultDomain<>(); + try { + String todayKeySuffix = LocalDate.now().format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE); + String pvKey = "stat:pv:" + todayKeySuffix; + redisService.incr(pvKey, 1L); + result.success("记录访问成功", "OK"); + } catch (Exception e) { + logger.error("记录PV失败", e); + result.fail("记录访问失败: " + e.getMessage()); + } return result; } @@ -121,7 +353,6 @@ public class SystemOverviewController { */ @GetMapping("/system-status") public ResultDomain> getSystemStatus() { - // TODO: 后续接入真实系统运行状态(CPU、内存、服务可用性等) ResultDomain> result = new ResultDomain<>(); Map data = new HashMap<>(); @@ -138,4 +369,36 @@ public class SystemOverviewController { item.put("value", value); return item; } + + private String formatDayLabel(LocalDate date) { + return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } + + private String formatDuration(int seconds) { + if (seconds <= 0) { + return "0秒"; + } + int minutes = seconds / 60; + int remain = seconds % 60; + if (minutes == 0) { + return remain + "秒"; + } + return minutes + "分" + remain + "秒"; + } + + /** + * 计算相对昨日的变化百分比,返回形如"+12%"或"-5%",当昨日为0或无数据时返回"+0%" + */ + private String formatPercentChange(long today, long yesterday) { + if (yesterday <= 0) { + return "+"+today+"%"; + } + + long diff = today - yesterday; + double percent = diff * 100.0 / yesterday; + // 保留一位小数 + String formatted = String.format("%.1f", Math.abs(percent)); + String sign = percent >= 0 ? "+" : "-"; + return sign + formatted + "%"; + } } diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java index 9f2c462..db70b59 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/mapper/UserMapper.java @@ -192,7 +192,7 @@ public interface UserMapper extends BaseMapper { UserVO selectUserInfoTotal(@Param("userId") String userId); - int countDeptUser(@Param("deptId") String deptId); + int countDeptUser(@Param("deptId") String deptId, @Param("filter") TbSysUser filter); /** * @description 查询部门及其子部门的所有用户ID diff --git a/schoolNewsServ/system/src/main/java/org/xyzh/system/service/user/service/impl/SysUserServiceImpl.java b/schoolNewsServ/system/src/main/java/org/xyzh/system/service/user/service/impl/SysUserServiceImpl.java index 562cadc..574727f 100644 --- a/schoolNewsServ/system/src/main/java/org/xyzh/system/service/user/service/impl/SysUserServiceImpl.java +++ b/schoolNewsServ/system/src/main/java/org/xyzh/system/service/user/service/impl/SysUserServiceImpl.java @@ -282,7 +282,7 @@ public class SysUserServiceImpl implements SysUserService { List users = userMapper.selectUserPage(filter, pageParam, userDeptRoles); PageDomain pageDomain = new PageDomain<>(); - int count = userMapper.countDeptUser(userDeptRoles.get(0).getDeptID()); + int count = userMapper.countDeptUser(userDeptRoles.get(0).getDeptID(), filter); pageDomain.setDataList(users); pageDomain.setPageParam(pageParam); pageParam.setTotalElements(count); @@ -299,7 +299,7 @@ public class SysUserServiceImpl implements SysUserService { List userVOs = userMapper.selectUserVOPage(filter, pageParam, userDeptRoles); PageDomain pageDomain = new PageDomain<>(); - int count = userMapper.countDeptUser(userDeptRoles.get(0).getDeptID()); + int count = userMapper.countDeptUser(userDeptRoles.get(0).getDeptID(), filter); pageDomain.setDataList(userVOs); pageDomain.setPageParam(pageParam); pageParam.setTotalElements(count); diff --git a/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml b/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml index be0f059..fd77c0e 100644 --- a/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml +++ b/schoolNewsServ/system/src/main/resources/mapper/UserMapper.xml @@ -95,24 +95,30 @@ - deleted = 0 + u.deleted = 0 - AND id = #{filter.id} + AND u.id = #{filter.id} - AND username = #{filter.username} + AND u.username = #{filter.username} - AND email = #{filter.email} + AND u.email = #{filter.email} - AND phone = #{filter.phone} + AND u.phone = #{filter.phone} - AND status = #{filter.status} + AND u.status = #{filter.status} - AND wechat_id = #{filter.wechatID} + AND u.wechat_id = #{filter.wechatID} + + + AND u.create_time >= #{filter.startTime} + + + AND u.create_time < #{filter.endTime} @@ -575,7 +581,7 @@ FROM tb_sys_user_dept_role tudr INNER JOIN tb_sys_dept d ON tudr.dept_id = d.dept_id AND d.deleted = 0 INNER JOIN tb_sys_user u ON tudr.user_id = u.id AND u.deleted = 0 - WHERE tudr.deleted = 0 + AND d.dept_path LIKE CONCAT( (SELECT dept_path FROM tb_sys_dept WHERE dept_id = #{deptId} AND deleted = 0), '%' diff --git a/schoolNewsWeb/src/apis/system/overview.ts b/schoolNewsWeb/src/apis/system/overview.ts index d9ea668..b573262 100644 --- a/schoolNewsWeb/src/apis/system/overview.ts +++ b/schoolNewsWeb/src/apis/system/overview.ts @@ -10,15 +10,16 @@ export interface SystemStatisticsDTO { totalUsersChange: string; totalResources: number; totalResourcesChange: string; - todayVisits: number; - todayVisitsChange: string; - activeUsers: number; - activeUsersChange: string; + totalPv: number; + totalPvChange: string; + totalUv: number; + totalUvChange: string; } export interface ActiveUsersChartDTO { labels: string[]; - values: number[]; + pvValues: number[]; + uvValues: number[]; } export interface ResourceCategoryItemDTO { @@ -56,5 +57,16 @@ export const systemOverviewApi = { async getTodayVisits(): Promise> { const response = await api.get('/system/overview/today-visits'); return response.data; + }, + + /** + * 记录一次页面访问(PV),前端在路由切换时调用 + */ + async trackVisit(): Promise> { + const response = await api.get('/system/overview/track-visit', undefined, { + showLoading: false, + showError: false + }); + return response.data; } }; diff --git a/schoolNewsWeb/src/assets/imgs/overview-pv.svg b/schoolNewsWeb/src/assets/imgs/overview-pv.svg new file mode 100644 index 0000000..e829c20 --- /dev/null +++ b/schoolNewsWeb/src/assets/imgs/overview-pv.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/schoolNewsWeb/src/assets/imgs/overview-resource.svg b/schoolNewsWeb/src/assets/imgs/overview-resource.svg new file mode 100644 index 0000000..62ee877 --- /dev/null +++ b/schoolNewsWeb/src/assets/imgs/overview-resource.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/schoolNewsWeb/src/assets/imgs/overview-user.svg b/schoolNewsWeb/src/assets/imgs/overview-user.svg new file mode 100644 index 0000000..6022422 --- /dev/null +++ b/schoolNewsWeb/src/assets/imgs/overview-user.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/schoolNewsWeb/src/assets/imgs/overview-uv.svg b/schoolNewsWeb/src/assets/imgs/overview-uv.svg new file mode 100644 index 0000000..db2f6ad --- /dev/null +++ b/schoolNewsWeb/src/assets/imgs/overview-uv.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/schoolNewsWeb/src/utils/permission.ts b/schoolNewsWeb/src/utils/permission.ts index ba7581c..999809f 100644 --- a/schoolNewsWeb/src/utils/permission.ts +++ b/schoolNewsWeb/src/utils/permission.ts @@ -7,6 +7,7 @@ import type { Router, NavigationGuardNext, RouteLocationNormalized } from 'vue-router'; import type { Store } from 'vuex'; import { AuthState } from '@/store/modules/auth'; +import { systemOverviewApi } from '@/apis/system/overview'; /** * 白名单路由 - 无需登录即可访问 @@ -42,13 +43,19 @@ export function setupRouterGuards(router: Router, store: Store) { }); // 全局后置钩子 - router.afterEach((to) => { // 结束页面加载进度条 finishProgress(); // 设置页面标题 setPageTitle(to.meta?.title as string); + + // 统计PV:对path以"/"开头的路由进行统计 + if (to.path.startsWith('/')) { + systemOverviewApi.trackVisit().catch(() => { + // 统计失败不影响正常页面使用 + }); + } }); // 全局解析守卫(在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用) diff --git a/schoolNewsWeb/src/views/admin/overview/SystemOverviewView.vue b/schoolNewsWeb/src/views/admin/overview/SystemOverviewView.vue index d7c1fc2..12e67fd 100644 --- a/schoolNewsWeb/src/views/admin/overview/SystemOverviewView.vue +++ b/schoolNewsWeb/src/views/admin/overview/SystemOverviewView.vue @@ -4,15 +4,55 @@
-
-
- {{ stat.icon }} -
+ +
+
用户
-

{{ stat.value }}

-

{{ stat.label }}

- - {{ stat.change }} +

用户总数

+ + {{ statistics.totalUsers }} + + {{ statistics.totalUsersChange }} 较昨日 + + +
+
+ +
+
资源
+
+

资源总数

+ + {{ statistics.totalResources }} + + {{ statistics.totalResourcesChange }} 较昨日 + + +
+
+ +
+
pv
+
+

今日访问

+ + {{ statistics.totalPv }} + + {{ statistics.totalPvChange }} 较昨日 + + +
+
+ +
+
uv
+
+

今日用户

+ + {{ statistics.totalUv }} + + {{ statistics.totalUvChange }} 较昨日 +
@@ -24,7 +64,7 @@