@@ -47,7 +47,8 @@ public class HandlerConfig {
|
||||
StatisticSubscriberHandler statisticSubscriberHandler,
|
||||
StatisticTokenHandler statisticTokenHandler,
|
||||
StatisticIncomeHandler statisticIncomeHandler,
|
||||
MonitoringStatisticHandler monitoringStatisticHandler
|
||||
MonitoringStatisticHandler monitoringStatisticHandler,
|
||||
DockViewFileHandler dockViewFileHandler
|
||||
|
||||
) {
|
||||
Map<Integer, RequestHandler> 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -29,6 +29,8 @@ public class DockViewStatsResponse {
|
||||
|
||||
private List<DockViewStatsResponse> content;
|
||||
|
||||
private List<DockViewResponse> history;
|
||||
|
||||
private Integer pageNumber;
|
||||
|
||||
private Integer pageSize;
|
||||
|
||||
@@ -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<String> 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<String> 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<DockViewStatsResponse> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ public interface DockViewRepository extends JpaRepository<DockView, Long> {
|
||||
|
||||
Page<DockView> findAllByFileIdOrderByCreatedAtDesc(String fileId, Pageable pageable);
|
||||
|
||||
Page<DockView> findAllByFileId(Pageable pageable, List<String> fileId);
|
||||
|
||||
Page<DockView> findAllByViewerIdOrderByCreatedAtDesc(Long viewerId, Pageable pageable);
|
||||
|
||||
long countByFileId(String fileId);
|
||||
@@ -59,6 +61,18 @@ public interface DockViewRepository extends JpaRepository<DockView, Long> {
|
||||
"GROUP BY dv.fileId")
|
||||
Optional<Object[]> 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<Object[]> findViewsStatsByFileIds(@Param("fileIds") List<String> fileIds);
|
||||
|
||||
|
||||
void deleteAllByFileId(String fileId);
|
||||
|
||||
void deleteAllByCreatedAtBefore(LocalDateTime date);
|
||||
|
||||
@@ -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<DockViewStatsResponse> getStatsByFileIds(List<String> fileIds) {
|
||||
List<Object[]> statsList = dockViewRepository.findViewsStatsByFileIds(fileIds);
|
||||
|
||||
Map<String, Object[]> 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<String> fileIds) {
|
||||
Pageable pageable = PageRequest.of(page, size,
|
||||
Sort.by(Sort.Direction.fromString(sortDirection), sortBy));
|
||||
|
||||
Page<DockView> dockViews = dockViewRepository.findAllByFileId(pageable, fileIds);
|
||||
|
||||
if (dockViews.isEmpty()) {
|
||||
return DockViewStatsResponse.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
List<DockViewResponse> 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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user