访问量统计

This commit is contained in:
zjf committed 2026-06-08 19:32:26 +08:00
1 parent 3c06ff7643
commit d4e6b506b3
7 files changed
+340

No files matched your search

@@ -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;
@@ -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<>();
}
+4
View File
@@ -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>
@@ -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));
}
@@ -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")
@@ -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);
@@ -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();
}
}
}