overview统计
This commit is contained in:
@@ -183,4 +183,7 @@ public interface ResourceService {
|
||||
* @since 2025-10-15
|
||||
*/
|
||||
ResultDomain<TbResource> searchResources(String keyword, String tagID, Integer status);
|
||||
|
||||
ResultDomain<Integer> getResourceCount(TbResource filter);
|
||||
|
||||
}
|
||||
|
||||
@@ -190,4 +190,13 @@ public interface CourseService {
|
||||
* @since 2025-10-28
|
||||
*/
|
||||
ResultDomain<CourseItemVO> getCourseProgress(String courseID);
|
||||
|
||||
/**
|
||||
* @description 获取课程数量
|
||||
* @param filter 过滤条件
|
||||
* @return ResultDomain<Integer> 课程数量
|
||||
* @author yslg
|
||||
* @since 2025-11-14
|
||||
*/
|
||||
ResultDomain<Integer> getCourseCount(TbCourse filter);
|
||||
}
|
||||
|
||||
@@ -237,4 +237,13 @@ public interface LearningTaskService {
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
ResultDomain<Map<String, Object>> getTaskStatisticsRankings(String taskID);
|
||||
|
||||
/**
|
||||
* @description 获取学习任务数量
|
||||
* @param filter 过滤条件
|
||||
* @return ResultDomain<Integer> 学习任务数量
|
||||
* @author yslg
|
||||
* @since 2025-11-14
|
||||
*/
|
||||
ResultDomain<Integer> getLearningTaskCount(TbLearningTask filter);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 更新时间
|
||||
|
||||
@@ -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 键
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,4 +754,19 @@ public class NCResourceServiceImpl implements ResourceService {
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Integer> getResourceCount(TbResource filter) {
|
||||
ResultDomain<Integer> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
List<UserDeptRoleVO> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +387,12 @@
|
||||
<if test="filter.isBanner != null">
|
||||
AND r.is_banner = #{filter.isBanner}
|
||||
</if>
|
||||
<if test="filter.startTime != null">
|
||||
AND r.publish_time >= #{filter.startTime}
|
||||
</if>
|
||||
<if test="filter.endTime != null">
|
||||
AND r.publish_time <= #{filter.endTime}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- updateResourceCollectCount -->
|
||||
|
||||
@@ -550,14 +550,14 @@ public class SCCourseServiceImpl implements SCCourseService {
|
||||
@Override
|
||||
public ResultDomain<CourseItemVO> getCourseProgress(String courseID) {
|
||||
ResultDomain<CourseItemVO> 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<TbCourseChapter> chapters = courseChapterMapper.selectCourseChapters(filter);
|
||||
|
||||
|
||||
// 查询并构建章节及节点结构(带进度)
|
||||
if (!chapters.isEmpty()) {
|
||||
List<String> chapterIDs = chapters.stream()
|
||||
.map(TbCourseChapter::getChapterID)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
// 查询带进度的节点(传入用户ID)
|
||||
List<CourseItemVO> nodesWithProgress = courseNodeMapper.selectNodesProgress(chapterIDs, user.getID());
|
||||
|
||||
|
||||
// 转换章节为CourseItemVO列表
|
||||
List<CourseItemVO> chapterVOs = chapters.stream()
|
||||
.map(CourseItemVO::fromChapter)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
// 按章节ID分组节点
|
||||
Map<String, List<CourseItemVO>> 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<Integer> getCourseCount(TbCourse filter) {
|
||||
ResultDomain<Integer> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
// 获取当前用户的部门角色
|
||||
List<UserDeptRoleVO> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,15 +796,15 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
|
||||
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
Map<String, Object> rankingsData = new HashMap<>();
|
||||
|
||||
|
||||
// 获取完成时间排行榜(前10名)
|
||||
List<Map<String, Object>> completionTimeRanking = taskUserMapper.getCompletionTimeRanking(taskID);
|
||||
rankingsData.put("completionTimeRanking", completionTimeRanking);
|
||||
|
||||
|
||||
// 获取学习时长排行榜(前10名)
|
||||
List<Map<String, Object>> 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<Integer> getLearningTaskCount(TbLearningTask filter) {
|
||||
ResultDomain<Integer> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
// 获取当前用户的部门角色
|
||||
List<UserDeptRoleVO> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
<artifactId>api-system</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xyzh</groupId>
|
||||
<artifactId>api-news</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xyzh</groupId>
|
||||
<artifactId>api-study</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Common模块依赖 -->
|
||||
<dependency>
|
||||
|
||||
@@ -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<Map<String, Object>> getSystemStatistics() {
|
||||
// TODO: 后续接入真实统计数据(用户表、资源表、访问统计表等)
|
||||
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
List<UserDeptRoleVO> 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<Integer> 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<Integer> 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<Map<String, Object>> getActiveUsersChart(
|
||||
@RequestParam(required = true, name = "start") String start, @RequestParam(required = true, name = "end") String end) {
|
||||
// TODO: 后续根据days参数(7/30天)查询真实活跃用户统计
|
||||
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
|
||||
Map<String, Object> 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<String> labels = new ArrayList<>();
|
||||
List<Integer> pvValues = new ArrayList<>();
|
||||
List<Integer> 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<Map<String, Object>> getResourceCategoryStats() {
|
||||
// TODO: 后续从资源表统计各类型资源数量
|
||||
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
try {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
// 饼图数据:名称 + 数量
|
||||
List<Map<String, Object>> categories = List.of(
|
||||
createCategory("文章", 1048),
|
||||
createCategory("视频", 735),
|
||||
createCategory("音频", 580),
|
||||
createCategory("课程", 484),
|
||||
createCategory("其他", 300)
|
||||
);
|
||||
// 获取各类型资源数量
|
||||
ResultDomain<Integer> resourceCountResult = resourceService.getResourceCount(new TbResource());
|
||||
ResultDomain<Integer> courseCountResult = courseService.getCourseCount(new TbCourse());
|
||||
ResultDomain<Integer> 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<Map<String, Object>> 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<Map<String, Object>> getTodayVisits() {
|
||||
// TODO: 后续接入访问日志/统计表,计算今日指标
|
||||
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
|
||||
Map<String, Object> 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<TbSysVisitStatistics> 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<String> trackVisit() {
|
||||
ResultDomain<String> 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<Map<String, Object>> getSystemStatus() {
|
||||
// TODO: 后续接入真实系统运行状态(CPU、内存、服务可用性等)
|
||||
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
|
||||
Map<String, Object> 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 + "%";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ public interface UserMapper extends BaseMapper<TbSysUser> {
|
||||
UserVO selectUserInfoTotal(@Param("userId") String userId);
|
||||
|
||||
|
||||
int countDeptUser(@Param("deptId") String deptId);
|
||||
int countDeptUser(@Param("deptId") String deptId, @Param("filter") TbSysUser filter);
|
||||
|
||||
/**
|
||||
* @description 查询部门及其子部门的所有用户ID
|
||||
|
||||
@@ -282,7 +282,7 @@ public class SysUserServiceImpl implements SysUserService {
|
||||
List<TbSysUser> users = userMapper.selectUserPage(filter, pageParam, userDeptRoles);
|
||||
PageDomain<TbSysUser> 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<UserVO> userVOs = userMapper.selectUserVOPage(filter, pageParam, userDeptRoles);
|
||||
PageDomain<UserVO> 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);
|
||||
|
||||
@@ -95,24 +95,30 @@
|
||||
|
||||
<sql id="Filter_Clause">
|
||||
<where>
|
||||
deleted = 0
|
||||
u.deleted = 0
|
||||
<if test="filter.id != null and filter.id != ''">
|
||||
AND id = #{filter.id}
|
||||
AND u.id = #{filter.id}
|
||||
</if>
|
||||
<if test="filter.username != null and filter.username != ''">
|
||||
AND username = #{filter.username}
|
||||
AND u.username = #{filter.username}
|
||||
</if>
|
||||
<if test="filter.email != null and filter.email != ''">
|
||||
AND email = #{filter.email}
|
||||
AND u.email = #{filter.email}
|
||||
</if>
|
||||
<if test="filter.phone != null and filter.phone != ''">
|
||||
AND phone = #{filter.phone}
|
||||
AND u.phone = #{filter.phone}
|
||||
</if>
|
||||
<if test="filter.status != null">
|
||||
AND status = #{filter.status}
|
||||
AND u.status = #{filter.status}
|
||||
</if>
|
||||
<if test="filter.wechatID != null and filter.wechatID != ''">
|
||||
AND wechat_id = #{filter.wechatID}
|
||||
AND u.wechat_id = #{filter.wechatID}
|
||||
</if>
|
||||
<if test="filter.startTime != null">
|
||||
AND u.create_time >= #{filter.startTime}
|
||||
</if>
|
||||
<if test="filter.endTime != null">
|
||||
AND u.create_time < #{filter.endTime}
|
||||
</if>
|
||||
</where>
|
||||
</sql>
|
||||
@@ -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
|
||||
<include refid="Filter_Clause"/>
|
||||
AND d.dept_path LIKE CONCAT(
|
||||
(SELECT dept_path FROM tb_sys_dept WHERE dept_id = #{deptId} AND deleted = 0),
|
||||
'%'
|
||||
|
||||
Reference in New Issue
Block a user