overview统计

This commit is contained in:
2025-11-14 18:31:39 +08:00
parent 6be3cc6abd
commit 9adc0c2058
24 changed files with 723 additions and 178 deletions

View File

@@ -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>

View File

@@ -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 + "%";
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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 &gt;= #{filter.startTime}
</if>
<if test="filter.endTime != null">
AND u.create_time &lt; #{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),
'%'