访问量统计
This commit is contained in:
1 parent
3c06ff7643
commit
d4e6b506b3
7 files changed
+340
No files matched your search
+3
@@ -28,6 +28,9 @@ public class AppAccessStatsQueryDTO {
|
||||
@Schema(description = "部门ID列表")
|
||||
private List<Long> deptIds;
|
||||
|
||||
@Schema(description = "层级统计根部门ID,不传则从组织树顶层开始")
|
||||
private Long rootDeptId;
|
||||
|
||||
@Schema(description = "页面编码列表")
|
||||
private List<String> pageCodes;
|
||||
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.bsd.pjcloud.admin.api.vo.stats;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnore;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "按部门层级访问统计")
|
||||
public class AppAccessDeptTreeStatsVO {
|
||||
|
||||
@Schema(description = "部门ID")
|
||||
@ExcelProperty("部门ID")
|
||||
private Long deptId;
|
||||
|
||||
@Schema(description = "上级部门ID")
|
||||
@ExcelIgnore
|
||||
private Long parentId;
|
||||
|
||||
@Schema(description = "部门名称")
|
||||
@ExcelProperty("部门名称")
|
||||
private String deptName;
|
||||
|
||||
@Schema(description = "是否单位: 0部门 1单位")
|
||||
@ExcelIgnore
|
||||
private Integer isUnit;
|
||||
|
||||
@Schema(description = "层级,从1开始")
|
||||
@ExcelIgnore
|
||||
private Integer level;
|
||||
|
||||
@Schema(description = "本级访问量(PV)")
|
||||
@ExcelProperty("本级PV")
|
||||
private Long selfPv = 0L;
|
||||
|
||||
@Schema(description = "本级访客数(UV)")
|
||||
@ExcelProperty("本级UV")
|
||||
private Long selfUv = 0L;
|
||||
|
||||
@Schema(description = "本级访问人员数")
|
||||
@ExcelProperty("本级访问用户数")
|
||||
private Long selfVisitUserCount = 0L;
|
||||
|
||||
@Schema(description = "本级访问轨迹次数")
|
||||
@ExcelProperty("本级轨迹数")
|
||||
private Long selfTrackCount = 0L;
|
||||
|
||||
@Schema(description = "合计访问量(PV),含全部下级部门")
|
||||
@ExcelProperty("合计PV")
|
||||
private Long totalPv = 0L;
|
||||
|
||||
@Schema(description = "合计访客数(UV),含全部下级部门")
|
||||
@ExcelProperty("合计UV")
|
||||
private Long totalUv = 0L;
|
||||
|
||||
@Schema(description = "合计访问人员数,含全部下级部门")
|
||||
@ExcelProperty("合计访问用户数")
|
||||
private Long totalVisitUserCount = 0L;
|
||||
|
||||
@Schema(description = "合计访问轨迹次数,含全部下级部门")
|
||||
@ExcelProperty("合计轨迹数")
|
||||
private Long totalTrackCount = 0L;
|
||||
|
||||
@Schema(description = "子部门统计")
|
||||
@ExcelIgnore
|
||||
private List<AppAccessDeptTreeStatsVO> children = new ArrayList<>();
|
||||
}
|
||||
@@ -33,6 +33,10 @@
|
||||
<description>pjcloud 通用用户权限管理系统业务处理模块</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
<artifactId>pjcloud-common-encrypt-api</artifactId>
|
||||
</dependency>
|
||||
<!--websocket 支持-->
|
||||
<dependency>
|
||||
<groupId>com.bsd</groupId>
|
||||
|
||||
+2
@@ -36,6 +36,7 @@ import com.bsd.pjcloud.admin.api.entity.SysUserRole;
|
||||
import com.bsd.pjcloud.admin.api.entity.user.SysUserTagEntity;
|
||||
import com.bsd.pjcloud.admin.api.vo.*;
|
||||
import com.bsd.pjcloud.admin.service.*;
|
||||
import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
|
||||
import com.bsd.pjcloud.common.core.constant.CommonConstants;
|
||||
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
|
||||
import com.bsd.pjcloud.common.core.util.MsgUtils;
|
||||
@@ -222,6 +223,7 @@ public class SysUserController {
|
||||
* @return 用户信息
|
||||
*/
|
||||
@GetMapping("/details/{id}")
|
||||
// @ApiCryptoAes
|
||||
public R user(@PathVariable Long id) {
|
||||
return R.ok(userService.selectUserVoById(id));
|
||||
}
|
||||
|
||||
+16
@@ -3,6 +3,7 @@ package com.bsd.pjcloud.admin.controller.stats;
|
||||
import com.bsd.pjcloud.admin.api.dto.stats.AppAccessStatsQueryDTO;
|
||||
import com.bsd.pjcloud.admin.api.dto.stats.AppAccessTrackRequestDTO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessDeptStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessDeptTreeStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessOverviewVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessUnitStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessPageStatsVO;
|
||||
@@ -68,6 +69,21 @@ public class AppAccessStatsController {
|
||||
return appAccessStatsService.byDeptForExport(dto);
|
||||
}
|
||||
|
||||
@Operation(summary = "按部门层级统计", description = "返回组织树,每级含本级与下级合计")
|
||||
@PostMapping("/by-dept-tree")
|
||||
@HasPermission("stats_access_view")
|
||||
public R<List<AppAccessDeptTreeStatsVO>> byDeptTree(@RequestBody AppAccessStatsQueryDTO dto) {
|
||||
return R.ok(appAccessStatsService.byDeptTree(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "按部门层级统计导出", description = "部门层级访问统计Excel导出")
|
||||
@ResponseExcel(name = "部门层级访问统计", suffix = com.alibaba.excel.support.ExcelTypeEnum.XLSX)
|
||||
@PostMapping("/by-dept-tree/export")
|
||||
@HasPermission("stats_access_view")
|
||||
public List<AppAccessDeptTreeStatsVO> byDeptTreeExport(@RequestBody AppAccessStatsQueryDTO dto) {
|
||||
return appAccessStatsService.byDeptTreeForExport(dto);
|
||||
}
|
||||
|
||||
@Operation(summary = "按一级部门统计", description = "各单位总访问数据汇总")
|
||||
@PostMapping("/by-unit")
|
||||
@HasPermission("stats_access_view")
|
||||
|
||||
+5
@@ -3,6 +3,7 @@ package com.bsd.pjcloud.admin.service;
|
||||
import com.bsd.pjcloud.admin.api.dto.stats.AppAccessStatsQueryDTO;
|
||||
import com.bsd.pjcloud.admin.api.dto.stats.AppAccessTrackRequestDTO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessDeptStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessDeptTreeStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessOverviewVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessPageStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessUnitStatsVO;
|
||||
@@ -24,6 +25,10 @@ public interface AppAccessStatsService {
|
||||
|
||||
List<AppAccessDeptStatsVO> byDeptForExport(AppAccessStatsQueryDTO dto);
|
||||
|
||||
List<AppAccessDeptTreeStatsVO> byDeptTree(AppAccessStatsQueryDTO dto);
|
||||
|
||||
List<AppAccessDeptTreeStatsVO> byDeptTreeForExport(AppAccessStatsQueryDTO dto);
|
||||
|
||||
List<AppAccessUnitStatsVO> byUnit(AppAccessStatsQueryDTO dto);
|
||||
|
||||
List<AppAccessUnitStatsVO> byUnitForExport(AppAccessStatsQueryDTO dto);
|
||||
|
||||
+240
@@ -6,12 +6,14 @@ import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.admin.api.dto.stats.AppAccessStatsQueryDTO;
|
||||
import com.bsd.pjcloud.admin.api.dto.stats.AppAccessTrackRequestDTO;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysDept;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.entity.stats.*;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessDeptStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessDeptTreeStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessOverviewVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessUnitStatsVO;
|
||||
import com.bsd.pjcloud.admin.api.vo.stats.AppAccessPageStatsVO;
|
||||
@@ -153,6 +155,187 @@ public class AppAccessStatsServiceImpl implements AppAccessStatsService {
|
||||
return byDeptWithLimit(dto, 5000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppAccessDeptTreeStatsVO> byDeptTree(AppAccessStatsQueryDTO dto) {
|
||||
return buildDeptTreeStats(dto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppAccessDeptTreeStatsVO> byDeptTreeForExport(AppAccessStatsQueryDTO dto) {
|
||||
return flattenDeptTreeStats(buildDeptTreeStats(dto));
|
||||
}
|
||||
|
||||
private List<AppAccessDeptTreeStatsVO> buildDeptTreeStats(AppAccessStatsQueryDTO dto) {
|
||||
List<SysDept> allDepts = sysDeptMapper.selectList(Wrappers.<SysDept>lambdaQuery()
|
||||
.orderByAsc(SysDept::getSortOrder)
|
||||
.orderByAsc(SysDept::getDeptId));
|
||||
if (CollUtil.isEmpty(allDepts)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Map<Long, SysDept> deptMap = allDepts.stream()
|
||||
.collect(Collectors.toMap(SysDept::getDeptId, item -> item, (a, b) -> a, LinkedHashMap::new));
|
||||
Map<Long, List<SysDept>> childrenMap = allDepts.stream()
|
||||
.filter(item -> item.getParentId() != null && item.getParentId() > 0)
|
||||
.collect(Collectors.groupingBy(SysDept::getParentId));
|
||||
|
||||
Map<Long, MetricsAccumulator> selfMetricsMap = loadSelfDeptMetrics(dto);
|
||||
Map<Long, MetricsAccumulator> totalMetricsMap = new HashMap<>();
|
||||
for (Long deptId : deptMap.keySet()) {
|
||||
computeTotalMetrics(deptId, childrenMap, selfMetricsMap, totalMetricsMap);
|
||||
}
|
||||
|
||||
List<SysDept> roots = resolveDeptTreeRoots(allDepts, deptMap, dto.getRootDeptId());
|
||||
List<AppAccessDeptTreeStatsVO> result = new ArrayList<>();
|
||||
for (SysDept root : roots) {
|
||||
result.add(buildDeptTreeNode(root, childrenMap, selfMetricsMap, totalMetricsMap, 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<Long, MetricsAccumulator> loadSelfDeptMetrics(AppAccessStatsQueryDTO dto) {
|
||||
TimeRange range = resolveRange(dto);
|
||||
LambdaQueryWrapper<AppAccessEventEntity> wrapper = buildEventListQuery(dto, range);
|
||||
List<AppAccessEventEntity> events = appAccessEventMapper.selectList(wrapper);
|
||||
Map<Long, MetricsAccumulator> selfMetricsMap = new HashMap<>();
|
||||
for (AppAccessEventEntity event : events) {
|
||||
if (event.getDeptId() == null) {
|
||||
continue;
|
||||
}
|
||||
selfMetricsMap.computeIfAbsent(event.getDeptId(), key -> new MetricsAccumulator())
|
||||
.addEvent(event.getUserId(), event.getDeviceId());
|
||||
}
|
||||
return selfMetricsMap;
|
||||
}
|
||||
|
||||
private MetricsAccumulator computeTotalMetrics(Long deptId,
|
||||
Map<Long, List<SysDept>> childrenMap,
|
||||
Map<Long, MetricsAccumulator> selfMetricsMap,
|
||||
Map<Long, MetricsAccumulator> totalMetricsMap) {
|
||||
MetricsAccumulator cached = totalMetricsMap.get(deptId);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
MetricsAccumulator total = copyMetrics(selfMetricsMap.get(deptId));
|
||||
List<SysDept> children = childrenMap.getOrDefault(deptId, Collections.emptyList());
|
||||
for (SysDept child : children) {
|
||||
total.merge(computeTotalMetrics(child.getDeptId(), childrenMap, selfMetricsMap, totalMetricsMap));
|
||||
}
|
||||
totalMetricsMap.put(deptId, total);
|
||||
return total;
|
||||
}
|
||||
|
||||
private AppAccessDeptTreeStatsVO buildDeptTreeNode(SysDept dept,
|
||||
Map<Long, List<SysDept>> childrenMap,
|
||||
Map<Long, MetricsAccumulator> selfMetricsMap,
|
||||
Map<Long, MetricsAccumulator> totalMetricsMap,
|
||||
int level) {
|
||||
MetricsAccumulator selfMetrics = selfMetricsMap.getOrDefault(dept.getDeptId(), MetricsAccumulator.empty());
|
||||
MetricsAccumulator totalMetrics = totalMetricsMap.getOrDefault(dept.getDeptId(), MetricsAccumulator.empty());
|
||||
|
||||
AppAccessDeptTreeStatsVO vo = new AppAccessDeptTreeStatsVO();
|
||||
vo.setDeptId(dept.getDeptId());
|
||||
vo.setParentId(dept.getParentId());
|
||||
vo.setDeptName(dept.getName());
|
||||
vo.setIsUnit(dept.getIsUnit());
|
||||
vo.setLevel(level);
|
||||
fillMetrics(vo, selfMetrics, totalMetrics);
|
||||
|
||||
List<SysDept> children = childrenMap.getOrDefault(dept.getDeptId(), Collections.emptyList());
|
||||
children.sort(Comparator
|
||||
.comparing(SysDept::getSortOrder, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(SysDept::getDeptId, Comparator.nullsLast(Long::compareTo)));
|
||||
for (SysDept child : children) {
|
||||
vo.getChildren().add(buildDeptTreeNode(child, childrenMap, selfMetricsMap, totalMetricsMap, level + 1));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private List<SysDept> resolveDeptTreeRoots(List<SysDept> allDepts, Map<Long, SysDept> deptMap, Long rootDeptId) {
|
||||
if (rootDeptId != null) {
|
||||
SysDept root = deptMap.get(rootDeptId);
|
||||
return root == null ? Collections.emptyList() : Collections.singletonList(root);
|
||||
}
|
||||
Set<Long> allIds = deptMap.keySet();
|
||||
return allDepts.stream()
|
||||
.filter(item -> item.getParentId() == null || item.getParentId() <= 0 || !allIds.contains(item.getParentId()))
|
||||
.sorted(Comparator
|
||||
.comparing(SysDept::getSortOrder, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(SysDept::getDeptId, Comparator.nullsLast(Long::compareTo)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<AppAccessDeptTreeStatsVO> flattenDeptTreeStats(List<AppAccessDeptTreeStatsVO> tree) {
|
||||
List<AppAccessDeptTreeStatsVO> rows = new ArrayList<>();
|
||||
flattenDeptTreeNode(tree, rows, 0);
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void flattenDeptTreeNode(List<AppAccessDeptTreeStatsVO> nodes, List<AppAccessDeptTreeStatsVO> rows, int depth) {
|
||||
if (CollUtil.isEmpty(nodes)) {
|
||||
return;
|
||||
}
|
||||
String indent = depth <= 0 ? "" : " ".repeat(depth);
|
||||
for (AppAccessDeptTreeStatsVO node : nodes) {
|
||||
AppAccessDeptTreeStatsVO row = new AppAccessDeptTreeStatsVO();
|
||||
row.setDeptId(node.getDeptId());
|
||||
row.setParentId(node.getParentId());
|
||||
row.setDeptName(indent + ObjectUtil.defaultIfNull(node.getDeptName(), ""));
|
||||
row.setIsUnit(node.getIsUnit());
|
||||
row.setLevel(node.getLevel());
|
||||
row.setSelfPv(node.getSelfPv());
|
||||
row.setSelfUv(node.getSelfUv());
|
||||
row.setSelfVisitUserCount(node.getSelfVisitUserCount());
|
||||
row.setSelfTrackCount(node.getSelfTrackCount());
|
||||
row.setTotalPv(node.getTotalPv());
|
||||
row.setTotalUv(node.getTotalUv());
|
||||
row.setTotalVisitUserCount(node.getTotalVisitUserCount());
|
||||
row.setTotalTrackCount(node.getTotalTrackCount());
|
||||
rows.add(row);
|
||||
flattenDeptTreeNode(node.getChildren(), rows, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMetrics(AppAccessDeptTreeStatsVO vo, MetricsAccumulator selfMetrics, MetricsAccumulator totalMetrics) {
|
||||
vo.setSelfPv(selfMetrics.pv());
|
||||
vo.setSelfUv(selfMetrics.uv());
|
||||
vo.setSelfVisitUserCount(selfMetrics.visitUserCount());
|
||||
vo.setSelfTrackCount(selfMetrics.trackCount());
|
||||
vo.setTotalPv(totalMetrics.pv());
|
||||
vo.setTotalUv(totalMetrics.uv());
|
||||
vo.setTotalVisitUserCount(totalMetrics.visitUserCount());
|
||||
vo.setTotalTrackCount(totalMetrics.trackCount());
|
||||
}
|
||||
|
||||
private MetricsAccumulator copyMetrics(MetricsAccumulator source) {
|
||||
if (source == null) {
|
||||
return MetricsAccumulator.empty();
|
||||
}
|
||||
return source.copy();
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<AppAccessEventEntity> buildEventListQuery(AppAccessStatsQueryDTO dto, TimeRange range) {
|
||||
LambdaQueryWrapper<AppAccessEventEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.select(AppAccessEventEntity::getDeptId, AppAccessEventEntity::getUserId, AppAccessEventEntity::getDeviceId);
|
||||
wrapper.ge(AppAccessEventEntity::getEventTime, range.start);
|
||||
wrapper.lt(AppAccessEventEntity::getEventTime, range.end);
|
||||
wrapper.isNotNull(AppAccessEventEntity::getUserId);
|
||||
Long tenantId = resolveTenantId(SecurityUtils.getUser(), dto.getTenantId());
|
||||
if (tenantId != null) {
|
||||
wrapper.eq(AppAccessEventEntity::getTenantId, tenantId);
|
||||
}
|
||||
if (CollUtil.isNotEmpty(dto.getUnitIds())) {
|
||||
wrapper.in(AppAccessEventEntity::getUnitId, dto.getUnitIds());
|
||||
}
|
||||
if (CollUtil.isNotEmpty(dto.getDeptIds())) {
|
||||
wrapper.in(AppAccessEventEntity::getDeptId, dto.getDeptIds());
|
||||
}
|
||||
if (CollUtil.isNotEmpty(dto.getPageCodes())) {
|
||||
wrapper.in(AppAccessEventEntity::getPageCode, dto.getPageCodes());
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private List<AppAccessDeptStatsVO> byDeptWithLimit(AppAccessStatsQueryDTO dto, int maxTopN) {
|
||||
TimeRange range = resolveRange(dto);
|
||||
QueryWrapper<AppAccessEventEntity> wrapper = buildEventBaseQuery(dto, range);
|
||||
@@ -586,4 +769,61 @@ public class AppAccessStatsServiceImpl implements AppAccessStatsService {
|
||||
private Long deptId;
|
||||
private String deptName;
|
||||
}
|
||||
|
||||
private static class MetricsAccumulator {
|
||||
private long pv;
|
||||
private long trackCount;
|
||||
private final Set<String> uvKeys = new HashSet<>();
|
||||
private final Set<Long> userIds = new HashSet<>();
|
||||
|
||||
static MetricsAccumulator empty() {
|
||||
return new MetricsAccumulator();
|
||||
}
|
||||
|
||||
void addEvent(Long userId, String deviceId) {
|
||||
pv++;
|
||||
trackCount++;
|
||||
if (userId != null) {
|
||||
uvKeys.add("U_" + userId);
|
||||
userIds.add(userId);
|
||||
} else if (StrUtil.isNotBlank(deviceId)) {
|
||||
uvKeys.add("D_" + deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
void merge(MetricsAccumulator other) {
|
||||
if (other == null) {
|
||||
return;
|
||||
}
|
||||
pv += other.pv;
|
||||
trackCount += other.trackCount;
|
||||
uvKeys.addAll(other.uvKeys);
|
||||
userIds.addAll(other.userIds);
|
||||
}
|
||||
|
||||
MetricsAccumulator copy() {
|
||||
MetricsAccumulator copy = new MetricsAccumulator();
|
||||
copy.pv = pv;
|
||||
copy.trackCount = trackCount;
|
||||
copy.uvKeys.addAll(uvKeys);
|
||||
copy.userIds.addAll(userIds);
|
||||
return copy;
|
||||
}
|
||||
|
||||
long pv() {
|
||||
return pv;
|
||||
}
|
||||
|
||||
long trackCount() {
|
||||
return trackCount;
|
||||
}
|
||||
|
||||
long uv() {
|
||||
return uvKeys.size();
|
||||
}
|
||||
|
||||
long visitUserCount() {
|
||||
return userIds.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user