From bb43b6ef4f03674bc4280650e73bf0e85a78f7e6 Mon Sep 17 00:00:00 2001 From: backdev-1 Date: Tue, 5 May 2026 13:44:49 +0700 Subject: [PATCH] dev add method link --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../dto/docviewer/DocViewerRequest.java | 16 +++ .../dto/docviewer/DockViewResponse.java | 31 +++++ .../dto/docviewer/DockViewStatsResponse.java | 2 + .../nocopy/handler/DockViewFileHandler.java | 122 ++++++++++++++++++ .../nocopy/repository/DockViewRepository.java | 14 ++ .../service/dockview/DockViewService.java | 94 ++++++++++++++ .../nocopy/service/geo/GeoCountryService.java | 49 ++++++- 8 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/docviewer/DockViewResponse.java create mode 100644 src/main/java/ru/soune/nocopy/handler/DockViewFileHandler.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 1306b7b..0939be6 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -47,7 +47,8 @@ public class HandlerConfig { StatisticSubscriberHandler statisticSubscriberHandler, StatisticTokenHandler statisticTokenHandler, StatisticIncomeHandler statisticIncomeHandler, - MonitoringStatisticHandler monitoringStatisticHandler + MonitoringStatisticHandler monitoringStatisticHandler, + DockViewFileHandler dockViewFileHandler ) { Map map = new HashMap<>(); @@ -87,6 +88,7 @@ public class HandlerConfig { map.put(30025, statisticTokenHandler); map.put(30026, statisticIncomeHandler); map.put(30027, monitoringStatisticHandler); + map.put(30028, dockViewFileHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/docviewer/DocViewerRequest.java b/src/main/java/ru/soune/nocopy/dto/docviewer/DocViewerRequest.java index e884473..be5622e 100644 --- a/src/main/java/ru/soune/nocopy/dto/docviewer/DocViewerRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/docviewer/DocViewerRequest.java @@ -18,4 +18,20 @@ public class DocViewerRequest { @JsonProperty("user_agent") private String userAgent; + + @JsonProperty("page") + private int page = 0; + + @JsonProperty("size") + private int size = 10; + + @JsonProperty("created_at") + private String sortBy = "createdAt"; + + @JsonProperty("sort_direction") + private String sortDirection = "desc"; + + @JsonProperty("action") + private String action; + } diff --git a/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewResponse.java b/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewResponse.java new file mode 100644 index 0000000..c26bfbb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewResponse.java @@ -0,0 +1,31 @@ +package ru.soune.nocopy.dto.docviewer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DockViewResponse { + + @JsonProperty("city") + private String city; + + @JsonProperty("country") + private String country; + + @JsonProperty("viewer") + private String viewer; + + @JsonProperty("document") + private String document; + + @JsonProperty("view_date") + private LocalDateTime viewDate; +} diff --git a/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewStatsResponse.java b/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewStatsResponse.java index ae821d2..9d5d440 100644 --- a/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewStatsResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/docviewer/DockViewStatsResponse.java @@ -29,6 +29,8 @@ public class DockViewStatsResponse { private List content; + private List history; + private Integer pageNumber; private Integer pageSize; diff --git a/src/main/java/ru/soune/nocopy/handler/DockViewFileHandler.java b/src/main/java/ru/soune/nocopy/handler/DockViewFileHandler.java new file mode 100644 index 0000000..b5d3f5c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/DockViewFileHandler.java @@ -0,0 +1,122 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.docviewer.DocViewerRequest; +import ru.soune.nocopy.dto.docviewer.DockViewStatsResponse; +import ru.soune.nocopy.dto.file.ActionResponse; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.service.dockview.DockViewService; +import ru.soune.nocopy.service.file.FileEntityService; +import ru.soune.nocopy.service.register.AuthService; + +import java.util.Arrays; +import java.util.List; + +@Component +@RequiredArgsConstructor +@Slf4j +public class DockViewFileHandler implements RequestHandler { + + private final DockViewService dockViewService; + + private final FileEntityService fileEntityService; + + private final AuthService authService; + + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + DocViewerRequest docViewerRequest = objectMapper.convertValue(request.getMessageBody(), DocViewerRequest.class); + String action = docViewerRequest.getAction(); + + switch (action) { + case "history_view": + return handleGetHistory(request, docViewerRequest); + case "view_stats": + return handleGetViewStats(request, docViewerRequest); + default: + ActionResponse response = ActionResponse.builder() + .action(action) + .availableActions(Arrays.asList("history_view", "view_stats")) + .build(); + + return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, response); + } + } + + private BaseResponse handleGetHistory(BaseRequest request, DocViewerRequest docViewerRequest) { + try { + Long userId = authService.useUserAuthToken(docViewerRequest.getToken()); + List fileIds = fileEntityService.getAllUserFiles(userId) + .stream() + .map(FileEntity::getId) + .toList(); + + if (fileIds.isEmpty()) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + "Files for search not found", + null); + } + + DockViewStatsResponse openLastHistory = dockViewService.getOpenLastHistory(docViewerRequest.getPage(), + docViewerRequest.getSize(), docViewerRequest.getSortDirection(), docViewerRequest.getSortBy(), + fileIds); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + openLastHistory); + + } catch (NotFoundAuthToken e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Authentication required", + null); + } catch (Exception e) { + log.error("Error getting view history", e); + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + "Failed to get file view history: " + e.getMessage(), + null); + } + } + + private BaseResponse handleGetViewStats(BaseRequest request, DocViewerRequest docViewerRequest) { + try { + Long userId = authService.useUserAuthToken(docViewerRequest.getToken()); + List fileIds = fileEntityService.getAllUserFiles(userId) + .stream() + .map(FileEntity::getId) + .toList(); + + if (fileIds.isEmpty()) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + "Files for search not found", + null); + } + + List statsByFileIds = dockViewService.getStatsByFileIds(fileIds); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statsByFileIds); + } catch (NotFoundAuthToken e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Authentication required", + null); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/DockViewRepository.java b/src/main/java/ru/soune/nocopy/repository/DockViewRepository.java index e8fc429..4a76fff 100644 --- a/src/main/java/ru/soune/nocopy/repository/DockViewRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/DockViewRepository.java @@ -25,6 +25,8 @@ public interface DockViewRepository extends JpaRepository { Page findAllByFileIdOrderByCreatedAtDesc(String fileId, Pageable pageable); + Page findAllByFileId(Pageable pageable, List fileId); + Page findAllByViewerIdOrderByCreatedAtDesc(Long viewerId, Pageable pageable); long countByFileId(String fileId); @@ -59,6 +61,18 @@ public interface DockViewRepository extends JpaRepository { "GROUP BY dv.fileId") Optional findViewsStatsByFileId(@Param("fileId") String fileId); + @Query("SELECT dv.fileId, " + + "COUNT(dv) as totalViews, " + + "COUNT(DISTINCT dv.viewerIp) as uniqueIps, " + + "COUNT(DISTINCT dv.viewerId) as uniqueUsers, " + + "MAX(dv.createdAt) as lastViewDate, " + + "MIN(dv.createdAt) as firstViewDate " + + "FROM DockView dv " + + "WHERE dv.fileId IN :fileIds " + + "GROUP BY dv.fileId") + List findViewsStatsByFileIds(@Param("fileIds") List fileIds); + + void deleteAllByFileId(String fileId); void deleteAllByCreatedAtBefore(LocalDateTime date); diff --git a/src/main/java/ru/soune/nocopy/service/dockview/DockViewService.java b/src/main/java/ru/soune/nocopy/service/dockview/DockViewService.java index c4f0ac2..0c831ca 100644 --- a/src/main/java/ru/soune/nocopy/service/dockview/DockViewService.java +++ b/src/main/java/ru/soune/nocopy/service/dockview/DockViewService.java @@ -3,13 +3,21 @@ package ru.soune.nocopy.service.dockview; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.docviewer.DocViewerRequest; +import ru.soune.nocopy.dto.docviewer.DockViewResponse; import ru.soune.nocopy.dto.docviewer.DockViewStatsResponse; import ru.soune.nocopy.entity.docviewer.DockView; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.repository.DockViewRepository; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.geo.GeoCountryService; import java.time.LocalDateTime; import java.util.*; @@ -22,6 +30,12 @@ public class DockViewService { private final DockViewRepository dockViewRepository; + private final GeoCountryService geoCountryService; + + private final FileEntityRepository fileEntityRepository; + + private final UserRepository userRepository; + @Transactional public DockView createDockView(DocViewerRequest request, Long userId) { DockView dockView = new DockView(); @@ -65,6 +79,86 @@ public class DockViewService { .build(); } + public List getStatsByFileIds(List fileIds) { + List statsList = dockViewRepository.findViewsStatsByFileIds(fileIds); + + Map statsMap = statsList.stream() + .collect(Collectors.toMap( + stats -> (String) stats[0], + stats -> stats + )); + + return fileIds.stream() + .map(fileId -> { + Object[] stats = statsMap.get(fileId); + + if (stats == null) { + return DockViewStatsResponse.builder() + .fileId(fileId) + .totalViews(0L) + .uniqueIps(0L) + .uniqueUsers(0L) + .firstViewDate(null) + .lastViewDate(null) + .build(); + } + + return DockViewStatsResponse.builder() + .fileId((String) stats[0]) + .totalViews((Long) stats[1]) + .uniqueIps((Long) stats[2]) + .uniqueUsers((Long) stats[3]) + .lastViewDate((LocalDateTime) stats[4]) + .firstViewDate((LocalDateTime) stats[5]) + .build(); + }) + .collect(Collectors.toList()); + } + + public DockViewStatsResponse getOpenLastHistory(int page, int size, String sortDirection, String sortBy, + List fileIds) { + Pageable pageable = PageRequest.of(page, size, + Sort.by(Sort.Direction.fromString(sortDirection), sortBy)); + + Page dockViews = dockViewRepository.findAllByFileId(pageable, fileIds); + + if (dockViews.isEmpty()) { + return DockViewStatsResponse.builder() + .build(); + } + + List history = new ArrayList<>(); + + for (DockView dockView: dockViews) { + String country = geoCountryService.getCountryByIp(dockView.getViewerIp()); + String city = geoCountryService.getCityByIp(dockView.getViewerIp()); + LocalDateTime createdAt = dockView.getCreatedAt(); + String fileId = dockView.getFileId(); + + FileEntity file = fileEntityRepository.findByFileId(fileId); + Long viewerId = dockView.getViewerId(); + User viewer = userRepository.findById(viewerId).orElse(null); + + DockViewResponse response = DockViewResponse.builder() + .city(city) + .country(country) + .viewDate(createdAt) + .document(file.getOriginalFileName()) + .viewer(viewer == null ? "guest" : viewer.getFullName()) + .build(); + + history.add(response); + } + + return DockViewStatsResponse.builder() + .history(history) + .pageNumber(dockViews.getNumber()) + .pageSize(dockViews.getSize()) + .totalPages(dockViews.getTotalPages()) + .totalElements(dockViews.getTotalElements()) + .build(); + } + public DockViewStatsResponse getBasicStatsByFileId(String fileId) { long totalViews = dockViewRepository.countByFileId(fileId); long uniqueIps = dockViewRepository.countUniqueIpsByFileId(fileId); diff --git a/src/main/java/ru/soune/nocopy/service/geo/GeoCountryService.java b/src/main/java/ru/soune/nocopy/service/geo/GeoCountryService.java index 68ca2e6..f005a93 100644 --- a/src/main/java/ru/soune/nocopy/service/geo/GeoCountryService.java +++ b/src/main/java/ru/soune/nocopy/service/geo/GeoCountryService.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.service.geo; import com.maxmind.geoip2.DatabaseReader; import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.CountryResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ClassPathResource; @@ -15,11 +16,17 @@ import java.net.InetAddress; @Slf4j public class GeoCountryService { - private final DatabaseReader dbReader; + private final DatabaseReader countryDbReader; + + private final DatabaseReader cityDbReader; public GeoCountryService() throws IOException { Resource resource = new ClassPathResource("geo/GeoLite2-Country.mmdb"); - this.dbReader = new DatabaseReader.Builder(resource.getInputStream()).build(); + this.countryDbReader = new DatabaseReader.Builder(resource.getInputStream()).build(); + + Resource cityResource = new ClassPathResource("geo/GeoLite2-City.mmdb"); + this.cityDbReader = new DatabaseReader.Builder(cityResource.getInputStream()).build(); + } public String getCountryName(String input) { @@ -44,12 +51,48 @@ public class GeoCountryService { } } + public String getCityByIp(String ip) { + try { + InetAddress ipAddress = InetAddress.getByName(ip); + CityResponse response = cityDbReader.city(ipAddress); + + return response.getCity().getName(); + } catch (Exception e) { + log.error("Failed to get city for IP: {}", ip, e); + return "UNKNOWN"; + } + } + + public String getCountryByIp(String ip) { + try { + InetAddress ipAddress = InetAddress.getByName(ip); + CountryResponse response = countryDbReader.country(ipAddress); + + return response.getCountry().getName(); + } catch (Exception e) { + log.error("Failed to get country for IP: {}", ip, e); + return "UNKNOWN"; + } + } + + public String getCountryCodeByIp(String ip) { + try { + InetAddress ipAddress = InetAddress.getByName(ip); + CountryResponse response = countryDbReader.country(ipAddress); + + return response.getCountry().getIsoCode(); + } catch (Exception e) { + log.error("Failed to get country code for IP: {}", ip, e); + return "UNKNOWN"; + } + } + private CountryResponse extractCountry(String input) throws IOException, GeoIp2Exception { String domain = extractDomain(input); InetAddress ip = InetAddress.getByName(domain); - return dbReader.country(ip); + return countryDbReader.country(ip); } private String extractDomain(String input) {