diff --git a/core/event-service/pom.xml b/core/event-service/pom.xml
index e38dce8..dc34144 100644
--- a/core/event-service/pom.xml
+++ b/core/event-service/pom.xml
@@ -1,132 +1,133 @@
-
4.0.0
+
ru.practicum
core
0.0.1-SNAPSHOT
+
event-service
- 22
- 22
+ 21
+ 21
UTF-8
+ 5.0.0
-
- org.springframework.data
- spring-data-jpa
-
-
- org.projectlombok
- lombok
- provided
-
-
- org.apache.tomcat.embed
- tomcat-embed-core
-
-
- com.querydsl
- querydsl-jpa
- jakarta
- 5.1.0
-
+
ru.practicum
interaction-api
0.0.1-SNAPSHOT
+
- org.mapstruct
- mapstruct
- 1.6.2
- provided
-
-
- org.mapstruct
- mapstruct-processor
- 1.6.2
- provided
+ org.springframework.boot
+ spring-boot-starter-web
+
- com.netflix.spectator
- spectator-api
- 1.7.3
- compile
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
- jakarta.persistence
- jakarta.persistence-api
+ org.springframework.boot
+ spring-boot-starter-actuator
+
- org.hibernate.orm
- hibernate-core
+ org.springframework.boot
+ spring-boot-starter-validation
+
- org.springframework.retry
- spring-retry
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
- org.springframework.cloud
- spring-cloud-starter-netflix-eureka-client
- 3.1.8
+ org.postgresql
+ postgresql
+ runtime
+
- org.springframework.cloud
- spring-cloud-config-client
- 4.2.0
+ com.h2database
+ h2
+ runtime
+
- org.springframework.boot
- spring-boot-starter-web
+ org.mapstruct
+ mapstruct
+ 1.5.5.Final
+
- org.springframework.boot
- spring-boot-starter-data-jpa
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
- org.postgresql
- postgresql
+ com.querydsl
+ querydsl-apt
+ ${querydsl.version}
+ jakarta
+ provided
+
- org.springframework.cloud
- spring-cloud-starter-openfeign
+ com.querydsl
+ querydsl-jpa
+ jakarta
+ ${querydsl.version}
- ru.practicum
- stats-client
- 0.0.1-SNAPSHOT
- compile
+ org.projectlombok
+ lombok
+ provided
-
-
-
-
- org.springframework.cloud
- spring-cloud-dependencies
- 2024.0.0
- pom
- import
-
-
-
+
org.springframework.boot
spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
-
- paketobuildpacks/builder-jammy-base:latest
-
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
@@ -144,36 +145,8 @@
-
-
- com.querydsl
- querydsl-apt
- jakarta
- 5.1.0
-
-
-
-
- org.codehaus.mojo
- build-helper-maven-plugin
- 3.3.0
-
-
- add-source
- generate-sources
-
- add-source
-
-
-
- ${project.build.directory}/generated-sources/java/
-
-
-
-
-
\ No newline at end of file
diff --git a/core/event-service/src/main/java/ru/practicum/controller/PublicEventController.java b/core/event-service/src/main/java/ru/practicum/controller/PublicEventController.java
index 6e23883..d4ba46c 100644
--- a/core/event-service/src/main/java/ru/practicum/controller/PublicEventController.java
+++ b/core/event-service/src/main/java/ru/practicum/controller/PublicEventController.java
@@ -5,14 +5,20 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
+import ru.practicum.AnalyzerClient;
+import ru.practicum.CollectorClient;
import ru.practicum.dto.event.EventFullDto;
+import ru.practicum.dto.event.EventRecommendationDto;
import ru.practicum.dto.event.EventShortDto;
+import ru.practicum.ewm.stats.proto.ActionTypeProto;
+import ru.practicum.ewm.stats.proto.RecommendationsMessages;
import ru.practicum.exception.IncorrectValueException;
import ru.practicum.service.event.EventSearchParams;
import ru.practicum.service.event.EventService;
import ru.practicum.service.event.PublicSearchParams;
import java.time.LocalDateTime;
+import java.util.ArrayList;
import java.util.List;
import static ru.practicum.constant.Constant.PATTERN_DATE;
@@ -25,7 +31,13 @@
@RequiredArgsConstructor
@Slf4j
public class PublicEventController {
+
+ private final CollectorClient collectorClient;
+
+ private final AnalyzerClient analyzerClient;
+
private final EventService eventService;
+ private static final String X_EWM_USER_ID_HEADER = "X-EWM-USER-ID";
/**
* Gets events.
@@ -83,21 +95,32 @@ public List getAll(
return eventShortDtoList;
}
- /**
- * Gets event by id.
- *
- * @param eventId the event id
- * @param request the request
- * @return the event by id
- */
@GetMapping("/{event-id}")
- public EventFullDto getEventById(@PathVariable("event-id") Long eventId, HttpServletRequest request) {
+ public EventFullDto getEventById(@PathVariable("event-id") Long eventId, @RequestHeader(X_EWM_USER_ID_HEADER) long userId) {
log.info("Получение информации о событии с id={}", eventId);
- // Получение IP клиента
- String clientIp = request.getRemoteAddr();
-
// Получение события через сервис
- return eventService.getEventById(eventId, clientIp);
+ return eventService.getEventById(eventId, userId);
+ }
+
+ @GetMapping("/recommendations")
+ public List getRecommendations(@RequestHeader(X_EWM_USER_ID_HEADER) long userId,
+ @RequestParam(defaultValue = "10") int maxResults) {
+ var recommendationStream = analyzerClient.getRecommendationsForUser(userId, maxResults);
+ var recommendationList = recommendationStream.toList();
+
+ List result = new ArrayList<>();
+ for (RecommendationsMessages.RecommendedEventProto requestProto : recommendationList) {
+ result.add(new EventRecommendationDto(requestProto.getEventId(), requestProto.getScore()));
+ }
+ return result;
+ }
+
+ @PutMapping("/{event-id}/like")
+ public void likeEvent(@PathVariable("event-id") Long eventId,
+ @RequestHeader(X_EWM_USER_ID_HEADER) long userId) {
+ eventService.addLike(userId, eventId);
+
+ collectorClient.sendUserAction(userId, eventId, ActionTypeProto.ACTION_LIKE);
}
}
diff --git a/core/event-service/src/main/java/ru/practicum/mapper/event/UtilEventClass.java b/core/event-service/src/main/java/ru/practicum/mapper/event/UtilEventClass.java
index a70527f..baadf02 100644
--- a/core/event-service/src/main/java/ru/practicum/mapper/event/UtilEventClass.java
+++ b/core/event-service/src/main/java/ru/practicum/mapper/event/UtilEventClass.java
@@ -128,7 +128,7 @@ public Event updateEvent(Event updatedEvent, UpdateEventAdminRequest request, Ca
request.getRequestModeration() : updatedEvent.getRequestModeration())
.state(updatedEvent.getState())
.title(request.getTitle() != null ? request.getTitle() : updatedEvent.getTitle())
- .views(updatedEvent.getViews())
+ .rating(updatedEvent.getRating())
.build();
}
@@ -160,7 +160,7 @@ public EventFullDto toEventFullDto(Event event) {
eventFullDto.setRequestModeration(event.getRequestModeration());
eventFullDto.setState(event.getState());
eventFullDto.setTitle(event.getTitle());
- eventFullDto.setViews(event.getViews());
+ eventFullDto.setRating(event.getRating());
eventFullDto.setEventDate(event.getEventDate().format(formatter));
return eventFullDto;
diff --git a/core/event-service/src/main/java/ru/practicum/model/Event.java b/core/event-service/src/main/java/ru/practicum/model/Event.java
index 93c5b4e..2e7cd3b 100644
--- a/core/event-service/src/main/java/ru/practicum/model/Event.java
+++ b/core/event-service/src/main/java/ru/practicum/model/Event.java
@@ -55,7 +55,7 @@ public class Event {
EventState state;
String title;
@Transient
- Long views;
+ double rating;
@Transient
Long likes;
}
diff --git a/core/event-service/src/main/java/ru/practicum/repository/EventRepository.java b/core/event-service/src/main/java/ru/practicum/repository/EventRepository.java
index 723a1c0..d0832d9 100644
--- a/core/event-service/src/main/java/ru/practicum/repository/EventRepository.java
+++ b/core/event-service/src/main/java/ru/practicum/repository/EventRepository.java
@@ -1,7 +1,9 @@
package ru.practicum.repository;
+import jakarta.transaction.Transactional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
@@ -77,4 +79,18 @@ List findAllEvents(@Param("text") String text,
@Query(value = "SELECT COUNT(*) FROM LIKES_EVENTS WHERE EVENT_ID = :eventId", nativeQuery = true)
long countLikesByEventId(Long eventId);
+
+ @Query(value = "SELECT EXISTS (" +
+ "SELECT * FROM LIKES_EVENTS WHERE USER_ID = :userId AND EVENT_ID = :eventId)", nativeQuery = true)
+ boolean checkLikeExistence(long userId, long eventId);
+
+ @Modifying
+ @Transactional
+ @Query(value = "INSERT INTO LIKES_EVENTS (USER_ID, EVENT_ID) values (:userId, :eventId)", nativeQuery = true)
+ void addLike(Long userId, Long eventId);
+
+ @Modifying
+ @Transactional
+ @Query(value = "DELETE FROM LIKES_EVENTS WHERE USER_ID = :userId AND EVENT_ID = :eventId", nativeQuery = true)
+ void deleteLike(Long userId, Long eventId);
}
diff --git a/core/event-service/src/main/java/ru/practicum/service/event/EventService.java b/core/event-service/src/main/java/ru/practicum/service/event/EventService.java
index ac758c6..d03b017 100644
--- a/core/event-service/src/main/java/ru/practicum/service/event/EventService.java
+++ b/core/event-service/src/main/java/ru/practicum/service/event/EventService.java
@@ -89,19 +89,16 @@ List getEvents(String text, List categories, Boolean paid,
LocalDateTime rangeStart, LocalDateTime rangeEnd,
Boolean onlyAvailable, String sort, int from, int size, String clientIp);
- /**
- * Gets event by id.
- *
- * @param id the id
- * @param clientIp the client ip
- * @return the event by id
- */
- EventFullDto getEventById(Long id, String clientIp);
+ EventFullDto getEventById(Long id, long userId);
EventFullDto getByIdInternal(long eventId);
@Transactional(readOnly = true)
List getAllByPublic(EventSearchParams searchParams, Boolean onlyAvailable, String sort, String clientIp);
+
+ EventShortDto addLike(long userId, long eventId);
+
+ void deleteLike(long userId, long eventId);
}
diff --git a/core/event-service/src/main/java/ru/practicum/service/event/EventServiceImpl.java b/core/event-service/src/main/java/ru/practicum/service/event/EventServiceImpl.java
index 4fafb81..71e6bbd 100644
--- a/core/event-service/src/main/java/ru/practicum/service/event/EventServiceImpl.java
+++ b/core/event-service/src/main/java/ru/practicum/service/event/EventServiceImpl.java
@@ -9,16 +9,16 @@
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import ru.practicum.StatClient;
+import ru.practicum.AnalyzerClient;
+import ru.practicum.CollectorClient;
import ru.practicum.client.RequestServiceClient;
import ru.practicum.client.UserServiceClient;
-import ru.practicum.dto.EndpointHitDto;
-import ru.practicum.dto.ViewStatsDto;
import ru.practicum.dto.category.CategoryDto;
import ru.practicum.dto.event.*;
import ru.practicum.enums.AdminStateAction;
import ru.practicum.enums.EventState;
import ru.practicum.enums.RequestStatus;
+import ru.practicum.ewm.stats.proto.ActionTypeProto;
import ru.practicum.exception.ConflictException;
import ru.practicum.exception.NotFoundException;
import ru.practicum.exception.ValidationException;
@@ -46,6 +46,8 @@
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class EventServiceImpl implements EventService {
+ private final CollectorClient collectorClient;
+
private final UserServiceClient userServiceClient;
private final LocationMapper locationMapper;
@@ -59,16 +61,17 @@ public class EventServiceImpl implements EventService {
LocationRepository locationRepository;
SearchEventRepository searchEventRepository;
CategoryRepository categoryRepository;
- StatClient statClient;
+ AnalyzerClient analyzerClient;
@Autowired
public EventServiceImpl(EventRepository eventRepository,
RequestServiceClient requestServiceClient,
EventMapper eventMapper, CategoryService categoryService, UtilEventClass utilEventClass,
LocationRepository locationRepository, SearchEventRepository searchEventRepository,
- CategoryRepository categoryRepository, StatClient statClient,
+ CategoryRepository categoryRepository,
LocationMapper locationMapper,
- UserServiceClient userServiceClient) {
+ UserServiceClient userServiceClient, AnalyzerClient analyzerClient,
+ CollectorClient collectorClient) {
this.eventRepository = eventRepository;
this.requestServiceClient = requestServiceClient;
this.eventMapper = eventMapper;
@@ -77,9 +80,10 @@ public EventServiceImpl(EventRepository eventRepository,
this.locationRepository = locationRepository;
this.searchEventRepository = searchEventRepository;
this.categoryRepository = categoryRepository;
- this.statClient = statClient;
+ this.analyzerClient = analyzerClient;
this.locationMapper = locationMapper;
this.userServiceClient = userServiceClient;
+ this.collectorClient = collectorClient;
}
@Override
@@ -213,12 +217,6 @@ public List getEvents(String text, List categories, Boolean
if (Boolean.TRUE.equals(text == null && categories == null && paid == null && rangeStart == null && rangeEnd == null
&& !onlyAvailable && sort == null && from == 0) && size == 10) {
- log.info("==> Статистика: вызов метода getEvents с пустыми параметрами от клиента {}", clientIp);
-
- // Записываем статистику
- saveEventsRequestToStats(clientIp);
-
- // Возвращаем пустой список
return Collections.emptyList();
}
@@ -256,21 +254,6 @@ public List getEvents(String text, List categories, Boolean
.map(event -> "/events/" + event.getId()) // Получаем URI для каждого мероприятия
.toList();
- // Запрашиваем статистику просмотров с использованием StatClient
- List viewStats = statClient.getStats(rangeStart.toString(), rangeEnd.toString(), uris, true);
-
- // Обработка случая, если статистика отсутствует
- if (viewStats == null || viewStats.isEmpty()) {
- log.warn("Сервис статистики вернул пустой результат или null");
- viewStats = Collections.emptyList();
- }
-
- // Заполняем Map с количеством просмотров
- for (ViewStatsDto stat : viewStats) {
- Long eventId = Long.valueOf(stat.getUri().substring(stat.getUri().lastIndexOf("/") + 1));
- eventViews.put(eventId, Math.toIntExact(stat.getHits()));
- }
-
// Сортировка
if ("VIEWS".equalsIgnoreCase(sort)) {
// Сортировка по количеству просмотров
@@ -283,10 +266,7 @@ public List getEvents(String text, List categories, Boolean
// Сортировка по дате события
filteredEvents.sort(Comparator.comparing(Event::getEventDate));
}
- log.info("Передаем запрос в статистику");
- // Логируем запрос в статистику
- saveEventsRequestToStats(clientIp);
// Применяем пагинацию
int start = Math.min(from, filteredEvents.size());
@@ -299,7 +279,7 @@ public List getEvents(String text, List categories, Boolean
}
@Override
- public EventFullDto getEventById(Long eventId, String clientIp) {
+ public EventFullDto getEventById(Long eventId, long userId) {
// Проверка существования события
Event event = eventRepository.findById(eventId).orElseThrow(
() -> new NotFoundException("Event with id=" + eventId + " not found!", "")
@@ -310,84 +290,79 @@ public EventFullDto getEventById(Long eventId, String clientIp) {
throw new NotFoundException("Event with id=" + eventId + " is not published yet!", "");
}
- // Увеличение количества просмотров
- saveEventRequestToStats(event, clientIp);
-
- // Получение количества просмотров из статистики
- long views = getViewsFromStats(event);
-
- event.setViews(views);
eventRepository.save(event);
+ collectorClient.sendUserAction(userId, eventId, ActionTypeProto.ACTION_VIEW);
+
// Подсчет подтвержденных запросов
long confirmedRequests = requestServiceClient.countByStatusAndEventId(RequestStatus.CONFIRMED, eventId);
// Создание DTO
EventFullDto eventFullDto = utilEventClass.toEventFullDto(event);
- eventFullDto.setViews(views);
eventFullDto.setConfirmedRequests(confirmedRequests);
- return eventFullDto;
- }
-
- private void saveEventsRequestToStats(String clientIp) {
- try {
- // Создание объекта для статистики
- log.info("Создание объекта для статистики");
- EndpointHitDto hitDto = new EndpointHitDto();
- hitDto.setApp("ewm-main-service");
- hitDto.setUri("/events");
- hitDto.setIp(clientIp);
- hitDto.setTimestamp(LocalDateTime.now().format(dateTimeFormatter));
-
- // Логируем успешный запрос
- log.info("Логируем запрос в статистику: URI={}, IP={}", hitDto.getUri(), hitDto.getIp());
-
- // Отправка статистики
- statClient.saveHit(hitDto);
- } catch (Exception e) {
- log.error("Ошибка при сохранении статистики для URI=/events, IP=" + clientIp, e);
- }
- }
- private void saveEventRequestToStats(Event event, String clientIp) {
- try {
- EndpointHitDto hitDto = new EndpointHitDto();
- hitDto.setApp("ewm-main-service");
- hitDto.setUri("/events/" + event.getId());
- hitDto.setIp(clientIp);
- hitDto.setTimestamp(LocalDateTime.now().format(dateTimeFormatter));
-
- statClient.saveHit(hitDto);
- } catch (Exception e) {
- log.error("Ошибка при сохранении статистики для события id=" + event.getId(), e);
- }
+ return eventFullDto;
}
- private long getViewsFromStats(Event event) {
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
- try {
- String uri = "/events/" + event.getId();
- // Добавляем одну секунду к началу и завершению диапазона
- String start = event.getCreatedOn().minusSeconds(1).format(formatter);
- String end = LocalDateTime.now().plusSeconds(1).format(formatter);
-
- List stats = statClient.getStats(
- start,
- end,
- List.of(uri),
- true
- );
-
- return stats.stream()
- .filter(stat -> stat.getUri().equals(uri))
- .mapToLong(ViewStatsDto::getHits)
- .sum();
- } catch (Exception e) {
- log.error("Ошибка при получении статистики просмотров для события id=" + event.getId(), e);
- return 0;
- }
- }
+// private void saveEventsRequestToStats(String clientIp) {
+// try {
+// // Создание объекта для статистики
+// log.info("Создание объекта для статистики");
+// EndpointHitDto hitDto = new EndpointHitDto();
+// hitDto.setApp("ewm-main-service");
+// hitDto.setUri("/events");
+// hitDto.setIp(clientIp);
+// hitDto.setTimestamp(LocalDateTime.now().format(dateTimeFormatter));
+//
+// // Логируем успешный запрос
+// log.info("Логируем запрос в статистику: URI={}, IP={}", hitDto.getUri(), hitDto.getIp());
+//
+// // Отправка статистики
+// statClient.saveHit(hitDto);
+// } catch (Exception e) {
+// log.error("Ошибка при сохранении статистики для URI=/events, IP=" + clientIp, e);
+// }
+// }
+
+// private void saveEventRequestToStats(Event event, String clientIp) {
+// try {
+// EndpointHitDto hitDto = new EndpointHitDto();
+// hitDto.setApp("ewm-main-service");
+// hitDto.setUri("/events/" + event.getId());
+// hitDto.setIp(clientIp);
+// hitDto.setTimestamp(LocalDateTime.now().format(dateTimeFormatter));
+//
+// statClient.saveHit(hitDto);
+// } catch (Exception e) {
+// log.error("Ошибка при сохранении статистики для события id=" + event.getId(), e);
+// }
+// }
+
+// private long getViewsFromStats(Event event) {
+// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+// try {
+// String uri = "/events/" + event.getId();
+// // Добавляем одну секунду к началу и завершению диапазона
+// String start = event.getCreatedOn().minusSeconds(1).format(formatter);
+// String end = LocalDateTime.now().plusSeconds(1).format(formatter);
+//
+// List stats = statClient.getStats(
+// start,
+// end,
+// List.of(uri),
+// true
+// );
+//
+// return stats.stream()
+// .filter(stat -> stat.getUri().equals(uri))
+// .mapToLong(ViewStatsDto::getHits)
+// .sum();
+// } catch (Exception e) {
+// log.error("Ошибка при получении статистики просмотров для события id=" + event.getId(), e);
+// return 0;
+// }
+// }
private void checkDateTime(LocalDateTime rangeStart, LocalDateTime rangeEnd) {
@@ -484,7 +459,7 @@ public List getAllByPublic(EventSearchParams searchParams, Boolea
Long view = 0L;
- event.setViews(view);
+ event.setRating(view);
event.setConfirmedRequests(
requestServiceClient.countByStatusAndEventId(RequestStatus.CONFIRMED, event.getId()));
event.setLikes(eventRepository.countLikesByEventId(event.getId()));
@@ -509,18 +484,6 @@ public List getAllByPublic(EventSearchParams searchParams, Boolea
.toList();
-
- List viewStats = statClient.getStats(rangeStart.toString(), rangeEnd.toString(), uris, true);
-
- if (viewStats == null || viewStats.isEmpty()) {
- log.warn("Сервис статистики вернул пустой результат или null");
- viewStats = Collections.emptyList();
- }
- for (ViewStatsDto stat : viewStats) {
- Long eventId = Long.valueOf(stat.getUri().substring(stat.getUri().lastIndexOf("/") + 1));
- eventViews.put(eventId, stat.getHits());
- }
-
if ("VIEWS".equalsIgnoreCase(sort)) {
// Сортировка по количеству просмотров
filteredEvents.sort((e1, e2) -> {
@@ -534,11 +497,35 @@ public List getAllByPublic(EventSearchParams searchParams, Boolea
}
log.info("Передаем запрос в статистику");
- // Логируем запрос в статистику
- saveEventsRequestToStats(clientIp);
-
return eventListBySearch.stream()
.map(eventMapper::toEventShortDto)
.toList();
}
+
+ @Override
+ public EventShortDto addLike(long userId, long eventId) {
+ Event event = eventRepository.findById(eventId).orElseThrow(
+ () -> new NotFoundException("Event with id = " + eventId, " not found")
+ );
+ if (event.getState() != EventState.PUBLISHED) {
+ throw new ConflictException("Event with id = ", eventId + " is not published");
+ }
+ eventRepository.addLike(userId, eventId);
+ event.setLikes(eventRepository.countLikesByEventId(eventId));
+ return eventMapper.toEventShortDto(event);
+ }
+
+ @Override
+ public void deleteLike(long userId, long eventId) {
+ Event event = eventRepository.findById(eventId).orElseThrow(
+ () -> new NotFoundException("Event with id = ", eventId + " not found")
+ );
+ boolean isLikeExists = eventRepository.checkLikeExistence(userId, eventId);
+ if (isLikeExists) {
+ eventRepository.deleteLike(userId, eventId);
+ } else {
+ throw new NotFoundException("Like for event: ", eventId
+ + " by user: " + userId + " not exists");
+ }
+ }
}
diff --git a/core/interaction-api/src/main/java/ru/practicum/dto/event/EventFullDto.java b/core/interaction-api/src/main/java/ru/practicum/dto/event/EventFullDto.java
index d53ede0..7b22ecf 100644
--- a/core/interaction-api/src/main/java/ru/practicum/dto/event/EventFullDto.java
+++ b/core/interaction-api/src/main/java/ru/practicum/dto/event/EventFullDto.java
@@ -36,6 +36,6 @@ public class EventFullDto {
boolean requestModeration;
EventState state;
String title;
- Long views;
+ double rating;
Long likes;
}
diff --git a/stats/stats-dto/src/main/java/ru/practicum/dto/ViewStatsDto.java b/core/interaction-api/src/main/java/ru/practicum/dto/event/EventRecommendationDto.java
similarity index 63%
rename from stats/stats-dto/src/main/java/ru/practicum/dto/ViewStatsDto.java
rename to core/interaction-api/src/main/java/ru/practicum/dto/event/EventRecommendationDto.java
index 99f252e..b88ffe3 100644
--- a/stats/stats-dto/src/main/java/ru/practicum/dto/ViewStatsDto.java
+++ b/core/interaction-api/src/main/java/ru/practicum/dto/event/EventRecommendationDto.java
@@ -1,4 +1,5 @@
-package ru.practicum.dto;
+package ru.practicum.dto.event;
+
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -6,15 +7,11 @@
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
-/**
- * The type View stats dto.
- */
@Data
-@NoArgsConstructor
@AllArgsConstructor
+@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
-public class ViewStatsDto {
- String app;
- String uri;
- Long hits;
-}
+public class EventRecommendationDto {
+ long eventId;
+ double score;
+}
\ No newline at end of file
diff --git a/core/interaction-api/src/main/java/ru/practicum/dto/event/EventShortDto.java b/core/interaction-api/src/main/java/ru/practicum/dto/event/EventShortDto.java
index a4366a2..aa7eb2f 100644
--- a/core/interaction-api/src/main/java/ru/practicum/dto/event/EventShortDto.java
+++ b/core/interaction-api/src/main/java/ru/practicum/dto/event/EventShortDto.java
@@ -24,7 +24,7 @@ public class EventShortDto {
UserShortDto initiator;
Boolean paid;
String title;
- Long views;
+ Long rating;
diff --git a/core/pom.xml b/core/pom.xml
index eb6fb25..a0a3336 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -17,4 +17,69 @@
interaction-api
request-service
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-config
+
+
+
+ org.springframework.retry
+ spring-retry
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-netflix-eureka-client
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
+
+
+ ru.practicum
+ aggregator
+ 0.0.1-SNAPSHOT
+ compile
+
+
+ ru.practicum
+ stats-client
+ 0.0.1-SNAPSHOT
+ compile
+
+
+ org.springframework
+ spring-tx
+
+
+ com.querydsl
+ querydsl-core
+ 5.0.0
+ compile
+
+
+ org.springframework.data
+ spring-data-commons
+
+
+ org.projectlombok
+ lombok
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
\ No newline at end of file
diff --git a/core/request-service/pom.xml b/core/request-service/pom.xml
index 620d13e..9728d2f 100644
--- a/core/request-service/pom.xml
+++ b/core/request-service/pom.xml
@@ -1,8 +1,9 @@
-
4.0.0
+
ru.practicum
core
@@ -12,97 +13,147 @@
request-service
- 22
- 22
+ 21
+ 21
UTF-8
+ 5.0.0
+
+
org.springframework.boot
- spring-boot-starter
+ spring-boot-starter-web
+
- org.springframework.cloud
- spring-cloud-starter-netflix-eureka-client
- 3.1.8
+ org.springframework.boot
+ spring-boot-starter-actuator
+
org.springframework.boot
- spring-boot-starter-web
+ spring-boot-starter-data-jpa
+
- org.projectlombok
- lombok
- provided
+ org.postgresql
+ postgresql
+ runtime
+
+
+ com.h2database
+ h2
+ runtime
+
+
org.mapstruct
mapstruct
- 1.6.2
- compile
+ 1.5.5.Final
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
org.mapstruct
mapstruct-processor
- 1.6.2
+ 1.5.5.Final
provided
+
- org.springframework.boot
- spring-boot-starter-data-jpa
+ org.springframework.cloud
+ spring-cloud-starter-netflix-eureka-client
+
- com.querydsl
- querydsl-jpa
- jakarta
- 5.1.0
+ org.springframework.cloud
+ spring-cloud-starter-config
+
- ru.practicum
- interaction-api
- 0.0.1-SNAPSHOT
- compile
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
- org.postgresql
- postgresql
+ org.springframework.retry
+ spring-retry
+
- org.springframework.boot
- spring-boot-starter-actuator
+ org.projectlombok
+ lombok
+ true
+
- org.springframework.cloud
- spring-cloud-starter-openfeign
+ com.querydsl
+ querydsl-apt
+ ${querydsl.version}
+ jakarta
+ provided
+
- org.springframework.cloud
- spring-cloud-config-client
- 4.2.0
+ com.querydsl
+ querydsl-jpa
+ jakarta
+ ${querydsl.version}
+
+
+ ru.practicum
+ interaction-api
+ 0.0.1-SNAPSHOT
+ compile
-
-
-
-
- org.springframework.cloud
- spring-cloud-dependencies
- 2024.0.0
- pom
- import
-
-
-
+
org.springframework.boot
spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
-
- paketobuildpacks/builder-jammy-base:latest
-
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
@@ -120,33 +171,6 @@
-
-
- com.querydsl
- querydsl-apt
- jakarta
- 5.1.0
-
-
-
-
- org.codehaus.mojo
- build-helper-maven-plugin
- 3.3.0
-
-
- add-source
- generate-sources
-
- add-source
-
-
-
- ${project.build.directory}/generated-sources/java/
-
-
-
-
diff --git a/core/request-service/src/main/java/ru/practicum/service/RequestServiceImpl.java b/core/request-service/src/main/java/ru/practicum/service/RequestServiceImpl.java
index 95a059f..e52ba3f 100644
--- a/core/request-service/src/main/java/ru/practicum/service/RequestServiceImpl.java
+++ b/core/request-service/src/main/java/ru/practicum/service/RequestServiceImpl.java
@@ -4,12 +4,14 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
+import ru.practicum.CollectorClient;
import ru.practicum.client.EventServiceClient;
import ru.practicum.client.UserServiceClient;
import ru.practicum.dto.request.EventRequestStatusUpdateRequest;
import ru.practicum.dto.request.EventRequestStatusUpdateResult;
import ru.practicum.dto.request.ParticipationRequestDto;
import ru.practicum.enums.RequestStatus;
+import ru.practicum.ewm.stats.proto.ActionTypeProto;
import ru.practicum.exception.ConflictException;
import ru.practicum.exception.NotFoundException;
import ru.practicum.mapper.RequestMapper;
@@ -30,6 +32,7 @@ public class RequestServiceImpl implements RequestService {
private final RequestMapper requestMapper;
private final UserServiceClient userServiceClient;
private final RequestRepository requestRepository;
+ private final CollectorClient collectorClient;
@Override
public List getRequestByUserId(Long userId) {
@@ -46,6 +49,7 @@ public ParticipationRequestDto createRequest(Long userId, Long eventId) {
requestToEventVerification(userId, eventId);
Request request = requestMapper.formUserAndEventToRequest(userId, eventId);
requestRepository.save(request);
+ collectorClient.sendUserAction(userId, eventId, ActionTypeProto.ACTION_REGISTER);
return requestMapper.toParticipationRequestDto(request);
}
diff --git a/core/user-service/pom.xml b/core/user-service/pom.xml
index 47b9445..e610891 100644
--- a/core/user-service/pom.xml
+++ b/core/user-service/pom.xml
@@ -1,142 +1,179 @@
-
4.0.0
+
ru.practicum
core
0.0.1-SNAPSHOT
+
user-service
- 22
- 22
+ 21
+ 21
UTF-8
+ 5.0.0
+
+
+
+ ru.practicum
+ stats-client
+ 0.0.1-SNAPSHOT
+
+
+
+ ru.practicum
+ aggregator
+ 0.0.1-SNAPSHOT
+
+
org.springframework.boot
spring-boot-starter-web
+
- org.zalando
- logbook-spring-boot-starter
- 3.9.0
+ org.springframework.boot
+ spring-boot-starter-actuator
+
org.springframework.boot
- spring-boot-starter-test
+ spring-boot-starter-validation
+
- org.springframework
- spring-web
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
- org.projectlombok
- lombok
- provided
+ org.postgresql
+ postgresql
+ runtime
+
- org.springframework.data
- spring-data-commons
+ com.h2database
+ h2
+ runtime
+
org.mapstruct
mapstruct
- 1.6.2
+ 1.5.5.Final
+
- org.mapstruct
- mapstruct-processor
- 1.6.2
- provided
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
jakarta.validation
jakarta.validation-api
+
- jakarta.transaction
- jakarta.transaction-api
+ org.springframework.boot
+ spring-boot-starter-validation
+
- org.springframework.data
- spring-data-jpa
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+ provided
+
- com.querydsl
- querydsl-core
- 5.1.0
+ org.springframework.cloud
+ spring-cloud-starter-netflix-eureka-client
+
- javax.persistence
- javax.persistence-api
- 2.2
+ org.springframework.cloud
+ spring-cloud-starter-config
+
- org.springframework.boot
- spring-boot-starter-data-jpa
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
- ru.practicum
- interaction-api
- 0.0.1-SNAPSHOT
- compile
+ org.springframework.retry
+ spring-retry
+
- org.springframework.boot
- spring-boot-starter-actuator
+ org.projectlombok
+ lombok
+ true
+
+
+
com.querydsl
- querydsl-jpa
+ querydsl-apt
+ ${querydsl.version}
jakarta
- 5.1.0
-
-
- org.springframework.cloud
- spring-cloud-starter-netflix-eureka-client
- 3.1.8
-
-
- org.springframework.cloud
- spring-cloud-config-client
- 4.2.0
+ provided
+
- org.postgresql
- postgresql
+ com.querydsl
+ querydsl-jpa
+ jakarta
+ ${querydsl.version}
- org.springframework.cloud
- spring-cloud-starter-config
+ ru.practicum
+ interaction-api
+ 0.0.1-SNAPSHOT
+ compile
-
-
-
-
- org.springframework.cloud
- spring-cloud-dependencies
- 2024.0.0
- pom
- import
-
-
-
+
org.springframework.boot
spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
-
- paketobuildpacks/builder-jammy-base:latest
-
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.5.Final
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
@@ -154,33 +191,6 @@
-
-
- com.querydsl
- querydsl-apt
- jakarta
- 5.1.0
-
-
-
-
- org.codehaus.mojo
- build-helper-maven-plugin
- 3.3.0
-
-
- add-source
- generate-sources
-
- add-source
-
-
-
- ${project.build.directory}/generated-sources/java/
-
-
-
-
diff --git a/docker-compose.yml b/docker-compose.yml
index b7f2a57..71fe4d9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -37,108 +37,104 @@ services:
depends_on:
config-server:
condition: service_healthy
+ event-service:
+ condition: service_healthy
user-service:
condition: service_healthy
request-service:
condition: service_healthy
- event-service:
- condition: service_healthy
- stats-server:
- condition: service_healthy
+
networks:
- ewm-net
environment:
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- stats-server:
- build: stats/stats-server
- container_name: ewm-stats-server
- ports:
- - "9090:9090"
+ event-service:
+ build: core/event-service
+ container_name: event-service
depends_on:
- stats-db:
+ event-db:
condition: service_healthy
config-server:
condition: service_healthy
+
networks:
- ewm-net
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://stats-db:5432/ewm-stats
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://event-db:5432/ewm-event
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- - SERVER_PORT=9090
+ - SERVER_PORT=8081
healthcheck:
- test: "curl --fail --silent localhost:9090/actuator/health | grep UP || exit 1"
+ test: "curl --fail --silent localhost:8081/actuator/health | grep UP || exit 1"
timeout: 5s
- interval: 15s
+ interval: 25s
retries: 10
- stats-db:
+ event-db:
image: postgres:16.1
- container_name: postgres-ewm-stats-db
+ container_name: postgres-ewm-event-db
+ networks:
+ - ewm-net
environment:
- POSTGRES_PASSWORD=root
- POSTGRES_USER=root
- - POSTGRES_DB=ewm-stats
- networks:
- - ewm-net
+ - POSTGRES_DB=ewm-event
+ ports:
+ - 5434:5433
healthcheck:
test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
timeout: 5s
interval: 10s
retries: 15
- # Event-service
- event-service:
- build: core/event-service
- container_name: event-service
+
+ request-service:
+ build: core/request-service
+ container_name: ewm-request-service
depends_on:
- event-db:
+ request-db:
condition: service_healthy
config-server:
condition: service_healthy
- stats-server:
- condition: service_healthy
networks:
- ewm-net
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://event-db:5432/ewm-event
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://request-db:5432/ewm-request
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- - SERVER_PORT=8081
+ - SERVER_PORT=8083
healthcheck:
- test: "curl --fail --silent localhost:8081/actuator/health | grep UP || exit 1"
+ test: "curl --fail --silent localhost:8083/actuator/health | grep UP || exit 1"
timeout: 5s
interval: 25s
retries: 10
- event-db:
+
+ request-db:
image: postgres:16.1
- container_name: postgres-ewm-event-db
+ container_name: postgres-ewm-request-db
networks:
- ewm-net
environment:
- POSTGRES_PASSWORD=root
- POSTGRES_USER=root
- - POSTGRES_DB=ewm-event
+ - POSTGRES_DB=ewm-request
healthcheck:
test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
timeout: 5s
interval: 10s
retries: 15
- # USER-SERVICE
user-service:
build: core/user-service
- container_name: user-service
+ container_name: ewm-user-service
depends_on:
- event-db:
+ user-db:
condition: service_healthy
config-server:
condition: service_healthy
- stats-server:
- condition: service_healthy
networks:
- ewm-net
environment:
@@ -146,9 +142,9 @@ services:
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- - SERVER_PORT=8083
+ - SERVER_PORT=8084
healthcheck:
- test: "curl --fail --silent localhost:8083/actuator/health | grep UP || exit 1"
+ test: "curl --fail --silent localhost:8084/actuator/health | grep UP || exit 1"
timeout: 5s
interval: 25s
retries: 10
@@ -168,51 +164,113 @@ services:
interval: 10s
retries: 15
- #REQUEST-SERVICE
- request-db:
- image: postgres:16.1
- container_name: postgres-ewm-request-db
+ kafka:
+ image: confluentinc/confluent-local:7.4.3
+ hostname: kafka
+ container_name: kafka
+ ports:
+ - "9092:9092" # for client connections
+ - "9101:9101" # JMX
networks:
- ewm-net
+ restart: unless-stopped
environment:
- - POSTGRES_PASSWORD=root
- - POSTGRES_USER=root
- - POSTGRES_DB=ewm-request
- volumes:
- - ${PWD}/core/request-service/src/main/resources/schema.sql:/docker-entrypoint-initdb.d/schema.sql
+ KAFKA_NODE_ID: 1
+ KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092'
+ KAFKA_JMX_PORT: 9101
+ KAFKA_JMX_HOSTNAME: localhost
+ KAFKA_PROCESS_ROLES: 'broker, controller'
+ KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'
+ KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'
+ CLUSTER_ID: 'K0EA9p0yEe6MkAAAAkKsEg'
+
+ kafka-init-topics:
+ image: confluentinc/confluent-local:7.4.3
+ container_name: kafka-init-topics
+ depends_on:
+ - kafka
+ networks:
+ - ewm-net
+ command: "bash -c \
+ 'kafka-topics --create --topic stats.user-actions.v1 \
+ --partitions 1 --replication-factor 1 --if-not-exists \
+ --bootstrap-server kafka:29092 && \
+ kafka-topics --create --topic stats.events-similarity.v1 \
+ --partitions 1 --replication-factor 1 --if-not-exists \
+ --bootstrap-server kafka:29092'"
+ init: true
+
+ collector:
+ build: stats/collector
+ container_name: ewm-collector
+ depends_on:
+ config-server:
+ condition: service_healthy
+ networks:
+ - ewm-net
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
+ - SERVER_PORT=8085
healthcheck:
- test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
+ test: "curl --fail --silent localhost:8085/actuator/health | grep UP || exit 1"
timeout: 5s
- interval: 10s
- retries: 15
+ interval: 15s
+ retries: 10
- request-service:
- build: core/request-service
- container_name: request-service
+ aggregator:
+ build: stats/aggregator
+ container_name: ewm-aggregator
depends_on:
- user-db:
- condition: service_healthy
config-server:
condition: service_healthy
- stats-server:
- condition: service_healthy
- user-service:
+ networks:
+ - ewm-net
+ environment:
+ - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
+ - SERVER_PORT=8086
+ healthcheck:
+ test: "curl --fail --silent localhost:8086/actuator/health | grep UP || exit 1"
+ timeout: 5s
+ interval: 15s
+ retries: 10
+
+ analyzer:
+ build: stats/analyzer
+ container_name: ewm-analyzer
+ depends_on:
+ analyzer-db:
condition: service_healthy
- request-db:
+ config-server:
condition: service_healthy
networks:
- ewm-net
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://request-db:5432/ewm-request
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://analyzer-db:5432/ewm-analyzer
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root
- EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://discovery-server:8761/eureka/
- - SERVER_PORT=8085
+ - SERVER_PORT=8087
healthcheck:
- test: "curl --fail --silent localhost:8085/actuator/health | grep UP || exit 1"
+ test: "curl --fail --silent localhost:8087/actuator/health | grep UP || exit 1"
timeout: 5s
- interval: 25s
+ interval: 15s
retries: 10
+ analyzer-db:
+ image: postgres:16.1
+ container_name: postgres-ewm-analyzer-db
+ environment:
+ - POSTGRES_PASSWORD=root
+ - POSTGRES_USER=root
+ - POSTGRES_DB=ewm-analyzer
+ networks:
+ - ewm-net
+ healthcheck:
+ test: pg_isready -q -d $$POSTGRES_DB -U $$POSTGRES_USER
+ timeout: 5s
+ interval: 10s
+ retries: 15
+
+
networks:
ewm-net:
\ No newline at end of file
diff --git a/infra/config-server/src/main/resources/config/stats.stats-server/application.yml b/infra/config-server/src/main/resources/config/stats.stats-server/application.yml
deleted file mode 100644
index 6886a06..0000000
--- a/infra/config-server/src/main/resources/config/stats.stats-server/application.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-spring:
- datasource:
- driverClassName: org.postgresql.Driver
- url: jdbc:postgresql://stats-db:5432/ewm-stats
- username: root
- password: root
-
- jpa:
- hibernate:
- ddl-auto: none
- database-platform: org.hibernate.dialect.PostgreSQLDialect
- generate-ddl: false
- properties:
- hibernate:
- format_sql: true
- show-sql: false
-
- sql:
- init:
- mode: always
\ No newline at end of file
diff --git a/infra/config-server/src/main/resources/config/stats/aggregator/application.yml b/infra/config-server/src/main/resources/config/stats/aggregator/application.yml
new file mode 100644
index 0000000..91d5086
--- /dev/null
+++ b/infra/config-server/src/main/resources/config/stats/aggregator/application.yml
@@ -0,0 +1,11 @@
+kafka:
+ bootstrap-servers: kafka:29092
+ consumer:
+ topic: stats.user-actions.v1
+ group-id: aggregator-actions
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: ru.practicum.AvroDeserializer
+ producer:
+ topic: stats.events-similarity.v1
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: ru.practicum.AvroSerializer
\ No newline at end of file
diff --git a/infra/config-server/src/main/resources/config/stats/analyzer/application.yml b/infra/config-server/src/main/resources/config/stats/analyzer/application.yml
new file mode 100644
index 0000000..fea45a3
--- /dev/null
+++ b/infra/config-server/src/main/resources/config/stats/analyzer/application.yml
@@ -0,0 +1,39 @@
+spring:
+ datasource:
+ driverClassName: org.postgresql.Driver
+ url: jdbc:postgresql://analyzer-db:5432/ewm-analyzer
+ username: root
+ password: root
+
+ jpa:
+ hibernate:
+ ddl-auto: update
+ properties:
+ hibernate:
+ format_sql: true
+ generate-ddl: false
+
+ sql:
+ init:
+ mode: always
+
+kafka:
+ bootstrap-servers: kafka:29092
+ user-actions-consumer:
+ group-id: analyzer-user-actions
+ topic: stats.user-actions.v1
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: ru.practicum.AvroDeserializer
+ events-similarity-consumer:
+ group-id: analyzer-events-sim
+ topic: stats.events-similarity.v1
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: ru.practicum.AvroDeserializer
+
+grpc:
+ client:
+ analyzer:
+ address: 'discovery:///analyzer'
+ enableKeepAlive: true
+ keepAliveWithoutCalls: true
+ negotiationType: plaintext
\ No newline at end of file
diff --git a/infra/config-server/src/main/resources/config/stats/collector/application.yml b/infra/config-server/src/main/resources/config/stats/collector/application.yml
new file mode 100644
index 0000000..74915bb
--- /dev/null
+++ b/infra/config-server/src/main/resources/config/stats/collector/application.yml
@@ -0,0 +1,20 @@
+spring:
+ kafka:
+ bootstrap-servers: kafka:29092
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: ru.practicum.AvroSerializer
+
+collector:
+ kafka:
+ topic: stats.user-actions.v1
+
+grpc:
+ server:
+ port: 0
+ client:
+ analyzer:
+ address: 'discovery:///collector'
+ enableKeepAlive: true
+ keepAliveWithoutCalls: true
+ negotiationType: plaintext
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 599095a..8ee149f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.4.0
+ 3.3.4
@@ -15,7 +15,7 @@
core
infra
stats
-
+
ru.practicum
explore-with-me
@@ -25,7 +25,14 @@
21
UTF-8
- 2024.0.0
+ 2023.0.3
+ 1.12.0
+ 3.25.1
+ 1.63.0
+ ${avro.version}
+ 2.4.0
+ 3.11.0
+ 3.1.0.RELEASE
@@ -37,6 +44,29 @@
pom
import
+
+ net.devh
+ grpc-spring-boot-starter
+ ${grpc-spring-boot-starter.version}
+
+
+
+ net.devh
+ grpc-server-spring-boot-starter
+ ${grpc-spring-boot-starter.version}
+
+
+
+ io.grpc
+ grpc-stub
+ ${grpc.version}
+
+
+
+ io.grpc
+ grpc-protobuf
+ ${grpc.version}
+
diff --git a/stats/stats-server/Dockerfile b/stats/aggregator/Dockerfile
similarity index 100%
rename from stats/stats-server/Dockerfile
rename to stats/aggregator/Dockerfile
diff --git a/stats/stats-server/pom.xml b/stats/aggregator/pom.xml
similarity index 83%
rename from stats/stats-server/pom.xml
rename to stats/aggregator/pom.xml
index d126e6a..480b505 100644
--- a/stats/stats-server/pom.xml
+++ b/stats/aggregator/pom.xml
@@ -3,22 +3,23 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
ru.practicum
stats
0.0.1-SNAPSHOT
- stats-server
+ aggregator
- 17
- 17
+ 21
+ 21
UTF-8
+ 0.0.1-SNAPSHOT
+
org.springframework.boot
spring-boot-starter-web
@@ -57,12 +58,6 @@
spring-boot-starter-test
test
-
- ru.practicum
- stats-dto
- 0.0.1-SNAPSHOT
- compile
-
org.projectlombok
@@ -93,6 +88,22 @@
spring-cloud-openfeign-core
+
+ org.apache.kafka
+ kafka-clients
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ ru.practicum
+ avro-schemas
+ ${avro-schemas.version}
+
+
diff --git a/stats/aggregator/src/main/java/ru/practicum/AggregatorApplication.java b/stats/aggregator/src/main/java/ru/practicum/AggregatorApplication.java
new file mode 100644
index 0000000..0897186
--- /dev/null
+++ b/stats/aggregator/src/main/java/ru/practicum/AggregatorApplication.java
@@ -0,0 +1,15 @@
+package ru.practicum;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+@SpringBootApplication
+@EnableDiscoveryClient
+@ConfigurationPropertiesScan
+public class AggregatorApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(AggregatorApplication.class, args);
+ }
+}
diff --git a/stats/aggregator/src/main/java/ru/practicum/config/KafkaConfig.java b/stats/aggregator/src/main/java/ru/practicum/config/KafkaConfig.java
new file mode 100644
index 0000000..ff3c0ab
--- /dev/null
+++ b/stats/aggregator/src/main/java/ru/practicum/config/KafkaConfig.java
@@ -0,0 +1,53 @@
+package ru.practicum.config;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.*;
+import ru.practicum.ewm.stats.avro.EventSimilarityAvro;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+
+import java.util.Map;
+
+@Configuration
+@RequiredArgsConstructor
+public class KafkaConfig {
+ private final KafkaProperties props;
+
+ @Bean
+ public ConsumerFactory consumerFactory() {
+ final Map config = Map.of(
+ ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, props.getBootstrapServers(),
+ ConsumerConfig.GROUP_ID_CONFIG, props.getConsumer().getGroupId(),
+ ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, props.getConsumer().getKeyDeserializer(),
+ ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, props.getConsumer().getValueDeserializer()
+ );
+ return new DefaultKafkaConsumerFactory<>(config);
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ return factory;
+ }
+
+ @Bean
+ public ProducerFactory producerFactory() {
+ Map config = Map.of(
+ ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, props.getBootstrapServers(),
+ ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, props.getProducer().getKeySerializer(),
+ ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, props.getProducer().getValueSerializer()
+ );
+ return new DefaultKafkaProducerFactory<>(config);
+ }
+
+ @Bean
+ public KafkaTemplate kafkaTemplate() {
+ return new KafkaTemplate<>(producerFactory());
+ }
+}
diff --git a/stats/aggregator/src/main/java/ru/practicum/config/KafkaProperties.java b/stats/aggregator/src/main/java/ru/practicum/config/KafkaProperties.java
new file mode 100644
index 0000000..fa8a503
--- /dev/null
+++ b/stats/aggregator/src/main/java/ru/practicum/config/KafkaProperties.java
@@ -0,0 +1,33 @@
+package ru.practicum.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "kafka")
+@Getter
+@Setter
+public class KafkaProperties {
+ private String bootstrapServers;
+ private Consumer consumer = new Consumer();
+ private Producer producer = new Producer();
+
+ @Getter
+ @Setter
+ public static class Consumer {
+ private String groupId;
+ private String topic;
+ private String keyDeserializer;
+ private String valueDeserializer;
+ }
+
+ @Getter
+ @Setter
+ public static class Producer {
+ private String topic;
+ private String keySerializer;
+ private String valueSerializer;
+ }
+}
\ No newline at end of file
diff --git a/stats/aggregator/src/main/java/ru/practicum/config/UserActionsConsumer.java b/stats/aggregator/src/main/java/ru/practicum/config/UserActionsConsumer.java
new file mode 100644
index 0000000..2c5591d
--- /dev/null
+++ b/stats/aggregator/src/main/java/ru/practicum/config/UserActionsConsumer.java
@@ -0,0 +1,25 @@
+package ru.practicum.config;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+import ru.practicum.service.SimilarityService;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class UserActionsConsumer {
+
+ private final SimilarityService similarityService;
+
+ @KafkaListener(
+ topics = "#{kafkaProperties.consumer.topic}",
+ containerFactory = "kafkaListenerContainerFactory"
+ )
+ public void consumeUserAction(UserActionAvro message) {
+ log.info("consume user action Kafka: {}", message);
+ similarityService.processUserAction(message);
+ }
+}
diff --git a/stats/aggregator/src/main/java/ru/practicum/service/MinWeightsMatrix.java b/stats/aggregator/src/main/java/ru/practicum/service/MinWeightsMatrix.java
new file mode 100644
index 0000000..aa49613
--- /dev/null
+++ b/stats/aggregator/src/main/java/ru/practicum/service/MinWeightsMatrix.java
@@ -0,0 +1,22 @@
+package ru.practicum.service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MinWeightsMatrix {
+
+ private final Map> minWeightsSums = new HashMap<>();
+
+ public void put(long eventA, long eventB, double sum) {
+ long first = Math.min(eventA, eventB);
+ long second = Math.max(eventA, eventB);
+ minWeightsSums.computeIfAbsent(first, k -> new HashMap<>()).put(second, sum);
+ }
+
+ public double get(long eventA, long eventB) {
+ long first = Math.min(eventA, eventB);
+ long second = Math.max(eventA, eventB);
+ return minWeightsSums.getOrDefault(first, Map.of())
+ .getOrDefault(second, 0.0);
+ }
+}
\ No newline at end of file
diff --git a/stats/aggregator/src/main/java/ru/practicum/service/SimilarityService.java b/stats/aggregator/src/main/java/ru/practicum/service/SimilarityService.java
new file mode 100644
index 0000000..dd51e96
--- /dev/null
+++ b/stats/aggregator/src/main/java/ru/practicum/service/SimilarityService.java
@@ -0,0 +1,110 @@
+package ru.practicum.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Service;
+import ru.practicum.config.KafkaProperties;
+import ru.practicum.ewm.stats.avro.ActionTypeAvro;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+import ru.practicum.ewm.stats.avro.EventSimilarityAvro;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class SimilarityService {
+
+ private final Map> weights = new HashMap<>();
+
+ private final Map eventWeightsSum = new HashMap<>();
+
+ private final MinWeightsMatrix minWeightsMatrix = new MinWeightsMatrix();
+
+ private final KafkaTemplate kafkaTemplate;
+ private final KafkaProperties props;
+
+ public SimilarityService(KafkaTemplate kafkaTemplate,
+ KafkaProperties props) {
+ this.kafkaTemplate = kafkaTemplate;
+ this.props = props;
+ }
+
+ public void processUserAction(UserActionAvro action) {
+ long userId = action.getUserId();
+ long eventId = action.getEventId();
+ int newWeight = convertActionType(action.getActionType());
+ long timestampMillis = action.getTimestamp();
+ Instant timestamp = Instant.ofEpochMilli(timestampMillis);
+
+ Map userMap = weights.computeIfAbsent(eventId, e -> new HashMap<>());
+ int oldWeight = userMap.getOrDefault(userId, 0);
+
+ if (newWeight <= oldWeight) {
+ log.debug("Обновление не требуется: userId={}, eventId={}, weight={} <= oldWeight={}",
+ userId, eventId, newWeight, oldWeight);
+ return;
+ }
+
+ userMap.put(userId, newWeight);
+
+ int oldSum = eventWeightsSum.getOrDefault(eventId, 0);
+ int diff = newWeight - oldWeight;
+ int updatedSum = oldSum + diff;
+ eventWeightsSum.put(eventId, updatedSum);
+
+ weights.keySet()
+ .stream()
+ .filter(otherEvent -> otherEvent.equals(eventId))
+ .forEach(otherEvent -> updatePairSimilarity(eventId, otherEvent, timestamp));
+ }
+
+ private void updatePairSimilarity(long eventA, long eventB, Instant timestamp) {
+ double sMin = calcSMin(eventA, eventB);
+ minWeightsMatrix.put(eventA, eventB, sMin);
+
+ double sA = eventWeightsSum.getOrDefault(eventA, 0);
+ double sB = eventWeightsSum.getOrDefault(eventB, 0);
+ if (sA == 0 || sB == 0) {
+
+ log.debug("Обнаружена нулевая сумма (sA={}, sB={}), пропускающая сходство для событий {} и {}",
+ sA, sB, eventA, eventB);
+ return;
+ }
+
+ float similarity = (float) (sMin / (sA * sB));
+
+ long first = Math.min(eventA, eventB);
+ long second = Math.max(eventA, eventB);
+
+ EventSimilarityAvro similarityMsg = EventSimilarityAvro.newBuilder()
+ .setEventA(first)
+ .setEventB(second)
+ .setScore(similarity)
+ .setTimestamp(timestamp)
+ .build();
+
+ kafkaTemplate.send(props.getProducer().getTopic(), similarityMsg);
+
+ log.debug("Обновлено сходство для (A={}, B={}) => {}", first, second, similarity);
+ }
+
+ private double calcSMin(long eventA, long eventB) {
+ Map userMapA = weights.getOrDefault(eventA, Map.of());
+ Map userMapB = weights.getOrDefault(eventB, Map.of());
+
+ return userMapA.entrySet().stream()
+ .filter(e -> userMapB.get(e.getKey()) != null)
+ .mapToDouble(e -> Math.min(e.getValue(), userMapB.get(e.getKey())))
+ .sum();
+ }
+
+ private int convertActionType(ActionTypeAvro actionType) {
+ return switch (actionType) {
+ case REGISTER -> 2;
+ case LIKE -> 3;
+ default -> 1;
+ };
+ }
+}
\ No newline at end of file
diff --git a/stats/aggregator/src/main/resources/application.yml b/stats/aggregator/src/main/resources/application.yml
new file mode 100644
index 0000000..2b1f593
--- /dev/null
+++ b/stats/aggregator/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+spring:
+ application:
+ name: aggregator
+ config:
+ import: "configserver:"
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ useRandomPolicy: true
+ max-interval: 6000
+ discovery:
+ enabled: true
+ service-id: config-server
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ serviceUrl:
+ defaultZone: http://${eureka.instance.hostname:localhost}:${eureka.instance.port:8761}/eureka/
+ instance:
+ preferIpAddress: true
+ hostname: localhost
+ instance-id: "${spring.application.name}:${random.value}"
+ leaseRenewalIntervalInSeconds: 10
+server:
+ port: 0
\ No newline at end of file
diff --git a/stats/analyzer/Dockerfile b/stats/analyzer/Dockerfile
new file mode 100644
index 0000000..0ff1817
--- /dev/null
+++ b/stats/analyzer/Dockerfile
@@ -0,0 +1,5 @@
+FROM eclipse-temurin:21-jre-jammy
+VOLUME /tmp
+ARG JAR_FILE=target/*.jar
+COPY ${JAR_FILE} app.jar
+ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
\ No newline at end of file
diff --git a/stats/analyzer/pom.xml b/stats/analyzer/pom.xml
new file mode 100644
index 0000000..b77554d
--- /dev/null
+++ b/stats/analyzer/pom.xml
@@ -0,0 +1,143 @@
+
+
+ 4.0.0
+
+ ru.practicum
+ stats
+ 0.0.1-SNAPSHOT
+
+
+ analyzer
+
+
+ 21
+ 21
+ UTF-8
+ 0.0.1-SNAPSHOT
+
+
+
+
+
+ net.devh
+ grpc-server-spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ ru.practicum
+ aggregator
+ 0.0.1-SNAPSHOT
+ compile
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-netflix-eureka-client
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-config
+
+
+
+ org.springframework.retry
+ spring-retry
+
+
+ org.springframework.cloud
+ spring-cloud-openfeign-core
+
+
+ org.springframework.cloud
+ spring-cloud-commons
+
+
+
+
+ org.apache.kafka
+ kafka-clients
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ ru.practicum
+ avro-schemas
+ ${avro-schemas.version}
+
+
+
+ ru.practicum
+ proto-schemas
+ ${avro-schemas.version}
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 16
+ 16
+
+
+
+
+
+
\ No newline at end of file
diff --git a/stats/stats-server/src/main/java/ru/practicum/StatServer.java b/stats/analyzer/src/main/java/ru/practicum/AnalyzerApplication.java
similarity index 79%
rename from stats/stats-server/src/main/java/ru/practicum/StatServer.java
rename to stats/analyzer/src/main/java/ru/practicum/AnalyzerApplication.java
index 1066613..12c8dd6 100644
--- a/stats/stats-server/src/main/java/ru/practicum/StatServer.java
+++ b/stats/analyzer/src/main/java/ru/practicum/AnalyzerApplication.java
@@ -1,3 +1,4 @@
+
package ru.practicum;
import org.springframework.boot.SpringApplication;
@@ -8,9 +9,9 @@
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
-public class StatServer {
+public class AnalyzerApplication {
public static void main(String[] args) {
- SpringApplication.run(StatServer.class, args);
+ SpringApplication.run(AnalyzerApplication.class, args);
}
-}
\ No newline at end of file
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/config/EventsSimilarityConsumer.java b/stats/analyzer/src/main/java/ru/practicum/config/EventsSimilarityConsumer.java
new file mode 100644
index 0000000..b0fa188
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/config/EventsSimilarityConsumer.java
@@ -0,0 +1,25 @@
+package ru.practicum.config;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+import ru.practicum.ewm.stats.avro.EventSimilarityAvro;
+import ru.practicum.service.event.EventSimilarityService;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class EventsSimilarityConsumer {
+
+ private final EventSimilarityService eventSimilarityService;
+
+ @KafkaListener(
+ topics = "${kafka.events-similarity-consumer.topic}",
+ containerFactory = "eventSimilarityKafkaListenerFactory"
+ )
+ public void consumeEventSimilarity(EventSimilarityAvro msg) {
+ log.info("Consumed event similarity: {}", msg);
+ eventSimilarityService.updateEventSimilarity(msg);
+ }
+}
\ No newline at end of file
diff --git a/stats/analyzer/src/main/java/ru/practicum/config/KafkaConfig.java b/stats/analyzer/src/main/java/ru/practicum/config/KafkaConfig.java
new file mode 100644
index 0000000..9a86cee
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/config/KafkaConfig.java
@@ -0,0 +1,65 @@
+package ru.practicum.config;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.ConsumerFactory;
+import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+import ru.practicum.ewm.stats.avro.EventSimilarityAvro;
+
+import java.util.Map;
+
+@Configuration
+@RequiredArgsConstructor
+public class KafkaConfig {
+ private final KafkaProperties props;
+
+ @Bean
+ public ConsumerFactory userActionsConsumerFactory() {
+ final Map config = Map.of(
+ ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, props.getBootstrapServers(),
+ ConsumerConfig.GROUP_ID_CONFIG, props.getUserActionsConsumer().getGroupId(),
+ ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, props.getUserActionsConsumer().getKeyDeserializer(),
+ ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, props.getUserActionsConsumer().getValueDeserializer()
+ );
+ return new DefaultKafkaConsumerFactory<>(config);
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory userActionsKafkaListenerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(userActionsConsumerFactory());
+ return factory;
+ }
+
+ @Bean
+ public ConsumerFactory eventSimilarityConsumerFactory() {
+ Map config = Map.of(
+ ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, props.getBootstrapServers(),
+ ConsumerConfig.GROUP_ID_CONFIG, props.getEventsSimilarityConsumer().getGroupId(),
+ ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, getClassFromString(props.getEventsSimilarityConsumer().getKeyDeserializer()),
+ ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, getClassFromString(props.getEventsSimilarityConsumer().getValueDeserializer())
+ );
+ return new DefaultKafkaConsumerFactory<>(config);
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory eventSimilarityKafkaListenerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(eventSimilarityConsumerFactory());
+ return factory;
+ }
+
+ private Class> getClassFromString(String className) {
+ try {
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Unable to load class: " + className, e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/stats/analyzer/src/main/java/ru/practicum/config/KafkaProperties.java b/stats/analyzer/src/main/java/ru/practicum/config/KafkaProperties.java
new file mode 100644
index 0000000..0e3d8d0
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/config/KafkaProperties.java
@@ -0,0 +1,22 @@
+package ru.practicum.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "kafka")
+public class KafkaProperties {
+ private String bootstrapServers;
+ private final ConsumerProps userActionsConsumer = new ConsumerProps();
+ private final ConsumerProps eventsSimilarityConsumer = new ConsumerProps();
+
+ @Data
+ public static class ConsumerProps {
+ private String topic;
+ private String groupId;
+ private String keyDeserializer;
+ private String valueDeserializer;
+ }
+}
\ No newline at end of file
diff --git a/stats/analyzer/src/main/java/ru/practicum/config/UserActionsConsumer.java b/stats/analyzer/src/main/java/ru/practicum/config/UserActionsConsumer.java
new file mode 100644
index 0000000..a0cca01
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/config/UserActionsConsumer.java
@@ -0,0 +1,25 @@
+package ru.practicum.config;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+import ru.practicum.service.user.UserActionService;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class UserActionsConsumer {
+
+ private final UserActionService userActionService;
+
+ @KafkaListener(
+ topics = "${kafka.user-actions-consumer.topic}",
+ containerFactory = "userActionsKafkaListenerFactory"
+ )
+ public void consumeUserActions(UserActionAvro message) {
+ log.info("user action: {}", message);
+ userActionService.updateUserAction(message);
+ }
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/controller/RecommendationsController.java b/stats/analyzer/src/main/java/ru/practicum/controller/RecommendationsController.java
new file mode 100644
index 0000000..27fcb07
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/controller/RecommendationsController.java
@@ -0,0 +1,106 @@
+package ru.practicum.controller;
+
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.stub.StreamObserver;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.devh.boot.grpc.server.service.GrpcService;
+import ru.practicum.ewm.stats.proto.RecommendationsControllerGrpc;
+import ru.practicum.ewm.stats.proto.RecommendationsMessages;
+import ru.practicum.model.RecommendedEvent;
+import ru.practicum.service.RecommendationService;
+
+import java.util.List;
+
+@GrpcService
+@RequiredArgsConstructor
+@Slf4j
+public class RecommendationsController extends RecommendationsControllerGrpc.RecommendationsControllerImplBase {
+
+ private final RecommendationService recommendationService;
+
+ @Override
+ public void getSimilarEvents(RecommendationsMessages.SimilarEventsRequestProto requestProto,
+ StreamObserver streamObserver) {
+ try {
+ List recommendedEvents = recommendationService.getSimilarEvents(requestProto);
+ for (RecommendedEvent recommendedEvent : recommendedEvents) {
+ RecommendationsMessages.RecommendedEventProto recommendedEventProto =
+ RecommendationsMessages.RecommendedEventProto.newBuilder()
+ .setEventId(recommendedEvent.eventId())
+ .setScore(recommendedEvent.score())
+ .build();
+ streamObserver.onNext(recommendedEventProto);
+ }
+ streamObserver.onCompleted();
+ } catch (IllegalArgumentException e) {
+ log.error("Illegal argument in getSimilarEvents: {}", e.getMessage(), e);
+ streamObserver.onError(
+ new StatusRuntimeException(
+ Status.INVALID_ARGUMENT.withDescription("unexpected error occurred").withCause(e))
+ );
+ } catch (Exception e) {
+ log.error("unexpected error occurred in getSimilarEvents: {}", e.getMessage(), e);
+ streamObserver.onError(
+ new StatusRuntimeException(Status.UNKNOWN.withDescription("unexpected error occurred").withCause(e))
+ );
+ }
+ }
+
+ @Override
+ public void getRecommendationsForUser(
+ RecommendationsMessages.UserPredictionsRequestProto requestProto,
+ StreamObserver streamObserver
+ ) {
+ try {
+ List events = recommendationService.getRecommendationsForUser(requestProto);
+ for (RecommendedEvent event : events) {
+ RecommendationsMessages.RecommendedEventProto recommendedEventProto =
+ RecommendationsMessages.RecommendedEventProto.newBuilder()
+ .setEventId(event.eventId())
+ .setScore(event.score())
+ .build();
+ streamObserver.onNext(recommendedEventProto);
+ }
+ streamObserver.onCompleted();
+ } catch (IllegalArgumentException e) {
+ log.error("Illegal argument in getRecommendationsForUser: {}", e.getMessage(), e);
+ streamObserver.onError(
+ new StatusRuntimeException(
+ Status.INVALID_ARGUMENT.withDescription("unexpected error occurred").withCause(e))
+ );
+ } catch (Exception e) {
+ log.error("unexpected error occurred in getRecommendationsForUser: {}", e.getMessage(), e);
+ streamObserver.onError(
+ new StatusRuntimeException(Status.UNKNOWN.withDescription("unexpected error occurred").withCause(e))
+ );
+ }
+ }
+
+ @Override
+ public void getInteractionsCount(RecommendationsMessages.InteractionsCountRequestProto requestProto,
+ StreamObserver streamObserver) {
+ try {
+ List events = recommendationService.getInteractionsCount(requestProto);
+ for (RecommendedEvent event : events) {
+ RecommendationsMessages.RecommendedEventProto eventProto = RecommendationsMessages.RecommendedEventProto.newBuilder()
+ .setEventId(event.eventId())
+ .setScore(event.score())
+ .build();
+ streamObserver.onNext(eventProto);
+ }
+ streamObserver.onCompleted();
+ } catch (IllegalArgumentException e) {
+ log.error("Illegal argument in getInteractionsCount: {}", e.getMessage(), e);
+ streamObserver.onError(
+ new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e))
+ );
+ } catch (Exception e) {
+ log.error("unexpected error occurred in getInteractionsCount: {}", e.getMessage(), e);
+ streamObserver.onError(
+ new StatusRuntimeException(Status.UNKNOWN.withDescription("unexpected error occurred").withCause(e))
+ );
+ }
+ }
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/model/EventSimilarity.java b/stats/analyzer/src/main/java/ru/practicum/model/EventSimilarity.java
new file mode 100644
index 0000000..3976ebd
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/model/EventSimilarity.java
@@ -0,0 +1,28 @@
+package ru.practicum.model;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Entity
+@Table(name = "events_similarity")
+public class EventSimilarity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private Long eventA;
+ private Long eventB;
+ private Float score;
+ private Instant timestamp;
+}
\ No newline at end of file
diff --git a/stats/analyzer/src/main/java/ru/practicum/model/RecommendedEvent.java b/stats/analyzer/src/main/java/ru/practicum/model/RecommendedEvent.java
new file mode 100644
index 0000000..47cd704
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/model/RecommendedEvent.java
@@ -0,0 +1,7 @@
+package ru.practicum.model;
+
+public record RecommendedEvent(
+ long eventId,
+ double score
+) {
+}
\ No newline at end of file
diff --git a/stats/analyzer/src/main/java/ru/practicum/model/UserAction.java b/stats/analyzer/src/main/java/ru/practicum/model/UserAction.java
new file mode 100644
index 0000000..b7f6f32
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/model/UserAction.java
@@ -0,0 +1,35 @@
+package ru.practicum.model;
+
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Entity
+@Table(name = "user_actions")
+public class UserAction {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private Long userId;
+ private Long eventId;
+
+ private Double maxWeight;
+
+ private Instant lastInteraction;
+}
\ No newline at end of file
diff --git a/stats/analyzer/src/main/java/ru/practicum/repository/EventSimilarityRepository.java b/stats/analyzer/src/main/java/ru/practicum/repository/EventSimilarityRepository.java
new file mode 100644
index 0000000..e5a121d
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/repository/EventSimilarityRepository.java
@@ -0,0 +1,11 @@
+package ru.practicum.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.model.EventSimilarity;
+
+import java.util.List;
+
+public interface EventSimilarityRepository extends JpaRepository {
+
+ List findByEventAOrEventB(Long eventA, Long eventB);
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/repository/UserActionRepository.java b/stats/analyzer/src/main/java/ru/practicum/repository/UserActionRepository.java
new file mode 100644
index 0000000..3e346a3
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/repository/UserActionRepository.java
@@ -0,0 +1,15 @@
+package ru.practicum.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.practicum.model.UserAction;
+
+import java.util.List;
+
+public interface UserActionRepository extends JpaRepository {
+
+ UserAction findByUserIdAndEventId(Long userId, Long eventId);
+
+ List findByUserId(Long userId);
+
+ List findByEventId(Long eventId);
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/service/RecommendationService.java b/stats/analyzer/src/main/java/ru/practicum/service/RecommendationService.java
new file mode 100644
index 0000000..4eec7f0
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/service/RecommendationService.java
@@ -0,0 +1,112 @@
+package ru.practicum.service;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import org.springframework.stereotype.Service;
+import ru.practicum.ewm.stats.proto.RecommendationsMessages;
+import ru.practicum.model.EventSimilarity;
+import ru.practicum.model.RecommendedEvent;
+import ru.practicum.model.UserAction;
+import ru.practicum.repository.EventSimilarityRepository;
+import ru.practicum.repository.UserActionRepository;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class RecommendationService {
+
+ final UserActionRepository userActionRepo;
+ final EventSimilarityRepository similarityRepo;
+
+ public List getSimilarEvents(RecommendationsMessages.SimilarEventsRequestProto requestProto) {
+ long eventId = requestProto.getEventId();
+ long userId = requestProto.getUserId();
+ int maxResults = requestProto.getMaxResults();
+
+ Set interacted = userInteracted(userId);
+ List result, recList = new ArrayList<>();
+
+ similarityRepo.findByEventAOrEventB(eventId, eventId)
+ .forEach(e -> {
+ long other = (e.getEventA() == eventId) ? e.getEventB() : e.getEventA();
+ if (!interacted.contains(other)) {
+ recList.add(new RecommendedEvent(other, e.getScore()));
+ }
+ });
+ result = recList.stream()
+ .sorted(Comparator.comparingDouble(RecommendedEvent::score).reversed()).toList();
+
+ return result.size() <= maxResults ? result : result.subList(0, maxResults);
+ }
+
+ public List getRecommendationsForUser(RecommendationsMessages.UserPredictionsRequestProto request) {
+ long userId = request.getUserId();
+ int maxRes = request.getMaxResults();
+
+ List all = userActionRepo.findByUserId(userId);
+ if (all.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ all.sort((a,b) -> b.getLastInteraction().compareTo(a.getLastInteraction()));
+
+ int min = Math.min(5, all.size());
+ List recent = all.subList(0, min);
+
+ Set interacted = userInteracted(userId);
+
+ Map bestScoreMap = new HashMap<>();
+ for (UserAction r : recent) {
+ long ev = r.getEventId();
+ List simList = similarityRepo.findByEventAOrEventB(ev, ev);
+ for (EventSimilarity e : simList) {
+ long other = (e.getEventA() == ev) ? e.getEventB() : e.getEventA();
+ if (interacted.contains(other)) {
+ continue;
+ }
+ float oldVal = bestScoreMap.getOrDefault(other, 0f);
+ if (e.getScore() > oldVal) {
+ bestScoreMap.put(other, e.getScore());
+ }
+ }
+ }
+
+ return bestScoreMap.entrySet().stream()
+ .map(e -> new RecommendedEvent(e.getKey(), e.getValue()))
+ .sorted(Comparator.comparingDouble(RecommendedEvent::score).reversed())
+ .limit(maxRes)
+ .collect(Collectors.toList());
+ }
+
+ public List getInteractionsCount(RecommendationsMessages.InteractionsCountRequestProto request) {
+ List events = request.getEventIdList();
+ List result = new ArrayList<>();
+
+ for (Long e : events) {
+ List list = userActionRepo.findByEventId(e);
+ double sum = 0.0;
+ for (UserAction uae : list) {
+ sum += uae.getMaxWeight();
+ }
+ result.add(new RecommendedEvent(e, (float) sum));
+ }
+ return result;
+ }
+
+ private Set userInteracted(long userId) {
+ return userActionRepo.findByUserId(userId)
+ .stream()
+ .map(UserAction::getEventId)
+ .collect(Collectors.toSet());
+ }
+}
\ No newline at end of file
diff --git a/stats/analyzer/src/main/java/ru/practicum/service/event/EventSimilarityService.java b/stats/analyzer/src/main/java/ru/practicum/service/event/EventSimilarityService.java
new file mode 100644
index 0000000..0686ad7
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/service/event/EventSimilarityService.java
@@ -0,0 +1,7 @@
+package ru.practicum.service.event;
+
+import ru.practicum.ewm.stats.avro.EventSimilarityAvro;
+
+public interface EventSimilarityService {
+ void updateEventSimilarity(EventSimilarityAvro eventSimilarityAvro);
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/service/event/EventSimilarityServiceImpl.java b/stats/analyzer/src/main/java/ru/practicum/service/event/EventSimilarityServiceImpl.java
new file mode 100644
index 0000000..814fdd0
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/service/event/EventSimilarityServiceImpl.java
@@ -0,0 +1,50 @@
+package ru.practicum.service.event;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.practicum.ewm.stats.avro.EventSimilarityAvro;
+import ru.practicum.model.EventSimilarity;
+import ru.practicum.repository.EventSimilarityRepository;
+
+import java.time.Instant;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class EventSimilarityServiceImpl implements EventSimilarityService {
+
+ private final EventSimilarityRepository eventSimilarityRepository;
+
+ @Override
+ public void updateEventSimilarity(EventSimilarityAvro eventSimilarityAvro) {
+ long eventA = eventSimilarityAvro.getEventA();
+ long eventB = eventSimilarityAvro.getEventB();
+ float score = eventSimilarityAvro.getScore();
+ Instant timestamp = eventSimilarityAvro.getTimestamp();
+
+ EventSimilarity existingEventSimilarity = findPair(eventA, eventB);
+
+ if (existingEventSimilarity == null) {
+ existingEventSimilarity = new EventSimilarity();
+ existingEventSimilarity.setEventA(eventA);
+ existingEventSimilarity.setEventB(eventB);
+ existingEventSimilarity.setScore(score);
+ existingEventSimilarity.setTimestamp(timestamp);
+ eventSimilarityRepository.save(existingEventSimilarity);
+ } else {
+ existingEventSimilarity.setScore(score);
+ existingEventSimilarity.setTimestamp(timestamp);
+ eventSimilarityRepository.save(existingEventSimilarity);
+ }
+ }
+
+ private EventSimilarity findPair(long eventA, long eventB) {
+ return eventSimilarityRepository.findByEventAOrEventB(eventA, eventB)
+ .stream()
+ .filter(e -> (e.getEventA().equals(eventA) && e.getEventB().equals(eventB))
+ || (e.getEventA().equals(eventB) && e.getEventB().equals(eventA)))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/service/user/UserActionService.java b/stats/analyzer/src/main/java/ru/practicum/service/user/UserActionService.java
new file mode 100644
index 0000000..59ecda9
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/service/user/UserActionService.java
@@ -0,0 +1,7 @@
+package ru.practicum.service.user;
+
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+
+public interface UserActionService {
+ void updateUserAction(UserActionAvro userActionAvro);
+}
diff --git a/stats/analyzer/src/main/java/ru/practicum/service/user/UserActionServiceImpl.java b/stats/analyzer/src/main/java/ru/practicum/service/user/UserActionServiceImpl.java
new file mode 100644
index 0000000..0e46784
--- /dev/null
+++ b/stats/analyzer/src/main/java/ru/practicum/service/user/UserActionServiceImpl.java
@@ -0,0 +1,58 @@
+package ru.practicum.service.user;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.practicum.ewm.stats.avro.ActionTypeAvro;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+import ru.practicum.model.UserAction;
+import ru.practicum.repository.UserActionRepository;
+
+import java.time.Instant;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class UserActionServiceImpl implements UserActionService {
+
+ private final UserActionRepository userActionRepository;
+
+ @Override
+ public void updateUserAction(UserActionAvro userActionAvro) {
+ long userId = userActionAvro.getUserId();
+ long eventId = userActionAvro.getEventId();
+ double newWeight = convertWeight(userActionAvro.getActionType());
+ long timestamp = userActionAvro.getTimestamp();
+
+ Instant interactionTime = Instant.ofEpochMilli(timestamp);
+
+ UserAction userAction = userActionRepository.findByUserIdAndEventId(userId, eventId);
+
+ if (userAction == null) {
+ userAction = new UserAction();
+ userAction.setUserId(userId);
+ userAction.setEventId(eventId);
+ userAction.setMaxWeight(newWeight);
+ userAction.setLastInteraction(interactionTime);
+ userActionRepository.save(userAction);
+ return;
+ }
+ if (newWeight > userAction.getMaxWeight()) {
+ userAction.setMaxWeight(newWeight);
+ }
+ if (interactionTime.isAfter(userAction.getLastInteraction())) {
+ userAction.setLastInteraction(interactionTime);
+ }
+ userActionRepository.save(userAction);
+
+
+ }
+
+ private double convertWeight(ActionTypeAvro actionType) {
+ return switch (actionType) {
+ case REGISTER -> 0.8;
+ case LIKE -> 1;
+ default -> 0.4;
+ };
+ }
+}
diff --git a/stats/analyzer/src/main/resources/application.yml b/stats/analyzer/src/main/resources/application.yml
new file mode 100644
index 0000000..ccc4033
--- /dev/null
+++ b/stats/analyzer/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+spring:
+ application:
+ name: analyzer
+ config:
+ import: "configserver:"
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ useRandomPolicy: true
+ max-interval: 6000
+ discovery:
+ enabled: true
+ service-id: config-server
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ serviceUrl:
+ defaultZone: http://${eureka.instance.hostname:localhost}:${eureka.instance.port:8761}/eureka/
+ instance:
+ prefer-ip-address: true
+ hostname: localhost
+ instance-id: "${spring.application.name}:${random.value}"
+ lease-renewal-interval-in-seconds: 10
+server:
+ port: 0
\ No newline at end of file
diff --git a/stats/collector/Dockerfile b/stats/collector/Dockerfile
new file mode 100644
index 0000000..0ff1817
--- /dev/null
+++ b/stats/collector/Dockerfile
@@ -0,0 +1,5 @@
+FROM eclipse-temurin:21-jre-jammy
+VOLUME /tmp
+ARG JAR_FILE=target/*.jar
+COPY ${JAR_FILE} app.jar
+ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
\ No newline at end of file
diff --git a/stats/collector/pom.xml b/stats/collector/pom.xml
new file mode 100644
index 0000000..d251941
--- /dev/null
+++ b/stats/collector/pom.xml
@@ -0,0 +1,94 @@
+
+
+ 4.0.0
+
+ ru.practicum
+ stats
+ 0.0.1-SNAPSHOT
+
+
+ collector
+
+
+ 21
+ 21
+ UTF-8
+ 0.0.1-SNAPSHOT
+ 0.0.1-SNAPSHOT
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ net.devh
+ grpc-server-spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-config
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-netflix-eureka-client
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ ru.practicum
+ avro-schemas
+ ${avro-schemas.version}
+
+
+
+ ru.practicum
+ proto-schemas
+ ${proto-schemas.version}
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
+
\ No newline at end of file
diff --git a/stats/collector/src/main/java/ru/practicum/CollectorApplication.java b/stats/collector/src/main/java/ru/practicum/CollectorApplication.java
new file mode 100644
index 0000000..deb088a
--- /dev/null
+++ b/stats/collector/src/main/java/ru/practicum/CollectorApplication.java
@@ -0,0 +1,15 @@
+package ru.practicum;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+@SpringBootApplication
+@EnableDiscoveryClient
+@ConfigurationPropertiesScan
+public class CollectorApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(CollectorApplication.class, args);
+ }
+}
diff --git a/stats/collector/src/main/java/ru/practicum/config/KafkaProducerConfig.java b/stats/collector/src/main/java/ru/practicum/config/KafkaProducerConfig.java
new file mode 100644
index 0000000..d8e861b
--- /dev/null
+++ b/stats/collector/src/main/java/ru/practicum/config/KafkaProducerConfig.java
@@ -0,0 +1,38 @@
+package ru.practicum.config;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.DefaultKafkaProducerFactory;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.core.ProducerFactory;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+
+import java.util.Map;
+
+@Getter
+@Setter
+@Configuration
+@RequiredArgsConstructor
+public class KafkaProducerConfig {
+
+ private final KafkaProperties kafkaProperties;
+
+ @Bean
+ public ProducerFactory kafkaProducer() {
+ Map props = Map.of(
+ ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaProperties.getBootstrapServers(),
+ ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, kafkaProperties.getProducer().getKeySerializer(),
+ ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, kafkaProperties.getProducer().getValueSerializer()
+ );
+ return new DefaultKafkaProducerFactory<>(props);
+ }
+
+ @Bean
+ public KafkaTemplate kafkaTemplate() {
+ return new KafkaTemplate<>(kafkaProducer());
+ }
+}
\ No newline at end of file
diff --git a/stats/collector/src/main/java/ru/practicum/config/KafkaProperties.java b/stats/collector/src/main/java/ru/practicum/config/KafkaProperties.java
new file mode 100644
index 0000000..fe43fd3
--- /dev/null
+++ b/stats/collector/src/main/java/ru/practicum/config/KafkaProperties.java
@@ -0,0 +1,29 @@
+package ru.practicum.config;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties("spring.kafka")
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class KafkaProperties {
+
+ String bootstrapServers;
+
+ Producer producer = new Producer();
+
+ @Value("${collector.kafka.topic}")
+ String userActionsTopic;
+
+ @Data
+ @FieldDefaults(level = AccessLevel.PRIVATE)
+ public static class Producer {
+ String keySerializer;
+ String valueSerializer;
+ }
+}
diff --git a/stats/collector/src/main/java/ru/practicum/mapper/UserActionMapper.java b/stats/collector/src/main/java/ru/practicum/mapper/UserActionMapper.java
new file mode 100644
index 0000000..9d0e3e6
--- /dev/null
+++ b/stats/collector/src/main/java/ru/practicum/mapper/UserActionMapper.java
@@ -0,0 +1,29 @@
+package ru.practicum.mapper;
+
+import ru.practicum.ewm.stats.avro.ActionTypeAvro;
+import ru.practicum.ewm.stats.proto.ActionTypeProto;
+import ru.practicum.ewm.stats.proto.UserActionProto;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+
+public class UserActionMapper {
+
+ public static UserActionAvro toAvro(UserActionProto userActionProto) {
+ long timestampMillis = userActionProto.getTimestamp().getSeconds() * 1000
+ + userActionProto.getTimestamp().getNanos() / 1_000_000;
+
+ return UserActionAvro.newBuilder()
+ .setUserId(userActionProto.getUserId())
+ .setEventId(userActionProto.getEventId())
+ .setActionType(toAvroActionType(userActionProto.getActionType()))
+ .setTimestamp(timestampMillis)
+ .build();
+ }
+
+ private static ActionTypeAvro toAvroActionType(ActionTypeProto protoType) {
+ return switch (protoType) {
+ case ACTION_REGISTER -> ActionTypeAvro.REGISTER;
+ case ACTION_LIKE -> ActionTypeAvro.LIKE;
+ default -> ActionTypeAvro.VIEW;
+ };
+ }
+}
diff --git a/stats/collector/src/main/java/ru/practicum/service/KafkaMessageProducer.java b/stats/collector/src/main/java/ru/practicum/service/KafkaMessageProducer.java
new file mode 100644
index 0000000..26f5e8b
--- /dev/null
+++ b/stats/collector/src/main/java/ru/practicum/service/KafkaMessageProducer.java
@@ -0,0 +1,20 @@
+package ru.practicum.service;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.kafka.core.KafkaTemplate;
+import ru.practicum.config.KafkaProperties;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class KafkaMessageProducer implements MessageProducer {
+
+ private final KafkaTemplate kafkaTemplate;
+ private final KafkaProperties properties;
+
+ @Override
+ public void sendUserAction(UserActionAvro userActionAvro) {
+ kafkaTemplate.send(properties.getUserActionsTopic(), userActionAvro);
+ }
+}
\ No newline at end of file
diff --git a/stats/collector/src/main/java/ru/practicum/service/MessageProducer.java b/stats/collector/src/main/java/ru/practicum/service/MessageProducer.java
new file mode 100644
index 0000000..e259c37
--- /dev/null
+++ b/stats/collector/src/main/java/ru/practicum/service/MessageProducer.java
@@ -0,0 +1,7 @@
+package ru.practicum.service;
+
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+
+public interface MessageProducer {
+ void sendUserAction(UserActionAvro userActionAvro);
+}
\ No newline at end of file
diff --git a/stats/collector/src/main/java/ru/practicum/service/UserActionController.java b/stats/collector/src/main/java/ru/practicum/service/UserActionController.java
new file mode 100644
index 0000000..ab25fe8
--- /dev/null
+++ b/stats/collector/src/main/java/ru/practicum/service/UserActionController.java
@@ -0,0 +1,41 @@
+package ru.practicum.service;
+
+import com.google.protobuf.Empty;
+import io.grpc.Status;
+import io.grpc.StatusRuntimeException;
+import io.grpc.stub.StreamObserver;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.devh.boot.grpc.server.service.GrpcService;
+import ru.practicum.ewm.stats.proto.UserActionControllerGrpc;
+import ru.practicum.ewm.stats.proto.UserActionProto;
+import ru.practicum.mapper.UserActionMapper;
+import ru.practicum.ewm.stats.avro.UserActionAvro;
+
+@Slf4j
+@GrpcService
+@RequiredArgsConstructor
+public class UserActionController extends UserActionControllerGrpc.UserActionControllerImplBase {
+
+ private final MessageProducer messageProducer;
+
+ @Override
+ public void collectUserAction(UserActionProto request, StreamObserver responseObserver) {
+ try {
+ UserActionAvro userActionAvro = UserActionMapper.toAvro(request);
+ messageProducer.sendUserAction(userActionAvro);
+ responseObserver.onNext(Empty.getDefaultInstance());
+ responseObserver.onCompleted();
+ } catch (IllegalArgumentException e) {
+ log.error("IllegalArgumentException collectUserAction: {}", e.getMessage(), e);
+ responseObserver.onError(
+ new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e))
+ );
+ } catch (Exception e) {
+ log.error("error collectUserAction: {}", e.getMessage(), e);
+ responseObserver.onError(
+ new StatusRuntimeException(Status.UNKNOWN.withDescription("error").withCause(e))
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/stats/collector/src/main/resources/application.yml b/stats/collector/src/main/resources/application.yml
new file mode 100644
index 0000000..5f9dddc
--- /dev/null
+++ b/stats/collector/src/main/resources/application.yml
@@ -0,0 +1,28 @@
+spring:
+ application:
+ name: collector
+ config:
+ import: "configserver:"
+ cloud:
+ config:
+ fail-fast: true
+ retry:
+ useRandomPolicy: true
+ max-interval: 6000
+ discovery:
+ enabled: true
+ service-id: config-server
+
+eureka:
+ client:
+ register-with-eureka: true
+ fetch-registry: true
+ serviceUrl:
+ defaultZone: http://${eureka.instance.hostname:localhost}:${eureka.instance.port:8761}/eureka/
+ instance:
+ prefer-ip-address: true
+ hostname: localhost
+ instance-id: "${spring.application.name}:${random.value}"
+ lease-renewal-interval-in-seconds: 10
+server:
+ port: 0
\ No newline at end of file
diff --git a/stats/pom.xml b/stats/pom.xml
index 321657b..7c3a106 100644
--- a/stats/pom.xml
+++ b/stats/pom.xml
@@ -17,8 +17,10 @@
stats-client
- stats-dto
- stats-server
+ collector
+ aggregator
+ serialization
+ analyzer
diff --git a/stats/serialization/avro-schemas/pom.xml b/stats/serialization/avro-schemas/pom.xml
new file mode 100644
index 0000000..27077af
--- /dev/null
+++ b/stats/serialization/avro-schemas/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+ ru.practicum
+ serialization
+ 0.0.1-SNAPSHOT
+
+
+ avro-schemas
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ org.apache.avro
+ avro
+ ${avro.version}
+
+
+
+ org.apache.kafka
+ kafka-clients
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.apache.avro
+ avro-maven-plugin
+
+
+ schemas
+ generate-sources
+
+ idl-protocol
+
+
+ ${project.basedir}/src/main/avro
+ ${project.build.directory}/generated-sources/avro
+ String
+
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.5.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ${project.build.directory}/generated-sources
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/stats/serialization/avro-schemas/src/main/avro/EventSimilarityProtocol.avdl b/stats/serialization/avro-schemas/src/main/avro/EventSimilarityProtocol.avdl
new file mode 100644
index 0000000..60267df
--- /dev/null
+++ b/stats/serialization/avro-schemas/src/main/avro/EventSimilarityProtocol.avdl
@@ -0,0 +1,10 @@
+@namespace("ru.practicum.ewm.stats.avro")
+protocol EventSimilarityProtocol {
+
+ record EventSimilarityAvro {
+ long eventA;
+ long eventB;
+ float score;
+ timestamp_ms timestamp;
+ }
+}
\ No newline at end of file
diff --git a/stats/serialization/avro-schemas/src/main/avro/UserActionAvro.avdl b/stats/serialization/avro-schemas/src/main/avro/UserActionAvro.avdl
new file mode 100644
index 0000000..9920089
--- /dev/null
+++ b/stats/serialization/avro-schemas/src/main/avro/UserActionAvro.avdl
@@ -0,0 +1,16 @@
+@namespace("ru.practicum.ewm.stats.avro")
+protocol UserActionProtocol {
+
+ enum ActionTypeAvro {
+ VIEW,
+ REGISTER,
+ LIKE
+ }
+
+ record UserActionAvro {
+ long userId;
+ long eventId;
+ ActionTypeAvro actionType;
+ long timestamp;
+ }
+}
\ No newline at end of file
diff --git a/stats/serialization/avro-schemas/src/main/java/ru/practicum/AvroDeserializer.java b/stats/serialization/avro-schemas/src/main/java/ru/practicum/AvroDeserializer.java
new file mode 100644
index 0000000..2431732
--- /dev/null
+++ b/stats/serialization/avro-schemas/src/main/java/ru/practicum/AvroDeserializer.java
@@ -0,0 +1,51 @@
+package ru.practicum;
+
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.specific.SpecificDatumReader;
+import org.apache.avro.specific.SpecificRecordBase;
+import org.apache.kafka.common.serialization.Deserializer;
+
+import java.io.ByteArrayInputStream;
+import java.util.Map;
+
+public class AvroDeserializer implements Deserializer {
+
+ private final Class targetType;
+
+ public AvroDeserializer(Class targetType) {
+ this.targetType = targetType;
+ }
+
+ public AvroDeserializer() {
+ this.targetType = null;
+ }
+
+ @Override
+ public void configure(Map configs, boolean isKey) {
+
+ }
+
+ @Override
+ public T deserialize(String topic, byte[] data) {
+ if (data == null) {
+ return null;
+ }
+
+ if (targetType == null) {
+ throw new IllegalStateException("targetType is undefined in AvroDeserializer");
+ }
+
+ try {
+ SpecificDatumReader datumReader = new SpecificDatumReader<>(targetType);
+ ByteArrayInputStream in = new ByteArrayInputStream(data);
+ BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(in, null);
+ return datumReader.read(null, decoder);
+ } catch (Exception e) {
+ throw new RuntimeException("Couldn't deserialize avro message for topic " + topic, e);
+ }
+ }
+
+ @Override
+ public void close(){}
+}
diff --git a/stats/serialization/avro-schemas/src/main/java/ru/practicum/AvroSerializer.java b/stats/serialization/avro-schemas/src/main/java/ru/practicum/AvroSerializer.java
new file mode 100644
index 0000000..e958572
--- /dev/null
+++ b/stats/serialization/avro-schemas/src/main/java/ru/practicum/AvroSerializer.java
@@ -0,0 +1,39 @@
+package ru.practicum;
+
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DatumWriter;
+import org.apache.avro.io.EncoderFactory;
+import org.apache.avro.specific.SpecificDatumWriter;
+import org.apache.avro.specific.SpecificRecordBase;
+import org.apache.kafka.common.errors.SerializationException;
+import org.apache.kafka.common.serialization.Serializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Map;
+
+public class AvroSerializer implements Serializer {
+
+ @Override
+ public void configure(Map configs, boolean isKey) {}
+
+ @Override
+ public byte[] serialize(String topic, T data) {
+ if (data == null) {
+ return null;
+ }
+
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(out, null);
+ DatumWriter writer = new SpecificDatumWriter<>(data.getSchema());
+ writer.write(data, encoder);
+ encoder.flush();
+ return out.toByteArray();
+ } catch (IOException e) {
+ throw new SerializationException("Error has occurred during serialization avro-message", e);
+ }
+ }
+
+ @Override
+ public void close(){}
+}
diff --git a/stats/serialization/pom.xml b/stats/serialization/pom.xml
new file mode 100644
index 0000000..3f32dfb
--- /dev/null
+++ b/stats/serialization/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+ ru.practicum
+ stats
+ 0.0.1-SNAPSHOT
+
+
+ serialization
+ pom
+
+ avro-schemas
+ proto-schemas
+
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
\ No newline at end of file
diff --git a/stats/serialization/proto-schemas/pom.xml b/stats/serialization/proto-schemas/pom.xml
new file mode 100644
index 0000000..e1dd02e
--- /dev/null
+++ b/stats/serialization/proto-schemas/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ ru.practicum
+ serialization
+ 0.0.1-SNAPSHOT
+
+
+ proto-schemas
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ io.grpc
+ grpc-stub
+
+
+
+ io.grpc
+ grpc-protobuf
+
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 1.3.5
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.github.ascopes
+ protobuf-maven-plugin
+
+
+ ${protobuf.version}
+
+
+
+ io.grpc
+ protoc-gen-grpc-java
+ ${grpc.version}
+
+
+
+
+
+
+
+ generate
+
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.5.0
+
+
+ add-source
+ generate-sources
+
+ add-source
+
+
+
+ ${project.build.directory}/generated-sources/protobuf
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/stats/serialization/proto-schemas/src/main/protobuf/controller/recommendations_controller.proto b/stats/serialization/proto-schemas/src/main/protobuf/controller/recommendations_controller.proto
new file mode 100644
index 0000000..c2895c8
--- /dev/null
+++ b/stats/serialization/proto-schemas/src/main/protobuf/controller/recommendations_controller.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+package ru.practicum.ewm.stats.proto;
+
+import "messages/recommendations_messages.proto";
+import "google/protobuf/empty.proto";
+
+service RecommendationsController {
+ rpc GetRecommendationsForUser(UserPredictionsRequestProto)
+ returns (stream RecommendedEventProto);
+
+ rpc GetSimilarEvents(SimilarEventsRequestProto)
+ returns (stream RecommendedEventProto);
+
+ rpc GetInteractionsCount(InteractionsCountRequestProto)
+ returns (stream RecommendedEventProto);
+}
\ No newline at end of file
diff --git a/stats/serialization/proto-schemas/src/main/protobuf/controller/user_action_controller.proto b/stats/serialization/proto-schemas/src/main/protobuf/controller/user_action_controller.proto
new file mode 100644
index 0000000..0108be8
--- /dev/null
+++ b/stats/serialization/proto-schemas/src/main/protobuf/controller/user_action_controller.proto
@@ -0,0 +1,10 @@
+syntax = "proto3";
+
+package ru.practicum.ewm.stats.proto;
+
+import "google/protobuf/empty.proto";
+import "messages/user_action.proto";
+
+service UserActionController {
+ rpc CollectUserAction(UserActionProto) returns (google.protobuf.Empty);
+}
\ No newline at end of file
diff --git a/stats/serialization/proto-schemas/src/main/protobuf/messages/recommendations_messages.proto b/stats/serialization/proto-schemas/src/main/protobuf/messages/recommendations_messages.proto
new file mode 100644
index 0000000..f4c7216
--- /dev/null
+++ b/stats/serialization/proto-schemas/src/main/protobuf/messages/recommendations_messages.proto
@@ -0,0 +1,25 @@
+syntax = "proto3";
+
+package ru.practicum.ewm.stats.proto;
+
+import "google/protobuf/timestamp.proto";
+
+message UserPredictionsRequestProto {
+ int64 user_id = 1;
+ int32 max_results = 2;
+}
+
+message SimilarEventsRequestProto {
+ int64 event_id = 1;
+ int64 user_id = 2;
+ int32 max_results = 3;
+}
+
+message InteractionsCountRequestProto {
+ repeated int64 event_id = 1;
+}
+
+message RecommendedEventProto {
+ int64 event_id = 1;
+ double score = 2;
+}
\ No newline at end of file
diff --git a/stats/serialization/proto-schemas/src/main/protobuf/messages/user_action.proto b/stats/serialization/proto-schemas/src/main/protobuf/messages/user_action.proto
new file mode 100644
index 0000000..2f91b96
--- /dev/null
+++ b/stats/serialization/proto-schemas/src/main/protobuf/messages/user_action.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package ru.practicum.ewm.stats.proto;
+
+import "google/protobuf/timestamp.proto";
+
+option java_multiple_files = true;
+option java_package = "ru.practicum.ewm.stats.proto";
+
+enum ActionTypeProto {
+ ACTION_VIEW = 0;
+ ACTION_REGISTER = 1;
+ ACTION_LIKE = 2;
+}
+
+message UserActionProto {
+ int64 user_id = 1;
+ int64 event_id = 2;
+ ActionTypeProto action_type = 3;
+ google.protobuf.Timestamp timestamp = 4;
+}
\ No newline at end of file
diff --git a/stats/stats-client/pom.xml b/stats/stats-client/pom.xml
index b110bde..c53c1d0 100644
--- a/stats/stats-client/pom.xml
+++ b/stats/stats-client/pom.xml
@@ -16,35 +16,48 @@
21
21
UTF-8
+ 2.13.1.RELEASE
+
- ru.practicum
- stats-dto
- 0.0.1-SNAPSHOT
+ org.springframework.boot
+ spring-boot-starter-web
org.springframework.boot
- spring-boot-starter-web
+ spring-boot-starter-actuator
+
+
+
+ net.devh
+ grpc-client-spring-boot-starter
+ ${grpc.spring.boot.starter.version}
- org.springframework
- spring-web
+ io.grpc
+ grpc-stub
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
org.projectlombok
lombok
- true
+ provided
+
- org.springframework.cloud
- spring-cloud-openfeign-core
+ ru.practicum
+ proto-schemas
+ 0.0.1-SNAPSHOT
-
\ No newline at end of file
diff --git a/stats/stats-client/src/main/java/ru/practicum/AnalyzerClient.java b/stats/stats-client/src/main/java/ru/practicum/AnalyzerClient.java
new file mode 100644
index 0000000..0916c5d
--- /dev/null
+++ b/stats/stats-client/src/main/java/ru/practicum/AnalyzerClient.java
@@ -0,0 +1,78 @@
+package ru.practicum;
+
+import lombok.extern.slf4j.Slf4j;
+import net.devh.boot.grpc.client.inject.GrpcClient;
+import org.springframework.stereotype.Service;
+import ru.practicum.ewm.stats.proto.RecommendationsControllerGrpc;
+import ru.practicum.ewm.stats.proto.RecommendationsMessages;
+
+import java.util.Iterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+@Slf4j
+@Service
+public class AnalyzerClient {
+
+ @GrpcClient("analyzer")
+ private RecommendationsControllerGrpc.RecommendationsControllerBlockingStub analyzerStub;
+
+ public Stream getSimilarEvents(
+ long eventId, long userId, int maxResults) {
+ try {
+ log.info("Fetching similar events: eventId={}, userId={}, maxResults={}", eventId, userId, maxResults);
+ RecommendationsMessages.SimilarEventsRequestProto requestProto =
+ RecommendationsMessages.SimilarEventsRequestProto.newBuilder()
+ .setEventId(eventId)
+ .setUserId(userId)
+ .setMaxResults(maxResults)
+ .build();
+ Iterator iterator = analyzerStub.getSimilarEvents(requestProto);
+ return toStream(iterator);
+ } catch (Exception e) {
+ log.error("Error occurred while fetching similar events: eventId={}, userId={}, maxResults={}",
+ eventId, userId, maxResults);
+ return Stream.empty();
+ }
+ }
+
+ public Stream getRecommendationsForUser(long userId, int maxResults) {
+ try {
+ log.info("Fetching recommendations for user : userId={}, maxResults={}", userId, maxResults);
+ RecommendationsMessages.UserPredictionsRequestProto requestProto =
+ RecommendationsMessages.UserPredictionsRequestProto.newBuilder()
+ .setUserId(userId)
+ .setMaxResults(maxResults)
+ .build();
+ Iterator iterator = analyzerStub.getRecommendationsForUser(requestProto);
+ return toStream(iterator);
+ } catch (Exception e) {
+ log.error("Error occurred while fetching recommendations for user : userId={}, maxResults={}", userId, maxResults);
+ return Stream.empty();
+ }
+ }
+
+ public Stream getInteractionsCount(Iterable eventIds) {
+ try {
+ log.info("Fetching interactions count for events");
+ RecommendationsMessages.InteractionsCountRequestProto.Builder builder =
+ RecommendationsMessages.InteractionsCountRequestProto.newBuilder();
+ eventIds.forEach(builder::addEventId);
+ RecommendationsMessages.InteractionsCountRequestProto requestProto = builder.build();
+ Iterator iterator = analyzerStub.getInteractionsCount(requestProto);
+ return toStream(iterator);
+ } catch (Exception e) {
+ log.error("Error occurred while fetching interactions count", e);
+ return Stream.empty();
+ }
+ }
+
+ private Stream toStream(Iterator iterator) {
+ return StreamSupport.stream(
+ Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED),
+ false
+ );
+ }
+}
diff --git a/stats/stats-client/src/main/java/ru/practicum/CollectorClient.java b/stats/stats-client/src/main/java/ru/practicum/CollectorClient.java
new file mode 100644
index 0000000..c4a3ac1
--- /dev/null
+++ b/stats/stats-client/src/main/java/ru/practicum/CollectorClient.java
@@ -0,0 +1,55 @@
+package ru.practicum;
+
+import com.google.protobuf.Empty;
+import lombok.extern.slf4j.Slf4j;
+import net.devh.boot.grpc.client.inject.GrpcClient;
+import org.springframework.stereotype.Service;
+import ru.practicum.ewm.stats.proto.ActionTypeProto;
+import ru.practicum.ewm.stats.proto.UserActionControllerGrpc;
+import ru.practicum.ewm.stats.proto.UserActionProto;
+
+import java.time.Instant;
+
+@Slf4j
+@Service
+public class CollectorClient {
+
+ @GrpcClient("collector")
+ private UserActionControllerGrpc.UserActionControllerBlockingStub collectorStub;
+
+ public void sendUserAction(long userId, long eventId, ActionTypeProto actionTypeProto) {
+ try {
+ log.info("Sending user action: userId={}, eventId={}, actionType={}", userId, eventId, actionTypeProto);
+ long secondes = Instant.now().getEpochSecond();
+ int nanos = Instant.now().getNano();
+
+ UserActionProto userActionProto = UserActionProto.newBuilder()
+ .setUserId(userId)
+ .setEventId(eventId)
+ .setActionType(actionTypeProto)
+ .setTimestamp(
+ com.google.protobuf.Timestamp.newBuilder()
+ .setSeconds(secondes)
+ .setNanos(nanos)
+ )
+ .build();
+ collectorStub.collectUserAction(userActionProto);
+ log.info("sendUserAction -> Collector answered");
+ } catch (Exception e) {
+ log.error("Ошибка при отправке действия пользователя: userId={}, eventId={}, actionType={}",
+ userId, eventId, actionTypeProto, e);
+ }
+ }
+
+ public void sendEventView(long userId, long eventId) {
+ sendUserAction(userId, eventId, ActionTypeProto.ACTION_VIEW);
+ }
+
+ public void sendEventLike(long userId, long eventId) {
+ sendUserAction(userId, eventId, ActionTypeProto.ACTION_LIKE);
+ }
+
+ public void sendEventRegistration(long userId, long eventId) {
+ sendUserAction(userId, eventId, ActionTypeProto.ACTION_REGISTER);
+ }
+}
diff --git a/stats/stats-client/src/main/java/ru/practicum/StatClient.java b/stats/stats-client/src/main/java/ru/practicum/StatClient.java
index b371b8d..c95620a 100644
--- a/stats/stats-client/src/main/java/ru/practicum/StatClient.java
+++ b/stats/stats-client/src/main/java/ru/practicum/StatClient.java
@@ -1,24 +1,24 @@
-package ru.practicum;
-
-import org.springframework.cloud.openfeign.FeignClient;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestParam;
-import ru.practicum.dto.EndpointHitDto;
-import ru.practicum.dto.ViewStatsDto;
-
-import java.util.List;
-
-@FeignClient(name = "stats-server")
-public interface StatClient {
-
- @PostMapping("/hit")
- void saveHit(@RequestBody EndpointHitDto hitDto);
-
- @GetMapping("stats")
- List getStats(@RequestParam(defaultValue = "") String start,
- @RequestParam(defaultValue = "") String end,
- @RequestParam(defaultValue = "") List uris,
- @RequestParam(defaultValue = "false") Boolean unique);
-}
\ No newline at end of file
+//package ru.practicum;
+//
+//import org.springframework.cloud.openfeign.FeignClient;
+//import org.springframework.web.bind.annotation.GetMapping;
+//import org.springframework.web.bind.annotation.PostMapping;
+//import org.springframework.web.bind.annotation.RequestBody;
+//import org.springframework.web.bind.annotation.RequestParam;
+//import ru.practicum.dto.EndpointHitDto;
+//import ru.practicum.dto.ViewStatsDto;
+//
+//import java.util.List;
+//
+//@FeignClient(name = "stats-server")
+//public interface StatClient {
+//
+// @PostMapping("/hit")
+// void saveHit(@RequestBody EndpointHitDto hitDto);
+//
+// @GetMapping("stats")
+// List getStats(@RequestParam(defaultValue = "") String start,
+// @RequestParam(defaultValue = "") String end,
+// @RequestParam(defaultValue = "") List uris,
+// @RequestParam(defaultValue = "false") Boolean unique);
+//}
\ No newline at end of file
diff --git a/stats/stats-client/src/main/java/ru/practicum/StatServiceClient.java b/stats/stats-client/src/main/java/ru/practicum/StatServiceClient.java
index 40afb07..2ced547 100644
--- a/stats/stats-client/src/main/java/ru/practicum/StatServiceClient.java
+++ b/stats/stats-client/src/main/java/ru/practicum/StatServiceClient.java
@@ -1,34 +1,34 @@
-package ru.practicum;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import ru.practicum.dto.EndpointHitDto;
-import ru.practicum.dto.ViewStatsDto;
-
-import java.util.Collections;
-import java.util.List;
-
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class StatServiceClient {
-
- private final StatClient statClient;
-
- public void saveHit(EndpointHitDto dto) {
- statClient.saveHit(dto);
- }
-
- public List getStats(String start,
- String end,
- List uris,
- Boolean unique) {
- try {
- return statClient.getStats(start, end, uris, unique);
- } catch (Exception e) {
- log.warn("Failed to get stats: {}", e.getMessage());
- }
- return Collections.emptyList();
- }
-}
\ No newline at end of file
+//package ru.practicum;
+//
+//import lombok.RequiredArgsConstructor;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.stereotype.Component;
+//import ru.practicum.dto.EndpointHitDto;
+//import ru.practicum.dto.ViewStatsDto;
+//
+//import java.util.Collections;
+//import java.util.List;
+//
+//@Slf4j
+//@Component
+//@RequiredArgsConstructor
+//public class StatServiceClient {
+//
+// private final StatClient statClient;
+//
+// public void saveHit(EndpointHitDto dto) {
+// statClient.saveHit(dto);
+// }
+//
+// public List getStats(String start,
+// String end,
+// List uris,
+// Boolean unique) {
+// try {
+// return statClient.getStats(start, end, uris, unique);
+// } catch (Exception e) {
+// log.warn("Failed to get stats: {}", e.getMessage());
+// }
+// return Collections.emptyList();
+// }
+//}
\ No newline at end of file
diff --git a/stats/stats-client/src/main/resources/application.properties b/stats/stats-client/src/main/resources/application.properties
deleted file mode 100644
index f181fe5..0000000
--- a/stats/stats-client/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-client.url=http://stats-server:9090
\ No newline at end of file
diff --git a/stats/stats-client/src/main/resources/application.yml b/stats/stats-client/src/main/resources/application.yml
new file mode 100644
index 0000000..cb75a5d
--- /dev/null
+++ b/stats/stats-client/src/main/resources/application.yml
@@ -0,0 +1,13 @@
+grpc:
+ client:
+ collector:
+ address: 'discovery:///collector'
+ enableKeepAlive: true
+ keepAliveWithoutCalls: true
+ negotiationType: plaintext
+
+ analyzer:
+ address: 'discovery:///analyzer'
+ enableKeepAlive: true
+ keepAliveWithoutCalls: true
+ negotiationType: plaintext
\ No newline at end of file
diff --git a/stats/stats-dto/pom.xml b/stats/stats-dto/pom.xml
deleted file mode 100644
index e810cb1..0000000
--- a/stats/stats-dto/pom.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
- 4.0.0
-
- ru.practicum
- stats
- 0.0.1-SNAPSHOT
-
-
- stats-dto
-
-
- 21
- 21
- UTF-8
-
-
-
-
- org.springframework.boot
- spring-boot-starter-validation
-
-
- org.projectlombok
- lombok
- provided
-
-
- org.springframework.boot
- spring-boot-starter-actuator
-
-
- com.fasterxml.jackson.datatype
- jackson-datatype-jsr310
-
-
-
\ No newline at end of file
diff --git a/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitDto.java b/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitDto.java
deleted file mode 100644
index 79babb3..0000000
--- a/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitDto.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package ru.practicum.dto;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import jakarta.validation.constraints.Pattern;
-import jakarta.validation.constraints.Size;
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.FieldDefaults;
-
-/**
- * The type Endpoint hit dto.
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-@FieldDefaults(level = AccessLevel.PRIVATE)
-public class EndpointHitDto {
- @JsonProperty(access = JsonProperty.Access.READ_ONLY)
- Integer id;
- @Size(max = 255)
- String app;
- @Size(max = 2048)
- String uri;
- @Pattern(
- regexp = "^((25[0-5]|2[0-4]\\d|1?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|1?\\d\\d?)$",
- message = "Неверный формат IP-адреса"
- )
- String ip;
- String timestamp;
-
- /**
- * Instantiates a new Endpoint hit dto.
- *
- * @param app the app
- * @param uri the uri
- * @param ip the ip
- * @param timestamp the timestamp
- */
-// Дополнительный конструктор без id
- public EndpointHitDto(String app, String uri, String ip, String timestamp) {
- this.app = app;
- this.uri = uri;
- this.ip = ip;
- this.timestamp = timestamp;
- }
-}
diff --git a/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitResponseDto.java b/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitResponseDto.java
deleted file mode 100644
index cd1f8ab..0000000
--- a/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitResponseDto.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package ru.practicum.dto;
-
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.FieldDefaults;
-
-/**
- * The type Endpoint hit response dto.
- */
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-@FieldDefaults(level = AccessLevel.PRIVATE)
-public class EndpointHitResponseDto {
-
- String app;
- String uri;
- String ip;
-}
diff --git a/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitSaveRequestDto.java b/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitSaveRequestDto.java
deleted file mode 100644
index 81441b9..0000000
--- a/stats/stats-dto/src/main/java/ru/practicum/dto/EndpointHitSaveRequestDto.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package ru.practicum.dto;
-
-import com.fasterxml.jackson.annotation.JsonFormat;
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.FieldDefaults;
-
-import java.time.LocalDateTime;
-
-/**
- * The type Endpoint hit save request dto.
- */
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-@FieldDefaults(level = AccessLevel.PRIVATE)
-public class EndpointHitSaveRequestDto {
-
- String app;
- String uri;
- String ip;
- @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
- LocalDateTime timestamp;
-
-}
diff --git a/stats/stats-dto/src/main/java/ru/practicum/dto/SecondaryViewStatsDto.java b/stats/stats-dto/src/main/java/ru/practicum/dto/SecondaryViewStatsDto.java
deleted file mode 100644
index 48603cf..0000000
--- a/stats/stats-dto/src/main/java/ru/practicum/dto/SecondaryViewStatsDto.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package ru.practicum.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-/**
- * The type Secondary view stats dto.
- */
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-public class SecondaryViewStatsDto {
- /**
- * The App.
- */
- String app;
- /**
- * The Uri.
- */
- String uri;
- /**
- * The Hits.
- */
- Long hits;
-}
diff --git a/stats/stats-server/src/main/java/ru/practicum/ErrorResponse.java b/stats/stats-server/src/main/java/ru/practicum/ErrorResponse.java
deleted file mode 100644
index 6965644..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/ErrorResponse.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package ru.practicum;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import lombok.Data;
-import org.slf4j.Logger;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-
-@Data
-public class ErrorResponse {
-
- @JsonProperty("error")
- private String message;
- @JsonIgnore
- private String stacktrace;
-
- public ErrorResponse(String message) {
- this.message = message;
- }
-
- public static ErrorResponse getErrorResponse(Exception e, Logger log) {
- log.info("Error", e);
- ErrorResponse errorResponse = new ErrorResponse(e.getMessage());
- StringWriter stringWriter = new StringWriter();
- PrintWriter pw = new PrintWriter(stringWriter);
- e.printStackTrace(pw);
- errorResponse.setStacktrace(pw.toString());
- return errorResponse;
- }
-}
\ No newline at end of file
diff --git a/stats/stats-server/src/main/java/ru/practicum/controller/StatController.java b/stats/stats-server/src/main/java/ru/practicum/controller/StatController.java
deleted file mode 100644
index 5bfbc21..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/controller/StatController.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package ru.practicum.controller;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.format.annotation.DateTimeFormat;
-import org.springframework.http.HttpStatus;
-import org.springframework.web.bind.annotation.*;
-import ru.practicum.ErrorResponse;
-import ru.practicum.dto.EndpointHitDto;
-import ru.practicum.dto.ViewStatsDto;
-import ru.practicum.service.StatService;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-
-@RestController
-@Slf4j
-public class StatController {
-
- private final StatService statsService;
-
- @Autowired
- public StatController(StatService service) {
- this.statsService = service;
- }
-
-
- @PostMapping("/hit")
- @ResponseStatus(HttpStatus.CREATED)
- public EndpointHitDto saveHit(@RequestBody EndpointHitDto hitDto) {
-
- return statsService.saveHit(hitDto);
- }
-
- @GetMapping("/stats")
- public List getHits(@RequestParam(value = "start", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start,
- @RequestParam(value = "end", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end,
- @RequestParam(value = "uris", required = false) List uris,
- @RequestParam(value = "unique", defaultValue = "false") boolean unique
- ) {
- if (start == null || end == null) {
- throw new IllegalArgumentException("Время не может быть Null");
- }
- if (start.isAfter(end)) {
- throw new IllegalArgumentException("Дата начала не может быть позже даты конца");
- }
- if (uris == null) {
- uris = new ArrayList<>();
- }
- log.info("/GET запрос на получение статистики");
- return statsService.getStats(start, end, uris, unique);
- }
-
- @ExceptionHandler
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- public ErrorResponse handleException(final IllegalArgumentException e) {
- ErrorResponse errorResponse = new ErrorResponse(e.getMessage());
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- e.printStackTrace(pw);
- errorResponse.setStacktrace(pw.toString());
- return errorResponse;
- }
-}
\ No newline at end of file
diff --git a/stats/stats-server/src/main/java/ru/practicum/mapper/EndpointHitMapper.java b/stats/stats-server/src/main/java/ru/practicum/mapper/EndpointHitMapper.java
deleted file mode 100644
index 7252c50..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/mapper/EndpointHitMapper.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package ru.practicum.mapper;
-
-import lombok.experimental.UtilityClass;
-import ru.practicum.dto.EndpointHitDto;
-import ru.practicum.model.EndpointHit;
-
-import java.time.LocalDateTime;
-
-import static ru.practicum.util.Constants.FORMATTER;
-
-@UtilityClass
-public class EndpointHitMapper {
-
- public static EndpointHitDto toHitDto(EndpointHit hit) {
- String dateTime = hit.getTimestamp().format(FORMATTER);
-
- return new EndpointHitDto(
- hit.getId(),
- hit.getApp(),
- hit.getUri(),
- hit.getIp(),
- dateTime
- );
- }
-
- public static EndpointHit dtoToHit(EndpointHitDto hitDto) {
-
- LocalDateTime localDateTime = LocalDateTime.parse(hitDto.getTimestamp(), FORMATTER);
- EndpointHit hit = new EndpointHit();
- hit.setId(hitDto.getId());
- hit.setApp(hitDto.getApp());
- hit.setUri(hitDto.getUri());
- hit.setIp(hitDto.getIp());
- hit.setTimestamp(localDateTime);
- return hit;
- }
-}
\ No newline at end of file
diff --git a/stats/stats-server/src/main/java/ru/practicum/model/EndpointHit.java b/stats/stats-server/src/main/java/ru/practicum/model/EndpointHit.java
deleted file mode 100644
index 6b78cc2..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/model/EndpointHit.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package ru.practicum.model;
-
-import jakarta.persistence.*;
-import lombok.*;
-import lombok.experimental.FieldDefaults;
-
-import java.time.LocalDateTime;
-
-/**
- * The type Endpoint hit.
- */
-@Setter
-@Getter
-@Entity
-@Table(name = "endpoint_hit")
-@AllArgsConstructor
-@FieldDefaults(level = AccessLevel.PRIVATE)
-@NoArgsConstructor
-public class EndpointHit {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- Integer id;
-
- @Column(name = "app")
- String app;
- @Column(name = "uri")
- String uri;
- @Column(name = "ip")
- String ip;
- @Column(name = "timestamp")
- LocalDateTime timestamp;
-
-}
\ No newline at end of file
diff --git a/stats/stats-server/src/main/java/ru/practicum/repository/EndpointHitRepository.java b/stats/stats-server/src/main/java/ru/practicum/repository/EndpointHitRepository.java
deleted file mode 100644
index f786e29..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/repository/EndpointHitRepository.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package ru.practicum.repository;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
-
-import ru.practicum.dto.ViewStatsDto;
-import ru.practicum.model.EndpointHit;
-
-import java.time.LocalDateTime;
-import java.util.List;
-
-/**
- * The interface Endpoint hit repository.
- */
-public interface EndpointHitRepository extends JpaRepository {
-
- String SELECT_STAT_WITHOUT_UNIQUE_IP_SQL = "SELECT " +
- "new ru.practicum.dto.ViewStatsDto(e.app, e.uri, " +
- "(SELECT count(ep.ip) FROM EndpointHit AS ep WHERE ep.uri = e.uri) AS hits) " +
- "FROM EndpointHit AS e WHERE e.uri IN ( ?3 ) AND e.timestamp BETWEEN ?1 AND ?2 " +
- "GROUP BY e.uri, e.app ORDER BY hits DESC ";
-
- /**
- * The constant SELECT_STAT_WITH_UNIQUE_IP_SQL.
- */
- String SELECT_STAT_WITH_UNIQUE_IP_SQL = "SELECT " +
- "new ru.practicum.dto.ViewStatsDto(e.app, e.uri, " +
- "(SELECT count(DISTINCT ep.ip) FROM EndpointHit AS ep WHERE ep.uri = e.uri) AS hits) " +
- "FROM EndpointHit AS e WHERE e.uri IN ( ?3 ) AND e.timestamp BETWEEN ?1 AND ?2 " +
- "GROUP BY e.uri, e.app ORDER BY hits DESC ";
-
- /**
- * The constant SELECT_STAT_ALL_WITHOUT_UNIQUE_IP_SQL.
- */
- String SELECT_STAT_ALL_WITHOUT_UNIQUE_IP_SQL = "SELECT " +
- "new ru.practicum.dto.ViewStatsDto(e.app, e.uri, " +
- "(SELECT count(ep.ip) FROM EndpointHit AS ep WHERE ep.uri = e.uri) AS hits) " +
- "FROM EndpointHit AS e WHERE e.timestamp BETWEEN ?1 AND ?2 GROUP BY e.uri, e.app ORDER BY hits DESC ";
-
- /**
- * The constant SELECT_STAT_ALL_WITH_UNIQUE_IP_SQL.
- */
- String SELECT_STAT_ALL_WITH_UNIQUE_IP_SQL = "SELECT " +
- "new ru.practicum.dto.ViewStatsDto(e.app, e.uri, " +
- "(SELECT count(DISTINCT ep.ip) FROM EndpointHit AS ep WHERE ep.uri = e.uri) AS hits) " +
- "FROM EndpointHit AS e WHERE e.timestamp BETWEEN ?1 AND ?2 GROUP BY e.uri, e.app ORDER BY hits DESC ";
-
- /**
- * Find stat without unique ip list.
- *
- * @param start the start
- * @param end the end
- * @param uris the uris
- * @return the list
- */
- @Query(SELECT_STAT_WITHOUT_UNIQUE_IP_SQL)
- List findStatWithoutUniqueIp(LocalDateTime start, LocalDateTime end, List uris);
-
- /**
- * Find stat with unique ip list.
- *
- * @param start the start
- * @param end the end
- * @param uris the uris
- * @return the list
- */
- @Query(SELECT_STAT_WITH_UNIQUE_IP_SQL)
- List findStatWithUniqueIp(LocalDateTime start, LocalDateTime end, List uris);
-
- /**
- * Find stat all without unique ip list.
- *
- * @param start the start
- * @param end the end
- * @return the list
- */
- @Query(SELECT_STAT_ALL_WITHOUT_UNIQUE_IP_SQL)
- List findStatAllWithoutUniqueIp(LocalDateTime start, LocalDateTime end);
-
- /**
- * Find stat all with unique ip list.
- *
- * @param start the start
- * @param end the end
- * @return the list
- */
- @Query(SELECT_STAT_ALL_WITH_UNIQUE_IP_SQL)
- List findStatAllWithUniqueIp(LocalDateTime start, LocalDateTime end);
-}
\ No newline at end of file
diff --git a/stats/stats-server/src/main/java/ru/practicum/service/StatService.java b/stats/stats-server/src/main/java/ru/practicum/service/StatService.java
deleted file mode 100644
index a7d24b9..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/service/StatService.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package ru.practicum.service;
-
-import ru.practicum.dto.EndpointHitDto;
-import ru.practicum.dto.ViewStatsDto;
-
-import java.time.LocalDateTime;
-import java.util.List;
-
-public interface StatService {
-
- EndpointHitDto saveHit(EndpointHitDto hitDto);
-
- List getStats(LocalDateTime start, LocalDateTime end, List uris, Boolean unique);
-}
diff --git a/stats/stats-server/src/main/java/ru/practicum/service/StatServiceImpl.java b/stats/stats-server/src/main/java/ru/practicum/service/StatServiceImpl.java
deleted file mode 100644
index 25bfb72..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/service/StatServiceImpl.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package ru.practicum.service;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import ru.practicum.dto.*;
-import ru.practicum.repository.EndpointHitRepository;
-
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-
-import static ru.practicum.mapper.EndpointHitMapper.dtoToHit;
-import static ru.practicum.mapper.EndpointHitMapper.toHitDto;
-
-/**
- * The type Stat service.
- */
-@Service
-@RequiredArgsConstructor
-@Slf4j
-public class StatServiceImpl implements StatService {
-
- private final EndpointHitRepository endpointHitRepository;
-
- @Override
- @Transactional
- public EndpointHitDto saveHit(EndpointHitDto hitDto) {
- return toHitDto(endpointHitRepository.save(dtoToHit(hitDto)));
- }
-
- @Transactional(readOnly = true)
- @Override
- public List getStats(LocalDateTime start, LocalDateTime end, List uris, Boolean unique) {
- log.info("Получение статистики с параметрами: start={}, end={}, uris={}, unique={}", start, end, uris, unique);
- List stats;
-
- if (unique) {
- stats = uris.isEmpty() ?
- endpointHitRepository.findStatAllWithUniqueIp(start, end) :
- endpointHitRepository.findStatWithUniqueIp(start, end, uris);
- } else {
- stats = uris.isEmpty() ?
- endpointHitRepository.findStatAllWithoutUniqueIp(start, end) :
- endpointHitRepository.findStatWithoutUniqueIp(start, end, uris);
- }
- log.info("Полученные статистические данные: {}", stats);
- return new ArrayList<>(stats);
- }
-}
diff --git a/stats/stats-server/src/main/java/ru/practicum/util/Constants.java b/stats/stats-server/src/main/java/ru/practicum/util/Constants.java
deleted file mode 100644
index 8766eb6..0000000
--- a/stats/stats-server/src/main/java/ru/practicum/util/Constants.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package ru.practicum.util;
-
-import java.time.format.DateTimeFormatter;
-
-public class Constants {
-
- public static final String TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss";
-
- public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(TIMESTAMP_PATTERN);
-}
\ No newline at end of file
diff --git a/stats/stats-server/src/main/resources/application.yml b/stats/stats-server/src/main/resources/application.yml
deleted file mode 100644
index e5a978d..0000000
--- a/stats/stats-server/src/main/resources/application.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-spring:
- application:
- name: stats-server
- config:
- import: optional:configserver:http://config-server:9091
- cloud:
- config:
- enabled: false
- datasource:
- driverClassName: org.postgresql.Driver
- url: jdbc:postgresql://ewm-db:5432/ewm-stats
- username: root
- password: root
- jpa:
- hibernate:
- ddl-auto: none
- database-platform: org.hibernate.dialect.PostgreSQLDialect
- generate-ddl: false
- properties:
- hibernate:
- format_sql: true
- show-sql: false
- sql:
- init:
- mode: always
-
-eureka:
- client:
- register-with-eureka: true
- fetch-registry: true
- serviceUrl:
- defaultZone: http://${eureka.instance.hostname:localhost}:${eureka.instance.port:8761}/eureka/
- instance:
- prefer-ip-address: true
- hostname: localhost
- instance-id: "${spring.application.name}:${random.value}"
- lease-renewal-interval-in-seconds: 10
-server:
- port: 9090
\ No newline at end of file
diff --git a/stats/stats-server/src/main/resources/schema.sql b/stats/stats-server/src/main/resources/schema.sql
deleted file mode 100644
index 49082a7..0000000
--- a/stats/stats-server/src/main/resources/schema.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-DROP TABLE IF EXISTS endpoint_hit;
-
-CREATE TABLE IF NOT EXISTS endpoint_hit (
- id SERIAL PRIMARY KEY,
- app VARCHAR(255),
- uri VARCHAR(255) NOT NULL,
- ip VARCHAR(255) NOT NULL,
- timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL
-);
\ No newline at end of file