diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/stats/AppAccessStatsQueryDTO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/stats/AppAccessStatsQueryDTO.java index bc2d476d..61458602 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/stats/AppAccessStatsQueryDTO.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/stats/AppAccessStatsQueryDTO.java @@ -28,6 +28,9 @@ public class AppAccessStatsQueryDTO { @Schema(description = "部门ID列表") private List deptIds; + @Schema(description = "层级统计根部门ID,不传则从组织树顶层开始") + private Long rootDeptId; + @Schema(description = "页面编码列表") private List pageCodes; diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/stats/AppAccessDeptTreeStatsVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/stats/AppAccessDeptTreeStatsVO.java new file mode 100644 index 00000000..f4417247 --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/stats/AppAccessDeptTreeStatsVO.java @@ -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 children = new ArrayList<>(); +} diff --git a/pjcloud-upms/pjcloud-upms-biz/pom.xml b/pjcloud-upms/pjcloud-upms-biz/pom.xml index 809c3e63..be91180d 100644 --- a/pjcloud-upms/pjcloud-upms-biz/pom.xml +++ b/pjcloud-upms/pjcloud-upms-biz/pom.xml @@ -33,6 +33,10 @@ pjcloud 通用用户权限管理系统业务处理模块 + + com.bsd + pjcloud-common-encrypt-api + com.bsd diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java index 2ef85c18..2c41e0f2 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java @@ -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)); } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/stats/AppAccessStatsController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/stats/AppAccessStatsController.java index 80adb092..e6f3ed37 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/stats/AppAccessStatsController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/stats/AppAccessStatsController.java @@ -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> 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 byDeptTreeExport(@RequestBody AppAccessStatsQueryDTO dto) { + return appAccessStatsService.byDeptTreeForExport(dto); + } + @Operation(summary = "按一级部门统计", description = "各单位总访问数据汇总") @PostMapping("/by-unit") @HasPermission("stats_access_view") diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/AppAccessStatsService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/AppAccessStatsService.java index 3524b67e..4425adc0 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/AppAccessStatsService.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/AppAccessStatsService.java @@ -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 byDeptForExport(AppAccessStatsQueryDTO dto); + List byDeptTree(AppAccessStatsQueryDTO dto); + + List byDeptTreeForExport(AppAccessStatsQueryDTO dto); + List byUnit(AppAccessStatsQueryDTO dto); List byUnitForExport(AppAccessStatsQueryDTO dto); diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java index 5b4d3080..c366e490 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/stats/AppAccessStatsServiceImpl.java @@ -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 byDeptTree(AppAccessStatsQueryDTO dto) { + return buildDeptTreeStats(dto); + } + + @Override + public List byDeptTreeForExport(AppAccessStatsQueryDTO dto) { + return flattenDeptTreeStats(buildDeptTreeStats(dto)); + } + + private List buildDeptTreeStats(AppAccessStatsQueryDTO dto) { + List allDepts = sysDeptMapper.selectList(Wrappers.lambdaQuery() + .orderByAsc(SysDept::getSortOrder) + .orderByAsc(SysDept::getDeptId)); + if (CollUtil.isEmpty(allDepts)) { + return Collections.emptyList(); + } + + Map deptMap = allDepts.stream() + .collect(Collectors.toMap(SysDept::getDeptId, item -> item, (a, b) -> a, LinkedHashMap::new)); + Map> childrenMap = allDepts.stream() + .filter(item -> item.getParentId() != null && item.getParentId() > 0) + .collect(Collectors.groupingBy(SysDept::getParentId)); + + Map selfMetricsMap = loadSelfDeptMetrics(dto); + Map totalMetricsMap = new HashMap<>(); + for (Long deptId : deptMap.keySet()) { + computeTotalMetrics(deptId, childrenMap, selfMetricsMap, totalMetricsMap); + } + + List roots = resolveDeptTreeRoots(allDepts, deptMap, dto.getRootDeptId()); + List result = new ArrayList<>(); + for (SysDept root : roots) { + result.add(buildDeptTreeNode(root, childrenMap, selfMetricsMap, totalMetricsMap, 1)); + } + return result; + } + + private Map loadSelfDeptMetrics(AppAccessStatsQueryDTO dto) { + TimeRange range = resolveRange(dto); + LambdaQueryWrapper wrapper = buildEventListQuery(dto, range); + List events = appAccessEventMapper.selectList(wrapper); + Map 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> childrenMap, + Map selfMetricsMap, + Map totalMetricsMap) { + MetricsAccumulator cached = totalMetricsMap.get(deptId); + if (cached != null) { + return cached; + } + MetricsAccumulator total = copyMetrics(selfMetricsMap.get(deptId)); + List 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> childrenMap, + Map selfMetricsMap, + Map 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 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 resolveDeptTreeRoots(List allDepts, Map deptMap, Long rootDeptId) { + if (rootDeptId != null) { + SysDept root = deptMap.get(rootDeptId); + return root == null ? Collections.emptyList() : Collections.singletonList(root); + } + Set 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 flattenDeptTreeStats(List tree) { + List rows = new ArrayList<>(); + flattenDeptTreeNode(tree, rows, 0); + return rows; + } + + private void flattenDeptTreeNode(List nodes, List 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 buildEventListQuery(AppAccessStatsQueryDTO dto, TimeRange range) { + LambdaQueryWrapper 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 byDeptWithLimit(AppAccessStatsQueryDTO dto, int maxTopN) { TimeRange range = resolveRange(dto); QueryWrapper 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 uvKeys = new HashSet<>(); + private final Set 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(); + } + } }