@@ -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.LocalDat e;
import org.xyzh.common.dto.resource.TbResource ;
import org.xyzh.common.dto.study.TbCours e;
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 = start Date. until ( endDate ) . getDays ( ) ;
LocalDate to day = Local Date. 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 ) ;
r esult . s uccess( " 获取资源分类统计成功 " , data ) ;
// 获取数量, 失败时默认为0
int resourceCount = ( resourceCountR esult . isS uccess( ) & & 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 + " % " ;
}
}