dev add method link
Test Workflow / test (push) Has been cancelled

This commit is contained in:
2026-05-05 13:44:49 +07:00
parent 6793bbfc5c
commit bb43b6ef4f
8 changed files with 328 additions and 4 deletions
@@ -47,7 +47,8 @@ public class HandlerConfig {
StatisticSubscriberHandler statisticSubscriberHandler, StatisticSubscriberHandler statisticSubscriberHandler,
StatisticTokenHandler statisticTokenHandler, StatisticTokenHandler statisticTokenHandler,
StatisticIncomeHandler statisticIncomeHandler, StatisticIncomeHandler statisticIncomeHandler,
MonitoringStatisticHandler monitoringStatisticHandler MonitoringStatisticHandler monitoringStatisticHandler,
DockViewFileHandler dockViewFileHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
@@ -87,6 +88,7 @@ public class HandlerConfig {
map.put(30025, statisticTokenHandler); map.put(30025, statisticTokenHandler);
map.put(30026, statisticIncomeHandler); map.put(30026, statisticIncomeHandler);
map.put(30027, monitoringStatisticHandler); map.put(30027, monitoringStatisticHandler);
map.put(30028, dockViewFileHandler);
return map; return map;
} }
@@ -18,4 +18,20 @@ public class DocViewerRequest {
@JsonProperty("user_agent") @JsonProperty("user_agent")
private String userAgent; 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<DockViewStatsResponse> content;
private List<DockViewResponse> history;
private Integer pageNumber; private Integer pageNumber;
private Integer pageSize; 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> findAllByFileIdOrderByCreatedAtDesc(String fileId, Pageable pageable);
Page<DockView> findAllByFileId(Pageable pageable, List<String> fileId);
Page<DockView> findAllByViewerIdOrderByCreatedAtDesc(Long viewerId, Pageable pageable); Page<DockView> findAllByViewerIdOrderByCreatedAtDesc(Long viewerId, Pageable pageable);
long countByFileId(String fileId); long countByFileId(String fileId);
@@ -59,6 +61,18 @@ public interface DockViewRepository extends JpaRepository<DockView, Long> {
"GROUP BY dv.fileId") "GROUP BY dv.fileId")
Optional<Object[]> findViewsStatsByFileId(@Param("fileId") String 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 deleteAllByFileId(String fileId);
void deleteAllByCreatedAtBefore(LocalDateTime date); void deleteAllByCreatedAtBefore(LocalDateTime date);
@@ -3,13 +3,21 @@ package ru.soune.nocopy.service.dockview;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.docviewer.DocViewerRequest; 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.dto.docviewer.DockViewStatsResponse;
import ru.soune.nocopy.entity.docviewer.DockView; 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.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.time.LocalDateTime;
import java.util.*; import java.util.*;
@@ -22,6 +30,12 @@ public class DockViewService {
private final DockViewRepository dockViewRepository; private final DockViewRepository dockViewRepository;
private final GeoCountryService geoCountryService;
private final FileEntityRepository fileEntityRepository;
private final UserRepository userRepository;
@Transactional @Transactional
public DockView createDockView(DocViewerRequest request, Long userId) { public DockView createDockView(DocViewerRequest request, Long userId) {
DockView dockView = new DockView(); DockView dockView = new DockView();
@@ -65,6 +79,86 @@ public class DockViewService {
.build(); .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) { public DockViewStatsResponse getBasicStatsByFileId(String fileId) {
long totalViews = dockViewRepository.countByFileId(fileId); long totalViews = dockViewRepository.countByFileId(fileId);
long uniqueIps = dockViewRepository.countUniqueIpsByFileId(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.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.CityResponse;
import com.maxmind.geoip2.model.CountryResponse; import com.maxmind.geoip2.model.CountryResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
@@ -15,11 +16,17 @@ import java.net.InetAddress;
@Slf4j @Slf4j
public class GeoCountryService { public class GeoCountryService {
private final DatabaseReader dbReader; private final DatabaseReader countryDbReader;
private final DatabaseReader cityDbReader;
public GeoCountryService() throws IOException { public GeoCountryService() throws IOException {
Resource resource = new ClassPathResource("geo/GeoLite2-Country.mmdb"); 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) { 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 { private CountryResponse extractCountry(String input) throws IOException, GeoIp2Exception {
String domain = extractDomain(input); String domain = extractDomain(input);
InetAddress ip = InetAddress.getByName(domain); InetAddress ip = InetAddress.getByName(domain);
return dbReader.country(ip); return countryDbReader.country(ip);
} }
private String extractDomain(String input) { private String extractDomain(String input) {