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