From 91c92cd536bfa5c2cd9970b66950042181ade66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 10:32:55 +0400 Subject: [PATCH 1/9] recommendations --- API-event-service-specification.json | 26 ++-- .../practicum/api/event/EventPublicApi.java | 30 +++- .../ru/practicum/api/request/RequestApi.java | 8 + .../client/RequestClientAbstractHelper.java | 23 +++ .../ru/practicum/dto/event/EventFullDto.java | 2 +- .../ru/practicum/dto/event/EventShortDto.java | 2 +- .../ru/practicum/dto/event/EventSort.java | 2 +- .../service/CompilationMapper.java | 2 +- .../controller/EventPublicController.java | 26 +++- .../java/ru/practicum/event/dal/View.java | 33 ---- .../practicum/event/dal/ViewRepository.java | 25 --- .../event/service/EventAdminServiceImpl.java | 19 +-- .../practicum/event/service/EventMapper.java | 11 +- .../service/EventPrivateServiceImpl.java | 31 ++-- .../event/service/EventPublicService.java | 6 +- .../event/service/EventPublicServiceImpl.java | 111 ++++++------- core/request-service/pom.xml | 5 + .../request/controller/RequestController.java | 5 + .../request/dal/RequestRepository.java | 2 + .../request/service/RequestService.java | 17 +- docker-compose.yml | 34 ++++ .../config/aggregator/application.yaml | 61 ++++++++ .../config/analyzer/application.yaml | 57 +++++++ .../config/collector/application.yaml | 46 ++++++ .../config/event-service/application.yaml | 2 + .../config/gateway-server/application.yaml | 26 ++-- pom.xml | 112 +++++++++++-- stats/aggregator/pom.xml | 100 ++++++++++++ .../ru/practicum/AggregatorApplication.java | 15 ++ .../ru/practicum/kafka/KafkaController.java | 36 +++++ .../properties/CustomProperties.java | 50 ++++++ .../practicum/service/UserActionService.java | 147 ++++++++++++++++++ .../src/main/resources/application.yaml | 25 +++ stats/analyzer/pom.xml | 124 +++++++++++++++ .../ru/practicum/AnalyzerApplication.java | 15 ++ .../ru/practicum/dal/EventSimilarity.java | 43 +++++ .../dal/EventSimilarityRepository.java | 61 ++++++++ .../java/ru/practicum/dal/UserAction.java | 41 +++++ .../practicum/dal/UserActionRepository.java | 41 +++++ .../grpc/GrpcRecommendationsController.java | 62 ++++++++ .../practicum/kafka/KafkaConsumerConfig.java | 73 +++++++++ .../ru/practicum/kafka/KafkaController.java | 46 ++++++ .../mapper/EventSimilarityMapper.java | 19 +++ .../properties/CustomProperties.java | 55 +++++++ .../service/EventSimilarityService.java | 42 +++++ .../service/SimilarityReportService.java | 89 +++++++++++ .../practicum/service/UserActionService.java | 40 +++++ .../src/main/resources/application.yaml | 25 +++ stats/analyzer/src/main/resources/schema.sql | 1 + stats/avro-schemas/pom.xml | 78 ++++++++++ .../main/avro/EventSimilarityProtocol.avdl | 12 ++ .../src/main/avro/UserActionProtocol.avdl | 16 ++ .../AbstractAvroDeserializer.java | 34 ++++ .../EventsSimilarityAvroDeserializer.java | 11 ++ .../UserActionAvroDeserializer.java | 11 ++ .../serializer/GeneralAvroSerializer.java | 33 ++++ stats/collector/pom.xml | 112 +++++++++++++ .../ru/practicum/CollectorApplication.java | 15 ++ .../grpc/GrpcUserActionController.java | 32 ++++ .../kafka/KafkaProducerInitializer.java | 21 +++ .../ru/practicum/mapper/UserActionMapper.java | 64 ++++++++ .../properties/CustomProperties.java | 21 +++ .../practicum/service/UserActionService.java | 29 ++++ .../src/main/resources/application.yaml | 25 +++ stats/pom.xml | 5 + stats/proto-schemas/pom.xml | 90 +++++++++++ .../proto/message/similarity_reports.proto | 29 ++++ .../src/main/proto/message/user_action.proto | 23 +++ .../proto/service/collector_controller.proto | 13 ++ .../service/recommendations_controller.proto | 22 +++ stats/stats-client/pom.xml | 19 ++- .../ewm/client/GrpcConfiguration.java | 78 ++++++++++ .../practicum/ewm/client/GrpcStatClient.java | 128 +++++++++++++++ .../practicum/ewm/client/RestStatClient.java | 30 +++- .../ru/practicum/ewm/client/StatClient.java | 11 ++ 75 files changed, 2625 insertions(+), 211 deletions(-) delete mode 100644 core/event-service/src/main/java/ru/practicum/event/dal/View.java delete mode 100644 core/event-service/src/main/java/ru/practicum/event/dal/ViewRepository.java create mode 100644 infra/config-server/src/main/resources/config/aggregator/application.yaml create mode 100644 infra/config-server/src/main/resources/config/analyzer/application.yaml create mode 100644 infra/config-server/src/main/resources/config/collector/application.yaml create mode 100644 stats/aggregator/pom.xml create mode 100644 stats/aggregator/src/main/java/ru/practicum/AggregatorApplication.java create mode 100644 stats/aggregator/src/main/java/ru/practicum/kafka/KafkaController.java create mode 100644 stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java create mode 100644 stats/aggregator/src/main/java/ru/practicum/service/UserActionService.java create mode 100644 stats/aggregator/src/main/resources/application.yaml create mode 100644 stats/analyzer/pom.xml create mode 100644 stats/analyzer/src/main/java/ru/practicum/AnalyzerApplication.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarity.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarityRepository.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/dal/UserAction.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/dal/UserActionRepository.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/grpc/GrpcRecommendationsController.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/kafka/KafkaConsumerConfig.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/kafka/KafkaController.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/mapper/EventSimilarityMapper.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/service/EventSimilarityService.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/service/SimilarityReportService.java create mode 100644 stats/analyzer/src/main/java/ru/practicum/service/UserActionService.java create mode 100644 stats/analyzer/src/main/resources/application.yaml create mode 100644 stats/analyzer/src/main/resources/schema.sql create mode 100644 stats/avro-schemas/pom.xml create mode 100644 stats/avro-schemas/src/main/avro/EventSimilarityProtocol.avdl create mode 100644 stats/avro-schemas/src/main/avro/UserActionProtocol.avdl create mode 100644 stats/avro-schemas/src/main/java/ru/practicum/deserializer/AbstractAvroDeserializer.java create mode 100644 stats/avro-schemas/src/main/java/ru/practicum/deserializer/EventsSimilarityAvroDeserializer.java create mode 100644 stats/avro-schemas/src/main/java/ru/practicum/deserializer/UserActionAvroDeserializer.java create mode 100644 stats/avro-schemas/src/main/java/ru/practicum/serializer/GeneralAvroSerializer.java create mode 100644 stats/collector/pom.xml create mode 100644 stats/collector/src/main/java/ru/practicum/CollectorApplication.java create mode 100644 stats/collector/src/main/java/ru/practicum/grpc/GrpcUserActionController.java create mode 100644 stats/collector/src/main/java/ru/practicum/kafka/KafkaProducerInitializer.java create mode 100644 stats/collector/src/main/java/ru/practicum/mapper/UserActionMapper.java create mode 100644 stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java create mode 100644 stats/collector/src/main/java/ru/practicum/service/UserActionService.java create mode 100644 stats/collector/src/main/resources/application.yaml create mode 100644 stats/proto-schemas/pom.xml create mode 100644 stats/proto-schemas/src/main/proto/message/similarity_reports.proto create mode 100644 stats/proto-schemas/src/main/proto/message/user_action.proto create mode 100644 stats/proto-schemas/src/main/proto/service/collector_controller.proto create mode 100644 stats/proto-schemas/src/main/proto/service/recommendations_controller.proto create mode 100644 stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcConfiguration.java create mode 100644 stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcStatClient.java diff --git a/API-event-service-specification.json b/API-event-service-specification.json index 3de2ee2..c02dbcb 100644 --- a/API-event-service-specification.json +++ b/API-event-service-specification.json @@ -1571,7 +1571,7 @@ }, "paid": true, "title": "Знаменитое шоу 'Летающая кукуруза'", - "views": 999 + "rating": 6.6 }, { "annotation": "За почти три десятилетия группа 'Java Core' закрепились на сцене как группа, объединяющая поколения.", @@ -1588,7 +1588,7 @@ }, "paid": true, "title": "Концерт рок-группы 'Java Core'", - "views": 991 + "rating": 3.7 } ], "items": { @@ -1705,11 +1705,11 @@ "description": "Заголовок", "example": "Знаменитое шоу 'Летающая кукуруза'" }, - "views": { - "type": "integer", - "description": "Количество просмотрев события", - "format": "int64", - "example": 999 + "rating": { + "type": "number", + "description": "Рейтинг события", + "format": "float8", + "example": 4.8 } } }, @@ -1762,11 +1762,11 @@ "description": "Заголовок", "example": "Знаменитое шоу 'Летающая кукуруза'" }, - "views": { - "type": "integer", + "rating": { + "type": "number", "description": "Количество просмотрев события", - "format": "int64", - "example": 999 + "format": "float8", + "example": 5.4 } }, "description": "Краткая информация о событии", @@ -1786,7 +1786,7 @@ }, "paid": true, "title": "Знаменитое шоу 'Летающая кукуруза'", - "views": 999 + "rating": 9.8 }, { "annotation": "За почти три десятилетия группа 'Java Core' закрепились на сцене как группа, объединяющая поколения.", @@ -1803,7 +1803,7 @@ }, "paid": true, "title": "Концерт рок-группы 'Java Core'", - "views": 991 + "rating": 6.8 } ] }, diff --git a/core/core-common/src/main/java/ru/practicum/api/event/EventPublicApi.java b/core/core-common/src/main/java/ru/practicum/api/event/EventPublicApi.java index 7119219..baf5ff5 100644 --- a/core/core-common/src/main/java/ru/practicum/api/event/EventPublicApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/event/EventPublicApi.java @@ -30,32 +30,48 @@ List getAllEventsByParams( ); // Получение подробной информации об опубликованном событии по его идентификатору - @GetMapping("/events/{id}") + @GetMapping("/events/{eventId}") @ResponseStatus(HttpStatus.OK) EventFullDto getInformationAboutEventByEventId( - @PathVariable @Positive Long id, + @RequestHeader("X-EWM-USER-ID") @Positive Long userId, + @PathVariable @Positive Long eventId, HttpServletRequest request ); // Получение информации о событии для сервиса комментариев - @GetMapping("/events/{id}/dto/comment") + @GetMapping("/events/{eventId}/dto/comment") @ResponseStatus(HttpStatus.OK) EventCommentDto getEventCommentDto( - @PathVariable @Positive Long id + @PathVariable @Positive Long eventId ); // Получение информации о списке событий для сервиса комментариев @PostMapping("/events/dto/list/comment") @ResponseStatus(HttpStatus.OK) Collection getEventCommentDtoList( - @RequestBody Collection ids + @RequestBody Collection eventIds ); // Получение информации о событии для сервиса заявок - @GetMapping("/events/{id}/dto/interaction") + @GetMapping("/events/{eventId}/dto/interaction") @ResponseStatus(HttpStatus.OK) EventInteractionDto getEventInteractionDto( - @PathVariable @Positive Long id + @PathVariable @Positive Long eventId + ); + + // рекомендации мероприятий для пользователя + @GetMapping("/events/recommendations") + @ResponseStatus(HttpStatus.OK) + Collection getRecommendations( + @RequestHeader("X-EWM-USER-ID") @Positive Long userId, + @RequestParam(defaultValue = "10") Integer size + ); + + @PutMapping("/events/{eventId}/like") + @ResponseStatus(HttpStatus.OK) + String sendLike( + @RequestHeader("X-EWM-USER-ID") @Positive Long userId, + @PathVariable @Positive Long eventId ); } \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/api/request/RequestApi.java b/core/core-common/src/main/java/ru/practicum/api/request/RequestApi.java index 163c60a..c079c78 100644 --- a/core/core-common/src/main/java/ru/practicum/api/request/RequestApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/request/RequestApi.java @@ -66,4 +66,12 @@ Map getConfirmedRequestsByEventIds( @RequestBody Collection eventIds ); + // Проверка участия пользователя в конкретном событии перед лайком + @GetMapping("/users/{userId}/events/{eventId}/check/participation") + @ResponseStatus(HttpStatus.OK) + String checkParticipation( + @PathVariable @Positive(message = "User Id not valid") Long userId, + @PathVariable @Positive(message = "Event Id not valid") Long eventId + ); + } \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/client/RequestClientAbstractHelper.java b/core/core-common/src/main/java/ru/practicum/client/RequestClientAbstractHelper.java index d00145e..c16f12f 100644 --- a/core/core-common/src/main/java/ru/practicum/client/RequestClientAbstractHelper.java +++ b/core/core-common/src/main/java/ru/practicum/client/RequestClientAbstractHelper.java @@ -1,8 +1,10 @@ package ru.practicum.client; +import feign.FeignException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import ru.practicum.api.request.RequestApi; +import ru.practicum.exception.ServiceInteractionException; import java.util.Collection; import java.util.Map; @@ -24,4 +26,25 @@ public Map retrieveConfirmedRequestsMapByEventIdList(Collection comments; diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java index 7f3843c..b0e56c7 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java @@ -30,6 +30,6 @@ public class EventShortDto { private LocalDateTime eventDate; private Long confirmedRequests; - private Long views; + private Double rating; } diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java index ea36265..0a14767 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java @@ -1,5 +1,5 @@ package ru.practicum.dto.event; public enum EventSort { - EVENT_DATE, VIEWS + EVENT_DATE, VIEWS, RATING } diff --git a/core/event-service/src/main/java/ru/practicum/compilation/service/CompilationMapper.java b/core/event-service/src/main/java/ru/practicum/compilation/service/CompilationMapper.java index 8c961d6..60f4894 100644 --- a/core/event-service/src/main/java/ru/practicum/compilation/service/CompilationMapper.java +++ b/core/event-service/src/main/java/ru/practicum/compilation/service/CompilationMapper.java @@ -13,7 +13,7 @@ public class CompilationMapper { public static CompilationDto toCompilationDto(Compilation compilation, Map userMap) { List eventShortDtoList = compilation.getEvents().stream() - .map(e -> EventMapper.toEventShortDto(e, userMap.get(e.getInitiatorId()), 0L, 0L)) + .map(e -> EventMapper.toEventShortDto(e, userMap.get(e.getInitiatorId()), 0L, 0.0)) .toList(); return CompilationDto.builder() diff --git a/core/event-service/src/main/java/ru/practicum/event/controller/EventPublicController.java b/core/event-service/src/main/java/ru/practicum/event/controller/EventPublicController.java index f249745..e4f58b8 100644 --- a/core/event-service/src/main/java/ru/practicum/event/controller/EventPublicController.java +++ b/core/event-service/src/main/java/ru/practicum/event/controller/EventPublicController.java @@ -49,23 +49,33 @@ public List getAllEventsByParams( // Получение подробной информации об опубликованном событии по его идентификатору @Override - public EventFullDto getInformationAboutEventByEventId(Long id, HttpServletRequest request) { - return eventPublicService.getEventById(id, request); + public EventFullDto getInformationAboutEventByEventId(Long userId, Long eventId, HttpServletRequest request) { + return eventPublicService.getEventById(userId, eventId, request); } @Override - public EventCommentDto getEventCommentDto(Long id) { - return eventPublicService.getEventCommentDto(id); + public EventCommentDto getEventCommentDto(Long eventId) { + return eventPublicService.getEventCommentDto(eventId); } @Override - public Collection getEventCommentDtoList(Collection ids) { - return eventPublicService.getEventCommentDtoList(ids); + public Collection getEventCommentDtoList(Collection eventIds) { + return eventPublicService.getEventCommentDtoList(eventIds); } @Override - public EventInteractionDto getEventInteractionDto(Long id) { - return eventPublicService.getEventInteractionDto(id); + public EventInteractionDto getEventInteractionDto(Long eventId) { + return eventPublicService.getEventInteractionDto(eventId); + } + + @Override + public Collection getRecommendations(Long userId, Integer size) { + return eventPublicService.getRecommendations(userId, size); + } + + @Override + public String sendLike(Long userId, Long eventId) { + return eventPublicService.sendLike(userId, eventId); } } \ No newline at end of file diff --git a/core/event-service/src/main/java/ru/practicum/event/dal/View.java b/core/event-service/src/main/java/ru/practicum/event/dal/View.java deleted file mode 100644 index 4cce663..0000000 --- a/core/event-service/src/main/java/ru/practicum/event/dal/View.java +++ /dev/null @@ -1,33 +0,0 @@ -package ru.practicum.event.dal; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.annotations.OnDelete; -import org.hibernate.annotations.OnDeleteAction; - -@Getter -@Setter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -@ToString -@Entity -@Builder -@NoArgsConstructor -@AllArgsConstructor -@Table(name = "views", indexes = {@Index(name = "idx_views_event_id", columnList = "event_id")}) -public class View { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @EqualsAndHashCode.Include - @Column(name = "id") - private Long id; - - @ManyToOne - @JoinColumn(name = "event_id", nullable = false) - @OnDelete(action = OnDeleteAction.RESTRICT) - private Event event; - - @Column(name = "ip", length = 15, nullable = false) - private String ip; - -} diff --git a/core/event-service/src/main/java/ru/practicum/event/dal/ViewRepository.java b/core/event-service/src/main/java/ru/practicum/event/dal/ViewRepository.java deleted file mode 100644 index 2d2110e..0000000 --- a/core/event-service/src/main/java/ru/practicum/event/dal/ViewRepository.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.practicum.event.dal; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -public interface ViewRepository extends JpaRepository { - - long countByEventId(Long eventId); - - @Query(""" - SELECT v.event.id, count(v) - FROM View v - WHERE v.event.id IN :eventIds - GROUP BY v.event.id - """) - List countsByEventIds( - @Param("eventIds") List eventIds - ); - - boolean existsByEventIdAndIp(Long eventId, String ip); - -} diff --git a/core/event-service/src/main/java/ru/practicum/event/service/EventAdminServiceImpl.java b/core/event-service/src/main/java/ru/practicum/event/service/EventAdminServiceImpl.java index 6481c77..5e32846 100644 --- a/core/event-service/src/main/java/ru/practicum/event/service/EventAdminServiceImpl.java +++ b/core/event-service/src/main/java/ru/practicum/event/service/EventAdminServiceImpl.java @@ -15,7 +15,7 @@ import ru.practicum.event.dal.Event; import ru.practicum.event.dal.EventRepository; import ru.practicum.event.dal.JpaSpecifications; -import ru.practicum.event.dal.ViewRepository; +import ru.practicum.ewm.client.StatClient; import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; @@ -33,11 +33,12 @@ public class EventAdminServiceImpl implements EventAdminService { private final TransactionTemplate transactionTemplate; private final EventRepository eventRepository; private final CategoryRepository categoryRepository; - private final ViewRepository viewRepository; private final RequestClientHelper requestClientHelper; private final UserClientHelper userClientHelper; + private final StatClient statClient; + // Поиск событий @Override public List getAllEventsByParams(EventAdminParams params) { @@ -52,20 +53,14 @@ public List getAllEventsByParams(EventAdminParams params) { Map userMap = userClientHelper.retrieveUserShortDtoMapByUserIdList(userIds); Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(eventIds); - - Map viewsMap = viewRepository.countsByEventIds(eventIds) - .stream() - .collect(Collectors.toMap( - r -> (Long) r[0], - r -> (Long) r[1] - )); + Map ratingMap = statClient.getRatingsByEventIdList(eventIds); return events.stream() .map(e -> EventMapper.toEventFullDto( e, userMap.get(e.getInitiatorId()), confirmedRequestsMap.get(e.getId()), - viewsMap.get(e.getId()) + ratingMap.get(e.getId()) )) .toList(); } @@ -81,6 +76,7 @@ public EventFullDto updateEventByAdmin(Long eventId, UpdateEventDto updateEventD UserShortDto userShortDto = userClientHelper.retrieveUserShortDtoByUserId(initiatorId); Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(List.of(eventId)); + Map ratingMap = statClient.getRatingsByEventIdList(List.of(eventId)); return transactionTemplate.execute(status -> { Event event = eventRepository.findById(eventId) @@ -125,8 +121,7 @@ public EventFullDto updateEventByAdmin(Long eventId, UpdateEventDto updateEventD eventRepository.save(event); - Long views = viewRepository.countByEventId(eventId); - return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), views); + return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), ratingMap.get(eventId)); }); } diff --git a/core/event-service/src/main/java/ru/practicum/event/service/EventMapper.java b/core/event-service/src/main/java/ru/practicum/event/service/EventMapper.java index 0097075..e2f0e96 100644 --- a/core/event-service/src/main/java/ru/practicum/event/service/EventMapper.java +++ b/core/event-service/src/main/java/ru/practicum/event/service/EventMapper.java @@ -37,9 +37,10 @@ public static EventFullDto toEventFullDto( Event event, UserShortDto userShortDto, Long confirmedRequests, - Long views + Double rating ) { if (confirmedRequests == null) confirmedRequests = 0L; + if (rating == null) rating = 0.0; return EventFullDto.builder() .id(event.getId()) .initiator(userShortDto) @@ -56,7 +57,7 @@ public static EventFullDto toEventFullDto( .publishedOn(event.getPublishedOn()) .createdOn(event.getCreatedOn()) .confirmedRequests(confirmedRequests) - .views(views) + .rating(rating) .build(); } @@ -64,10 +65,10 @@ public static EventShortDto toEventShortDto( Event event, UserShortDto userShortDto, Long confirmedRequests, - Long views + Double rating ) { if (confirmedRequests == null) confirmedRequests = 0L; - if (views == null) views = 0L; + if (rating == null) rating = 0.0; return EventShortDto.builder() .id(event.getId()) .initiator(userShortDto) @@ -77,7 +78,7 @@ public static EventShortDto toEventShortDto( .paid(event.getPaid()) .eventDate(event.getEventDate()) .confirmedRequests(confirmedRequests) - .views(views) + .rating(rating) .build(); } diff --git a/core/event-service/src/main/java/ru/practicum/event/service/EventPrivateServiceImpl.java b/core/event-service/src/main/java/ru/practicum/event/service/EventPrivateServiceImpl.java index be1a315..c2324ae 100644 --- a/core/event-service/src/main/java/ru/practicum/event/service/EventPrivateServiceImpl.java +++ b/core/event-service/src/main/java/ru/practicum/event/service/EventPrivateServiceImpl.java @@ -14,7 +14,7 @@ import ru.practicum.dto.user.UserShortDto; import ru.practicum.event.dal.Event; import ru.practicum.event.dal.EventRepository; -import ru.practicum.event.dal.ViewRepository; +import ru.practicum.ewm.client.StatClient; import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -31,11 +30,12 @@ public class EventPrivateServiceImpl implements EventPrivateService { private final TransactionTemplate transactionTemplate; private final CategoryRepository categoryRepository; private final EventRepository eventRepository; - private final ViewRepository viewRepository; private final UserClientHelper userClientHelper; private final RequestClientHelper requestClientHelper; + private final StatClient statClient; + // Добавление нового события @Override public EventFullDto addEvent(Long userId, NewEventDto newEventDto) { @@ -47,7 +47,7 @@ public EventFullDto addEvent(Long userId, NewEventDto newEventDto) { Event newEvent = EventMapper.toNewEvent(newEventDto, userId, category); eventRepository.save(newEvent); - return EventMapper.toEventFullDto(newEvent, userShortDto, 0L, 0L); + return EventMapper.toEventFullDto(newEvent, userShortDto, 0L, 0.0); }); } @@ -62,14 +62,11 @@ public EventFullDto getEventByUserIdAndEventId(Long userId, Long eventId) { if (!Objects.equals(userId, event.getInitiatorId())) throw new ConflictException("User " + userId + " is not an initiator of event " + eventId, "Forbidden action"); - Long views = transactionTemplate.execute(status -> { - return viewRepository.countByEventId(eventId); - }); - UserShortDto userShortDto = userClientHelper.retrieveUserShortDtoByUserId(userId); Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(List.of(eventId)); + Map ratingMap = statClient.getRatingsByEventIdList(List.of(eventId)); - return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), views); + return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), ratingMap.get(eventId)); } // Получение событий, добавленных текущим пользователем @@ -85,22 +82,14 @@ public List getEventsByUserId(Long userId, Integer from, Integer List eventIds = events.stream().map(Event::getId).toList(); Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(eventIds); - - Map viewsMap = transactionTemplate.execute(status -> { - return viewRepository.countsByEventIds(eventIds) - .stream() - .collect(Collectors.toMap( - r -> (Long) r[0], - r -> (Long) r[1] - )); - }); + Map ratingMap = statClient.getRatingsByEventIdList(eventIds); return events.stream() .map(e -> EventMapper.toEventShortDto( e, userShortDto, confirmedRequestsMap.get(e.getId()), - viewsMap.get(e.getId()) + ratingMap.get(e.getId()) )) .toList(); } @@ -110,6 +99,7 @@ public List getEventsByUserId(Long userId, Integer from, Integer public EventFullDto updateEventByUserIdAndEventId(Long userId, Long eventId, UpdateEventDto updateEventDto) { UserShortDto userShortDto = userClientHelper.retrieveUserShortDtoByUserId(userId); Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(List.of(eventId)); + Map ratingMap = statClient.getRatingsByEventIdList(List.of(eventId)); return transactionTemplate.execute(status -> { Event event = eventRepository.findById(eventId) @@ -152,8 +142,7 @@ public EventFullDto updateEventByUserIdAndEventId(Long userId, Long eventId, Upd eventRepository.save(event); - Long views = viewRepository.countByEventId(eventId); - return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), views); + return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), ratingMap.get(eventId)); }); } diff --git a/core/event-service/src/main/java/ru/practicum/event/service/EventPublicService.java b/core/event-service/src/main/java/ru/practicum/event/service/EventPublicService.java index aeb4aa4..633507c 100644 --- a/core/event-service/src/main/java/ru/practicum/event/service/EventPublicService.java +++ b/core/event-service/src/main/java/ru/practicum/event/service/EventPublicService.java @@ -10,7 +10,7 @@ public interface EventPublicService { List getAllEventsByParams(EventParams eventParams, HttpServletRequest request); - EventFullDto getEventById(Long id, HttpServletRequest request); + EventFullDto getEventById(Long userId, Long eventId, HttpServletRequest request); EventCommentDto getEventCommentDto(Long id); @@ -18,4 +18,8 @@ public interface EventPublicService { EventInteractionDto getEventInteractionDto(Long id); + Collection getRecommendations(Long userId, Integer size); + + String sendLike(Long userId, Long eventId); + } diff --git a/core/event-service/src/main/java/ru/practicum/event/service/EventPublicServiceImpl.java b/core/event-service/src/main/java/ru/practicum/event/service/EventPublicServiceImpl.java index 49b67a0..053fab2 100644 --- a/core/event-service/src/main/java/ru/practicum/event/service/EventPublicServiceImpl.java +++ b/core/event-service/src/main/java/ru/practicum/event/service/EventPublicServiceImpl.java @@ -3,16 +3,16 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; -import ru.practicum.EventHitDto; import ru.practicum.client.RequestClientHelper; import ru.practicum.client.UserClientHelper; import ru.practicum.dto.event.*; import ru.practicum.dto.user.UserShortDto; -import ru.practicum.event.dal.*; +import ru.practicum.event.dal.Event; +import ru.practicum.event.dal.EventRepository; +import ru.practicum.event.dal.JpaSpecifications; import ru.practicum.ewm.client.StatClient; import ru.practicum.exception.BadRequestException; import ru.practicum.exception.NotFoundException; @@ -27,7 +27,6 @@ public class EventPublicServiceImpl implements EventPublicService { private final TransactionTemplate transactionTemplate; private final EventRepository eventRepository; - private final ViewRepository viewRepository; private final UserClientHelper userClientHelper; private final RequestClientHelper requestClientHelper; @@ -47,19 +46,18 @@ public List getAllEventsByParams(EventParams params, HttpServletR } List events = transactionTemplate.execute(status -> { - Sort sort = Sort.by(Sort.Direction.ASC, "eventDate"); - if (EventSort.VIEWS.equals(params.getEventSort())) sort = Sort.by(Sort.Direction.DESC, "views"); - PageRequest pageRequest = PageRequest.of(params.getFrom() / params.getSize(), params.getSize(), sort); + PageRequest pageRequest = PageRequest.of(params.getFrom() / params.getSize(), params.getSize()); return eventRepository.findAll(JpaSpecifications.publicFilters(params), pageRequest).getContent(); }); if (events == null) return List.of(); Set userIds = events.stream().map(Event::getInitiatorId).collect(Collectors.toSet()); - List eventIds = events.stream().map(Event::getId).toList(); - Map userMap = userClientHelper.retrieveUserShortDtoMapByUserIdList(userIds); + // информация о каждом событии должна включать в себя количество просмотров и количество уже одобренных заявок на участие + List eventIds = events.stream().map(Event::getId).toList(); Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(eventIds); + Map ratingMap = statClient.getRatingsByEventIdList(eventIds); if (params.getOnlyAvailable() == true && !confirmedRequestsMap.isEmpty()) { events = events.stream() @@ -71,70 +69,43 @@ public List getAllEventsByParams(EventParams params, HttpServletR }).toList(); } - Map viewsMap = Optional.ofNullable( - transactionTemplate.execute(status -> { - return viewRepository.countsByEventIds(eventIds) - .stream() - .collect(Collectors.toMap( - r -> (Long) r[0], - r -> (Long) r[1] - )); - }) - ).orElse(Map.of()); - - // информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики - statClient.hit(EventHitDto.builder() - .ip(request.getRemoteAddr()) - .uri(request.getRequestURI()) - .app("ewm-main-service") - .timestamp(LocalDateTime.now()) - .build()); - - return events.stream() + List unsortedResult = events.stream() .map(e -> EventMapper.toEventShortDto( e, userMap.get(e.getInitiatorId()), confirmedRequestsMap.get(e.getId()), - viewsMap.get(e.getId()) + ratingMap.get(e.getId()) )) .toList(); + + Comparator resultComparator = switch (params.getEventSort()) { + case VIEWS, RATING -> Comparator.comparing(EventShortDto::getRating).reversed(); + default -> Comparator.comparing(EventShortDto::getEventDate).reversed(); + }; + + return unsortedResult.stream() + .sorted(resultComparator) + .toList(); } // Получение подробной информации об опубликованном событии по его идентификатору @Override - public EventFullDto getEventById(Long eventId, HttpServletRequest request) { + public EventFullDto getEventById(Long userId, Long eventId, HttpServletRequest request) { Event event = transactionTemplate.execute(status -> { // событие должно быть опубликовано return eventRepository.findByIdAndState(eventId, State.PUBLISHED) .orElseThrow(() -> new NotFoundException("Event not found")); }); - Long views = transactionTemplate.execute(status -> { - Long viewsBefore = viewRepository.countByEventId(eventId); - // делаем новый уникальный просмотр - if (!viewRepository.existsByEventIdAndIp(eventId, request.getRemoteAddr())) { - View view = View.builder() - .event(event) - .ip(request.getRemoteAddr()) - .build(); - viewRepository.save(view); - } - return viewsBefore; - }); - - // информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики - statClient.hit(EventHitDto.builder() - .ip(request.getRemoteAddr()) - .uri(request.getRequestURI()) - .app("ewm-main-service") - .timestamp(LocalDateTime.now()) - .build()); - UserShortDto userShortDto = userClientHelper.retrieveUserShortDtoByUserId(event.getInitiatorId()); // информация о событии должна включать в себя количество просмотров и количество подтвержденных запросов Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(List.of(eventId)); + Map ratingMap = statClient.getRatingsByEventIdList(List.of(eventId)); - return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), views); + // информацию о том, что по этому эндпоинту был осуществлен и обработан запрос, нужно сохранить в сервисе статистики + statClient.sendView(userId, eventId); + + return EventMapper.toEventFullDto(event, userShortDto, confirmedRequestsMap.get(eventId), ratingMap.get(eventId)); } @Override @@ -163,4 +134,38 @@ public EventInteractionDto getEventInteractionDto(Long eventId) { return EventMapper.toInteractionDto(event); } + @Override + public Collection getRecommendations(Long userId, Integer size) { + Map recommendationMap = statClient.getUserRecommendations(userId, size); + if (recommendationMap.isEmpty()) return List.of(); + + List events = transactionTemplate.execute(status -> { + return eventRepository.findAllById(recommendationMap.keySet()); + }); + if (events == null || events.isEmpty()) return List.of(); + + Set userIds = events.stream().map(Event::getInitiatorId).collect(Collectors.toSet()); + Map userMap = userClientHelper.retrieveUserShortDtoMapByUserIdList(userIds); + + List eventIds = events.stream().map(Event::getId).toList(); + Map confirmedRequestsMap = requestClientHelper.retrieveConfirmedRequestsMapByEventIdList(eventIds); + + return events.stream() + .map(e -> EventMapper.toEventShortDto( + e, + userMap.get(e.getInitiatorId()), + confirmedRequestsMap.get(e.getId()), + recommendationMap.get(e.getId()) + )) + .sorted(Comparator.comparing(EventShortDto::getRating).reversed()) + .toList(); + } + + @Override + public String sendLike(Long userId, Long eventId) { + if (!requestClientHelper.passedParticipationCheck(userId, eventId)) + throw new BadRequestException("User " + userId + " tries to like event " + eventId + " in which he did not participate"); + return statClient.sendLike(userId, eventId); + } + } \ No newline at end of file diff --git a/core/request-service/pom.xml b/core/request-service/pom.xml index 40c7b6d..c05f353 100644 --- a/core/request-service/pom.xml +++ b/core/request-service/pom.xml @@ -16,6 +16,11 @@ + + ru.practicum + stats-client + + ru.practicum core-common diff --git a/core/request-service/src/main/java/ru/practicum/request/controller/RequestController.java b/core/request-service/src/main/java/ru/practicum/request/controller/RequestController.java index 85eff65..79aca3c 100644 --- a/core/request-service/src/main/java/ru/practicum/request/controller/RequestController.java +++ b/core/request-service/src/main/java/ru/practicum/request/controller/RequestController.java @@ -59,4 +59,9 @@ public Map getConfirmedRequestsByEventIds(Collection eventIds) return requestService.getConfirmedRequestsByEventIds(eventIds); } + @Override + public String checkParticipation(Long userId, Long eventId) { + return requestService.checkParticipation(userId, eventId); + } + } \ No newline at end of file diff --git a/core/request-service/src/main/java/ru/practicum/request/dal/RequestRepository.java b/core/request-service/src/main/java/ru/practicum/request/dal/RequestRepository.java index 02251a8..6c4fed3 100644 --- a/core/request-service/src/main/java/ru/practicum/request/dal/RequestRepository.java +++ b/core/request-service/src/main/java/ru/practicum/request/dal/RequestRepository.java @@ -13,6 +13,8 @@ public interface RequestRepository extends JpaRepository { boolean existsByRequesterIdAndEventId(Long userId, Long eventId); + boolean existsByRequesterIdAndEventIdAndStatus(Long userId, Long eventId, ParticipationRequestStatus status); + long countByEventIdAndStatus(Long eventId, ParticipationRequestStatus status); List findByRequesterId(Long userId); diff --git a/core/request-service/src/main/java/ru/practicum/request/service/RequestService.java b/core/request-service/src/main/java/ru/practicum/request/service/RequestService.java index 402e236..4e91e08 100644 --- a/core/request-service/src/main/java/ru/practicum/request/service/RequestService.java +++ b/core/request-service/src/main/java/ru/practicum/request/service/RequestService.java @@ -12,6 +12,7 @@ import ru.practicum.dto.request.EventRequestStatusUpdateResultDto; import ru.practicum.dto.request.ParticipationRequestDto; import ru.practicum.dto.request.ParticipationRequestStatus; +import ru.practicum.ewm.client.StatClient; import ru.practicum.exception.ConflictException; import ru.practicum.exception.NotFoundException; import ru.practicum.request.dal.Request; @@ -31,6 +32,8 @@ public class RequestService { private final UserClientHelper userClientHelper; private final EventClientAbstractHelper eventClientHelper; + private final StatClient statClient; + // ЗАЯВКИ ТЕКУЩЕГО ПОЛЬЗОВАТЕЛЯ // Добавление запроса от текущего пользователя на участие в событии @@ -39,7 +42,7 @@ public ParticipationRequestDto addRequest(Long userId, Long eventId) { EventInteractionDto eventDto = eventClientHelper.retrieveEventInteractionDtoByEventIdOrFall(eventId); - return transactionTemplate.execute(status -> { + ParticipationRequestDto result = transactionTemplate.execute(status -> { // нельзя добавить повторный запрос (Ожидается код ошибки 409) if (requestRepository.existsByRequesterIdAndEventId(userId, eventId)) throw new ConflictException("User tries to make duplicate request", "Forbidden action"); @@ -72,6 +75,10 @@ public ParticipationRequestDto addRequest(Long userId, Long eventId) { requestRepository.save(newRequest); return RequestMapper.toDto(newRequest); }); + + statClient.sendRegister(userId, eventId); + + return result; } // Отмена своего запроса на участие в событии @@ -191,4 +198,12 @@ public Map getConfirmedRequestsByEventIds(Collection eventIds) )); } + @Transactional(readOnly = true) + public String checkParticipation(Long userId, Long eventId) { + if (requestRepository.existsByRequesterIdAndEventIdAndStatus(userId, eventId, ParticipationRequestStatus.CONFIRMED)) { + return "true"; + } + throw new NotFoundException("Not found CONFIRMED request for user " + userId + " and event " + eventId); + } + } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0199e0b..934f644 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -157,3 +157,37 @@ services: timeout: 5s interval: 5s retries: 10 + + stats-kafka: + image: confluentinc/cp-kafka:latest + hostname: stats-kafka + container_name: stats-kafka + ports: + - "9092:9092" + environment: + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: broker, controller + KAFKA_LISTENERS: INTERNAL://0.0.0.0:29092, EXTERNAL://0.0.0.0:9092, CONTROLLER://0.0.0.0:29093 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://stats-kafka:29092, EXTERNAL://localhost:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT, EXTERNAL:PLAINTEXT, CONTROLLER:PLAINTEXT + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@stats-kafka:29093' + CLUSTER_ID: 'aaaaabb78bccc32ddd21ee' + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TELEMETRY_REPLICATION_FACTOR: 1 + + kafka-init-topics: + image: confluentinc/cp-kafka:latest + container_name: stats-kafka-init-topics + depends_on: + - stats-kafka + command: bash -c + 'kafka-topics --create --partitions 1 --replication-factor 1 --if-not-exists + --bootstrap-server stats-kafka:29092 --topic stats.user-actions.v1 && + kafka-topics --create --partitions 1 --replication-factor 1 --if-not-exists + --bootstrap-server stats-kafka:29092 --topic stats.events-similarity.v1 ' + init: true diff --git a/infra/config-server/src/main/resources/config/aggregator/application.yaml b/infra/config-server/src/main/resources/config/aggregator/application.yaml new file mode 100644 index 0000000..5b6c012 --- /dev/null +++ b/infra/config-server/src/main/resources/config/aggregator/application.yaml @@ -0,0 +1,61 @@ +info: + app: + name: aggregator + description: Explore With Me + version: 1.0-SNAPSHOT + company: + name: Somekind Software + +my-area-guide: + kafka: + user-action-topic: stats.user-actions.v1 + events-similarity-topic: stats.events-similarity.v1 + aggregator: + minimum-sum-algorithm: optimized + weights: + like: 1.0 + register: 0.8 + view: 0.4 + +server: + shutdown: graceful + +logging: + level: + ru.practicum.service.UserActionService: DEBUG + io.grpc: INFO + +management: + endpoints.web.exposure.include: health,info,metrics + endpoint.health.show-details: always + metrics.export.prometheus.enabled: true + info.env.enabled: true + +spring: + lifecycle: + timeout-per-shutdown-phase: 30s + kafka: + bootstrap-servers: localhost:9092 + producer: + auto-startup: false + key-serializer: org.apache.kafka.common.serialization.VoidSerializer + value-serializer: ru.practicum.serializer.GeneralAvroSerializer + acks: all + retries: 10 + linger-ms: 5 + properties: + enable.idempotence: true + max.in.flight.requests.per.connection: 5 + consumer: + key-deserializer: org.apache.kafka.common.serialization.VoidDeserializer + value-deserializer: ru.practicum.deserializer.UserActionAvroDeserializer + group-id: aggregator-consumer-group-01 + auto-offset-reset: latest + enable-auto-commit: false + max-poll-records: 500 + listener: + auto-startup: false + type: single + ack-mode: RECORD + + diff --git a/infra/config-server/src/main/resources/config/analyzer/application.yaml b/infra/config-server/src/main/resources/config/analyzer/application.yaml new file mode 100644 index 0000000..8b5534e --- /dev/null +++ b/infra/config-server/src/main/resources/config/analyzer/application.yaml @@ -0,0 +1,57 @@ +info: + app: + name: analyzer + description: Explore With Me + version: 1.0-SNAPSHOT + company: + name: Somekind Software + +my-area-guide: + kafka: + user-action-topic: stats.user-actions.v1 + events-similarity-topic: stats.events-similarity.v1 + user-action-consumer-group: analyzer-user-action-group-01 + events-similarity-consumer-group: analyzer-events-similarity-group-01 + bootstrap-servers: localhost:9092 + auto-offset-reset: latest + enable-auto-commit: false + max-poll-records: 500 + analyzer: + weights: + like: 1.0 + register: 0.8 + view: 0.4 + +server: + shutdown: graceful + +logging: + level: + ru.practicum.service.UserActionService: DEBUG + ru.practicum.service.EventSimilarityService: DEBUG + io.grpc: INFO + +management: + endpoints.web.exposure.include: health,info,metrics + endpoint.health.show-details: always + metrics.export.prometheus.enabled: true + info.env.enabled: true + +spring: + lifecycle: + timeout-per-shutdown-phase: 30s + jpa: + show-sql: true + properties.hibernate.format_sql: false + hibernate.ddl-auto: create + sql: + init.mode: always + datasource: + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://localhost:5432/stat_db?currentSchema=analyzer + username: postgres + password: 12345 + +grpc: + server: + port: 0 diff --git a/infra/config-server/src/main/resources/config/collector/application.yaml b/infra/config-server/src/main/resources/config/collector/application.yaml new file mode 100644 index 0000000..e9aa32b --- /dev/null +++ b/infra/config-server/src/main/resources/config/collector/application.yaml @@ -0,0 +1,46 @@ +info: + app: + name: collector + description: Explore With Me + version: 1.0-SNAPSHOT + company: + name: Somekind Software + +my-area-guide: + kafka: + user-action-topic: stats.user-actions.v1 + events-similarity-topic: stats.events-similarity.v1 + +server: + shutdown: graceful + +logging: + level: + ru.practicum.service.UserActionService: DEBUG + io.grpc: INFO + +management: + endpoints.web.exposure.include: health,info,metrics + endpoint.health.show-details: always + metrics.export.prometheus.enabled: true + info.env.enabled: true + +spring: + lifecycle: + timeout-per-shutdown-phase: 30s + kafka: + bootstrap-servers: localhost:9092 + producer: + key-serializer: org.apache.kafka.common.serialization.VoidSerializer + value-serializer: ru.practicum.serializer.GeneralAvroSerializer + acks: all + retries: 10 + linger-ms: 5 + properties: + enable.idempotence: true + max.in.flight.requests.per.connection: 5 + +grpc: + server: + port: 0 + diff --git a/infra/config-server/src/main/resources/config/event-service/application.yaml b/infra/config-server/src/main/resources/config/event-service/application.yaml index df63fa5..d150ee7 100644 --- a/infra/config-server/src/main/resources/config/event-service/application.yaml +++ b/infra/config-server/src/main/resources/config/event-service/application.yaml @@ -19,6 +19,8 @@ management: explore-with-me: stat-server.discovery.name: stats-server + collector.discovery.name: collector + analyzer.discovery.name: analyzer datetime.format: yyyy-MM-dd HH:mm:ss main.datetime.format: yyyy-MM-dd HH:mm:ss stat.datetime.format: yyyy-MM-dd HH:mm:ss diff --git a/infra/config-server/src/main/resources/config/gateway-server/application.yaml b/infra/config-server/src/main/resources/config/gateway-server/application.yaml index 674fb10..7660638 100644 --- a/infra/config-server/src/main/resources/config/gateway-server/application.yaml +++ b/infra/config-server/src/main/resources/config/gateway-server/application.yaml @@ -25,21 +25,21 @@ spring: uri: lb://user-service predicates: - Path= - /admin/users, - /admin/users/*, - /admin/users/*/short, - /admin/users/all/* + /admin/users, + /admin/users/*, + /admin/users/*/short, + /admin/users/all/* - id: comment_service uri: lb://comment-service predicates: - Path= - /comments/**, - /events/*/comments/**, - /users/*/events/*/comments, - /users/*/comments/**, - /admin/comments/**, - /admin/users/*/comments + /comments/**, + /events/*/comments/**, + /users/*/events/*/comments, + /users/*/comments/**, + /admin/comments/**, + /admin/users/*/comments - id: request_service uri: lb://request-service @@ -48,7 +48,8 @@ spring: /requests/**, /users/*/requests, /users/*/requests/*/cancel, - /users/*/events/*/requests + /users/*/events/*/requests, + /users/*/events/*/check/participation - id: event-service uri: lb://event-service @@ -64,4 +65,5 @@ spring: /categories/**, /admin/categories/**, /compilations/**, - /admin/compilations/** + /admin/compilations/**, + /events/*/like diff --git a/pom.xml b/pom.xml index 10191e4..55d116f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,10 +10,10 @@ - Explore With Me + My Area Guide ru.practicum - explore-with-me + my-area-guide 0.0.1-SNAPSHOT pom @@ -27,22 +27,20 @@ UTF-8 21 + 1.12.1 + 1.76.0 + 4.33.1 + 3.1.0.RELEASE 0.0.1-SNAPSHOT 2025.0.0 - 3.14.0 + 4.0.2 - - org.springframework.cloud - spring-cloud-dependencies - ${spring-cloud-dependencies.version} - pom - import - + ru.practicum @@ -59,7 +57,87 @@ ru.practicum core-common - ${version} + ${project.version} + + + + ru.practicum + proto-schemas + ${project.version} + + + + ru.practicum + avro-schemas + ${project.version} + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + pom + import + + + + + + org.apache.avro + avro + ${avro.version} + + + + + + net.devh + grpc-spring-boot-starter + ${grpc-spring-boot-starter.version} + + + + net.devh + grpc-server-spring-boot-starter + ${grpc-spring-boot-starter.version} + + + + net.devh + grpc-client-spring-boot-starter + ${grpc-spring-boot-starter.version} + + + + io.grpc + grpc-core + ${grpc.version} + + + + io.grpc + grpc-stub + ${grpc.version} + + + + io.grpc + grpc-api + ${grpc.version} + + + + io.grpc + grpc-protobuf + ${grpc.version} + + + + io.grpc + grpc-netty-shaded + ${grpc.version} @@ -75,6 +153,18 @@ ${maven-compiler-plugin.version} + + org.apache.avro + avro-maven-plugin + ${avro.version} + + + + io.github.ascopes + protobuf-maven-plugin + ${protobuf-plugin.version} + + diff --git a/stats/aggregator/pom.xml b/stats/aggregator/pom.xml new file mode 100644 index 0000000..0617d57 --- /dev/null +++ b/stats/aggregator/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + aggregator + + + + + + + ru.practicum + avro-schemas + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springframework.retry + spring-retry + + + + + + org.springframework.kafka + spring-kafka + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + + + + 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..8599363 --- /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; + +@SpringBootApplication +@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/kafka/KafkaController.java b/stats/aggregator/src/main/java/ru/practicum/kafka/KafkaController.java new file mode 100644 index 0000000..af19112 --- /dev/null +++ b/stats/aggregator/src/main/java/ru/practicum/kafka/KafkaController.java @@ -0,0 +1,36 @@ +package ru.practicum.kafka; + +import lombok.RequiredArgsConstructor; +import org.apache.avro.specific.SpecificRecordBase; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.config.KafkaListenerEndpointRegistry; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import ru.practicum.ewm.stats.avro.UserActionAvro; +import ru.practicum.properties.CustomProperties; +import ru.practicum.service.UserActionService; + +@Service +@RequiredArgsConstructor +public class KafkaController { + + private final KafkaTemplate kafkaTemplate; + private final KafkaListenerEndpointRegistry kafkaRegistry; + + private final CustomProperties customProperties; + private final UserActionService userActionService; + + @EventListener(ApplicationReadyEvent.class) + public void initKafkaProducer() { + kafkaTemplate.flush(); + kafkaRegistry.start(); + } + + @KafkaListener(topics = "#{customProperties.kafka.userActionTopic}") + public void listen(UserActionAvro userActionAvro) { + userActionService.handleUserAction(userActionAvro); + } + +} diff --git a/stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java b/stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java new file mode 100644 index 0000000..3c265c2 --- /dev/null +++ b/stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java @@ -0,0 +1,50 @@ +package ru.practicum.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import ru.practicum.ewm.stats.avro.UserActionAvro; + +import java.math.BigDecimal; + +@Getter +@Setter +@ConfigurationProperties("explore-with-me") +@Component +public class CustomProperties { + + private final Kafka kafka = new Kafka(); + private final Aggregator aggregator = new Aggregator(); + + @Getter + @Setter + public static class Kafka { + private String userActionTopic = "user-actions"; + private String eventsSimilarityTopic = "events-similarity"; + } + + @Getter + @Setter + public static class Aggregator { + private final Weights weights = new Weights(); + private String minimumSumAlgorithm = "optimized"; + } + + @Getter + @Setter + public static class Weights { + private String like = "0.9"; + private String register = "0.7"; + private String view = "0.3"; + + public BigDecimal ofUserAction(UserActionAvro userActionAvro) { + return switch (userActionAvro.getActionType()) { + case LIKE -> new BigDecimal(like); + case REGISTER -> new BigDecimal(register); + default -> new BigDecimal(view); + }; + } + } + +} diff --git a/stats/aggregator/src/main/java/ru/practicum/service/UserActionService.java b/stats/aggregator/src/main/java/ru/practicum/service/UserActionService.java new file mode 100644 index 0000000..3b92a12 --- /dev/null +++ b/stats/aggregator/src/main/java/ru/practicum/service/UserActionService.java @@ -0,0 +1,147 @@ +package ru.practicum.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.avro.specific.SpecificRecordBase; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import ru.practicum.ewm.stats.avro.EventSimilarityAvro; +import ru.practicum.ewm.stats.avro.UserActionAvro; +import ru.practicum.properties.CustomProperties; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserActionService { + + private final KafkaTemplate kafkaTemplate; + private final CustomProperties customProperties; + + // Map> - таблица весов с быстрым доступом по userId + private final Map> weightsByUser = new HashMap<>(); + // Map> - таблица весов с быстрым доступом по eventId + private final Map> weightsByEvent = new HashMap<>(); + + // Map - таблица сумм векторов для каждого события + private final Map eventSums = new HashMap<>(); + + // Map> - таблица сумм минимумов пар векторов событий + private final Map> minWeightSums = new HashMap<>(); + + public void handleUserAction(UserActionAvro userActionAvro) { + Long userId = userActionAvro.getUserId(); + Long eventId = userActionAvro.getEventId(); + BigDecimal oldWeight = BigDecimal.ZERO; + BigDecimal newWeight = customProperties.getAggregator().getWeights().ofUserAction(userActionAvro); + log.debug("IN: W[{},{}] = {}", userId, eventId, newWeight); + + Map userWeights = weightsByUser.computeIfAbsent(userId, id -> new HashMap<>()); + Map eventWeights = weightsByEvent.computeIfAbsent(eventId, id -> new HashMap<>()); + + if (userWeights.containsKey(eventId)) { + oldWeight = userWeights.get(eventId); + if (newWeight.compareTo(oldWeight) <= 0) { + log.debug("WEIGHT: {} <= {}, ничего не делаем!", newWeight, oldWeight); + return; + } + log.debug("WEIGHT: {} > {}, кладем {}", newWeight, oldWeight, newWeight); + } else { + log.debug("WEIGHT: первое взаимодействие юзера {} и события {}, кладем {}", userId, eventId, newWeight); + } + + // обновляем веса + userWeights.put(eventId, newWeight); + eventWeights.put(userId, newWeight); + // обновляем сумму вектора события + recountEventSum(eventId, oldWeight, newWeight); + // обновляем суммы минимумов векторов событий + if (customProperties.getAggregator().getMinimumSumAlgorithm().toLowerCase().contains("naive")) { + recountEventMinWeightsNaive(userId, eventId); + } else { + recountEventMinWeightsOptimized(userId, eventId, oldWeight, newWeight); + } + // считаем подобие и отправляем в кафку + sendSimilarity(userId, eventId); + } + + // вычисление и отправка подобия на основе имеющихся таблиц + private void sendSimilarity(Long userId, Long eventId) { + for (Long anotherEventId : weightsByUser.get(userId).keySet()) { + if (!Objects.equals(eventId, anotherEventId)) { + long first = Math.min(eventId, anotherEventId); + long second = Math.max(eventId, anotherEventId); + + double numerator = minWeightSums.get(first).get(second).doubleValue(); + double sqrt1 = Math.sqrt(eventSums.get(first).doubleValue()); + double sqrt2 = Math.sqrt(eventSums.get(second).doubleValue()); + double denominator = sqrt1 * sqrt2; + double similarity = numerator / denominator; + + EventSimilarityAvro eventSimilarityAvro = EventSimilarityAvro.newBuilder() + .setEventA(first) + .setEventB(second) + .setScore(similarity) + .setTimestamp(Instant.now()) + .build(); + + kafkaTemplate.send(customProperties.getKafka().getEventsSimilarityTopic(), eventSimilarityAvro); + log.debug("SENT: {}", eventSimilarityAvro); + } + } + } + + // пересчет таблицы сумм векторов события + private void recountEventSum(Long eventId, BigDecimal oldWeight, BigDecimal newWeight) { + BigDecimal delta = newWeight.subtract(oldWeight); + BigDecimal prevSum = eventSums.get(eventId); + eventSums.merge(eventId, delta, BigDecimal::add); + log.debug("SUM: Для события {} пересчитана сумма {} + {} = {}", eventId, prevSum, delta, eventSums.get(eventId)); + } + + // пересчет таблицы сумм минимумов двух векторов событий - версия 1, наивная + private void recountEventMinWeightsNaive(Long userId, Long eventId) { + for (Long secondEventId : weightsByUser.get(userId).keySet()) { + if (!Objects.equals(secondEventId, eventId)) { + Map eventWeights1 = weightsByEvent.get(eventId); + Map eventWeights2 = weightsByEvent.get(secondEventId); + + Set userIds = new HashSet<>(eventWeights1.keySet()); + userIds.retainAll(eventWeights2.keySet()); + + BigDecimal sum = userIds.stream() + .map(id -> eventWeights1.get(id).min(eventWeights2.get(id))) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + Long first = Math.min(eventId, secondEventId); + Long second = Math.max(eventId, secondEventId); + minWeightSums.computeIfAbsent(first, k -> new HashMap<>()).put(second, sum);; + log.debug("MIN1: Для событий {} и {} посчитана сумма минимумов {}", first, second, sum); + } + } + } + + // пересчет таблицы сумм минимумов двух векторов событий - версия 2, оптимизированная + private void recountEventMinWeightsOptimized(Long userId, Long eventId, BigDecimal oldWeight, BigDecimal newWeight) { + for (Map.Entry anotherEventEntry : weightsByUser.get(userId).entrySet()) { + if (!Objects.equals(eventId, anotherEventEntry.getKey())) { + Long first = Math.min(eventId, anotherEventEntry.getKey()); + Long second = Math.max(eventId, anotherEventEntry.getKey()); + + Map firstEventSums = minWeightSums.computeIfAbsent(first, k -> new HashMap<>()); + BigDecimal oldSum = firstEventSums.getOrDefault(second, BigDecimal.ZERO); + + BigDecimal oldMinimum = oldWeight.min(anotherEventEntry.getValue()); + BigDecimal newMinimum = newWeight.min(anotherEventEntry.getValue()); + BigDecimal newSum = oldSum.subtract(oldMinimum).add(newMinimum); + + firstEventSums.put(second, newSum); + log.debug("MIN2: Для событий {} и {} посчитана сумма минимумов {}", first, second, newSum); + } + } + } + +} diff --git a/stats/aggregator/src/main/resources/application.yaml b/stats/aggregator/src/main/resources/application.yaml new file mode 100644 index 0000000..59e9d9d --- /dev/null +++ b/stats/aggregator/src/main/resources/application.yaml @@ -0,0 +1,25 @@ +spring: + application: + name: aggregator + config: + import: "configserver:" + cloud: + config: + discovery: + enabled: true + serviceId: config-server + fail-fast: true + retry: + useRandomPolicy: true + max-interval: 10000 + max-attempts: 100 + +eureka: + client: + registerWithEureka: true + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + instance: + instance-id: ${spring.application.name}${random.int} + preferIpAddress: false + hostname: localhost diff --git a/stats/analyzer/pom.xml b/stats/analyzer/pom.xml new file mode 100644 index 0000000..a7364b0 --- /dev/null +++ b/stats/analyzer/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + analyzer + + + + + + + ru.practicum + proto-schemas + + + + ru.practicum + avro-schemas + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + org.postgresql + postgresql + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springframework.retry + spring-retry + + + + + + net.devh + grpc-server-spring-boot-starter + + + + + + org.springframework.kafka + spring-kafka + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + + + + diff --git a/stats/analyzer/src/main/java/ru/practicum/AnalyzerApplication.java b/stats/analyzer/src/main/java/ru/practicum/AnalyzerApplication.java new file mode 100644 index 0000000..fab04a9 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/AnalyzerApplication.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; + +@SpringBootApplication +@ConfigurationPropertiesScan +public class AnalyzerApplication { + + public static void main(String[] args) { + SpringApplication.run(AnalyzerApplication.class, args); + } + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarity.java b/stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarity.java new file mode 100644 index 0000000..300d596 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarity.java @@ -0,0 +1,43 @@ +package ru.practicum.dal; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.Check; + +import java.time.Instant; + +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table( + name = "similarities", + indexes = {@Index(name = "idx_similarities_event_a_event_b", columnList = "event_a, event_b")}, + uniqueConstraints = {@UniqueConstraint(name = "unique_event_a_event_b", columnNames = {"event_a", "event_b"})} +) +@Check(constraints = "event_a < event_b") +public class EventSimilarity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + @Column(name = "id") + private Long id; + + @Column(name = "event_a", nullable = false) + private Long eventA; + + @Column(name = "event_b", nullable = false) + private Long eventB; + + @Column(name = "score", nullable = false) + private Double score; + + @Column(name = "timestamp", nullable = false) + private Instant timestamp; + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarityRepository.java b/stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarityRepository.java new file mode 100644 index 0000000..8e2acab --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/dal/EventSimilarityRepository.java @@ -0,0 +1,61 @@ +package ru.practicum.dal; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface EventSimilarityRepository extends JpaRepository { + + EventSimilarity findByEventAAndEventB(Long eventA, Long eventB); + + @Query(nativeQuery = true, value = """ + ( + SELECT s.event_b event_id, s.score score + FROM similarities s + WHERE s.event_a IN :eventList + AND NOT EXISTS (SELECT 1 FROM actions a WHERE a.event_id = s.event_b AND a.user_id = :userId) + ) + UNION ALL + ( + SELECT s.event_a event_id, s.score score + FROM similarities s + WHERE s.event_b IN :eventList + AND NOT EXISTS (SELECT 1 FROM actions a WHERE a.event_id = s.event_a AND a.user_id = :userId) + ) + ORDER BY score DESC + LIMIT :limit; + """) + List findSimilarByEventIdListNotSeenByUser( + @Param("userId") Long userId, + @Param("eventList") List eventList, + @Param("limit") Integer limit + ); + + @Query(nativeQuery = true, value = """ + WITH simwt AS ( + ( + SELECT s.event_a src_event, s.event_b event_id, s.score score, a.weight weight + FROM similarities s + JOIN actions a ON a.event_id = s.event_b AND a.user_id = :userId + WHERE s.event_a IN :eventList + ) + UNION ALL + ( + SELECT s.event_b src_event, s.event_a event_id, s.score score, a.weight weight + FROM similarities s + JOIN actions a ON a.event_id = s.event_a AND a.user_id = :userId + WHERE s.event_b IN :eventList + )) + SELECT src_event, SUM(score*weight)/SUM(score) result_score + FROM simwt + GROUP BY src_event + ORDER BY result_score DESC + """) + List findWeightedAverageListByEventIdList( + @Param("userId") Long userId, + @Param("eventList") List eventList + ); + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/dal/UserAction.java b/stats/analyzer/src/main/java/ru/practicum/dal/UserAction.java new file mode 100644 index 0000000..6bf6b72 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/dal/UserAction.java @@ -0,0 +1,41 @@ +package ru.practicum.dal; + +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.Instant; + +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString +@Entity +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table( + name = "actions", + indexes = {@Index(name = "idx_actions_user_id_event_id", columnList = "user_id, event_id")} +) +public class UserAction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + @Column(name = "id") + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "event_id", nullable = false) + private Long eventId; + + @Column(name = "weight", nullable = false) + private BigDecimal weight; + + @Column(name = "timestamp", nullable = false) + private Instant timestamp; + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/dal/UserActionRepository.java b/stats/analyzer/src/main/java/ru/practicum/dal/UserActionRepository.java new file mode 100644 index 0000000..fafb0e3 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/dal/UserActionRepository.java @@ -0,0 +1,41 @@ +package ru.practicum.dal; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface UserActionRepository extends JpaRepository { + + @Query(nativeQuery = true, value = """ + SELECT a.event_id + FROM actions a + WHERE a.user_id = :userId + GROUP BY a.event_id + ORDER BY MAX(a.timestamp) DESC + LIMIT :limit + """) + List findRecentEventIdListByUserId( + @Param("userId") Long userId, + @Param("limit") Integer limit + ); + + @Query(nativeQuery = true, value = """ + WITH max_weights AS + ( + SELECT a.event_id event_id, a.user_id user_id, MAX(a.weight) max_weight + FROM actions a + WHERE a.event_id IN :eventList + GROUP BY a.event_id, a.user_id + ) + SELECT m.event_id event_id, SUM(m.max_weight) weight_sum + FROM max_weights m + GROUP BY m.event_id + """) + List findWeightSumListByEventIdList( + @Param("eventList") List eventList + ); + + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/grpc/GrpcRecommendationsController.java b/stats/analyzer/src/main/java/ru/practicum/grpc/GrpcRecommendationsController.java new file mode 100644 index 0000000..ac36516 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/grpc/GrpcRecommendationsController.java @@ -0,0 +1,62 @@ +package ru.practicum.grpc; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import net.devh.boot.grpc.server.service.GrpcService; +import ru.practicum.grpc.collector.RecommendationsControllerGrpc; +import ru.practicum.grpc.similarity.reports.InteractionsCountRequestProto; +import ru.practicum.grpc.similarity.reports.RecommendedEventProto; +import ru.practicum.grpc.similarity.reports.SimilarEventsRequestProto; +import ru.practicum.grpc.similarity.reports.UserPredictionsRequestProto; +import ru.practicum.service.SimilarityReportService; + +import java.util.List; + +@GrpcService +@RequiredArgsConstructor +public class GrpcRecommendationsController extends RecommendationsControllerGrpc.RecommendationsControllerImplBase { + + private final SimilarityReportService similarityReportService; + + @Override + public void getRecommendationsForUser(UserPredictionsRequestProto request, StreamObserver responseObserver) { + try { + List result = similarityReportService.getRecommendationsForUser(request); + result.forEach(responseObserver::onNext); + responseObserver.onCompleted(); + } catch (Exception e) { + responseObserver.onError( + new StatusRuntimeException(Status.INTERNAL.withDescription(e.getMessage()).withCause(e)) + ); + } + } + + @Override + public void getSimilarEvents(SimilarEventsRequestProto request, StreamObserver responseObserver) { + try { + List result = similarityReportService.getSimilarEvents(request); + result.forEach(responseObserver::onNext); + responseObserver.onCompleted(); + } catch (Exception e) { + responseObserver.onError( + new StatusRuntimeException(Status.INTERNAL.withDescription(e.getMessage()).withCause(e)) + ); + } + } + + @Override + public void getInteractionsCount(InteractionsCountRequestProto request, StreamObserver responseObserver) { + try { + List result = similarityReportService.getInteractionsCount(request); + result.forEach(responseObserver::onNext); + responseObserver.onCompleted(); + } catch (Exception e) { + responseObserver.onError( + new StatusRuntimeException(Status.INTERNAL.withDescription(e.getMessage()).withCause(e)) + ); + } + } + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/kafka/KafkaConsumerConfig.java b/stats/analyzer/src/main/java/ru/practicum/kafka/KafkaConsumerConfig.java new file mode 100644 index 0000000..d091629 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/kafka/KafkaConsumerConfig.java @@ -0,0 +1,73 @@ +package ru.practicum.kafka; + +import lombok.RequiredArgsConstructor; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.VoidDeserializer; +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 org.springframework.kafka.listener.ContainerProperties; +import ru.practicum.deserializer.EventsSimilarityAvroDeserializer; +import ru.practicum.deserializer.UserActionAvroDeserializer; +import ru.practicum.ewm.stats.avro.EventSimilarityAvro; +import ru.practicum.ewm.stats.avro.UserActionAvro; +import ru.practicum.properties.CustomProperties; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@RequiredArgsConstructor +public class KafkaConsumerConfig { + + private final CustomProperties customProperties; + + private Map getNewCommonConsumerProperties() { + Map props = new HashMap<>(); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, VoidDeserializer.class); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, customProperties.getKafka().getBootstrapServers()); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, customProperties.getKafka().getAutoOffsetReset()); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, customProperties.getKafka().getEnableAutoCommit()); + props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, customProperties.getKafka().getMaxPollRecords()); + return props; + } + + @Bean + public ConsumerFactory userActionConsumerFactory() { + Map props = getNewCommonConsumerProperties(); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, UserActionAvroDeserializer.class); + props.put(ConsumerConfig.GROUP_ID_CONFIG, customProperties.getKafka().getUserActionConsumerGroup()); + return new DefaultKafkaConsumerFactory<>(props); + } + + @Bean + public ConsumerFactory eventsSimilarityConsumerFactory() { + Map props = getNewCommonConsumerProperties(); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, EventsSimilarityAvroDeserializer.class); + props.put(ConsumerConfig.GROUP_ID_CONFIG, customProperties.getKafka().getEventsSimilarityConsumerGroup()); + return new DefaultKafkaConsumerFactory<>(props); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory userActionListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(userActionConsumerFactory()); + factory.setAutoStartup(false); + factory.setBatchListener(false); + factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD); + return factory; + } + + @Bean + public ConcurrentKafkaListenerContainerFactory eventsSimilarityListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(eventsSimilarityConsumerFactory()); + factory.setAutoStartup(false); + factory.setBatchListener(false); + factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD); + return factory; + } + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/kafka/KafkaController.java b/stats/analyzer/src/main/java/ru/practicum/kafka/KafkaController.java new file mode 100644 index 0000000..4700b80 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/kafka/KafkaController.java @@ -0,0 +1,46 @@ +package ru.practicum.kafka; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.config.KafkaListenerEndpointRegistry; +import org.springframework.stereotype.Service; +import ru.practicum.ewm.stats.avro.EventSimilarityAvro; +import ru.practicum.ewm.stats.avro.UserActionAvro; +import ru.practicum.properties.CustomProperties; +import ru.practicum.service.EventSimilarityService; +import ru.practicum.service.UserActionService; + +@Service +@RequiredArgsConstructor +public class KafkaController { + + private final KafkaListenerEndpointRegistry kafkaRegistry; + + private final CustomProperties customProperties; + private final UserActionService userActionService; + private final EventSimilarityService eventSimilarityService; + + @EventListener(ApplicationReadyEvent.class) + public void initKafkaProducer() { + kafkaRegistry.start(); + } + + @KafkaListener( + topics = "#{customProperties.kafka.userActionTopic}", + containerFactory = "userActionListenerContainerFactory" + ) + public void listenUserAction(UserActionAvro userActionAvro) { + userActionService.handleUserAction(userActionAvro); + } + + @KafkaListener( + topics = "#{customProperties.kafka.eventsSimilarityTopic}", + containerFactory = "eventsSimilarityListenerContainerFactory" + ) + public void listenEventSimilarity(EventSimilarityAvro eventSimilarityAvro) { + eventSimilarityService.handleEventSimilarity(eventSimilarityAvro); + } + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/mapper/EventSimilarityMapper.java b/stats/analyzer/src/main/java/ru/practicum/mapper/EventSimilarityMapper.java new file mode 100644 index 0000000..899dc48 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/mapper/EventSimilarityMapper.java @@ -0,0 +1,19 @@ +package ru.practicum.mapper; + +import ru.practicum.dal.EventSimilarity; +import ru.practicum.ewm.stats.avro.EventSimilarityAvro; + +public class EventSimilarityMapper { + + public static EventSimilarity fromAvroToNewEntity(EventSimilarityAvro avro) { + Long first = Math.min(avro.getEventA(), avro.getEventB()); + Long second = Math.max(avro.getEventA(), avro.getEventB()); + return EventSimilarity.builder() + .eventA(first) + .eventB(second) + .score(avro.getScore()) + .timestamp(avro.getTimestamp()) + .build(); + } + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java b/stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java new file mode 100644 index 0000000..60c601a --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java @@ -0,0 +1,55 @@ +package ru.practicum.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import ru.practicum.ewm.stats.avro.UserActionAvro; + +import java.math.BigDecimal; + +@Getter +@Setter +@ConfigurationProperties("explore-with-me") +@Component +public class CustomProperties { + + private final Kafka kafka = new Kafka(); + private final Analyzer analyzer = new Analyzer(); + + @Getter + @Setter + public static class Kafka { + private String userActionTopic = "user-actions"; + private String eventsSimilarityTopic = "events-similarity"; + private String userActionConsumerGroup = "analyzer-user-action-group"; + private String eventsSimilarityConsumerGroup = "analyzer-events-similarity-group"; + private String bootstrapServers = "localhost:9092"; + private String autoOffsetReset = "latest"; + private String enableAutoCommit = "false"; + private String maxPollRecords = "500"; + } + + @Getter + @Setter + public static class Analyzer { + private final Weights weights = new Weights(); + } + + @Getter + @Setter + public static class Weights { + private String like = "0.9"; + private String register = "0.7"; + private String view = "0.3"; + + public BigDecimal ofUserAction(UserActionAvro userActionAvro) { + return switch (userActionAvro.getActionType()) { + case LIKE -> new BigDecimal(like); + case REGISTER -> new BigDecimal(register); + default -> new BigDecimal(view); + }; + } + } + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/service/EventSimilarityService.java b/stats/analyzer/src/main/java/ru/practicum/service/EventSimilarityService.java new file mode 100644 index 0000000..8c4050d --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/service/EventSimilarityService.java @@ -0,0 +1,42 @@ +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.dal.EventSimilarity; +import ru.practicum.dal.EventSimilarityRepository; +import ru.practicum.ewm.stats.avro.EventSimilarityAvro; +import ru.practicum.mapper.EventSimilarityMapper; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EventSimilarityService { + + private final EventSimilarityRepository eventSimilarityRepository; + + @Transactional + public void handleEventSimilarity(EventSimilarityAvro eventSimilarityAvro) { + log.debug("IN: {}", eventSimilarityAvro); + String logAction; + Long first = Math.min(eventSimilarityAvro.getEventA(), eventSimilarityAvro.getEventB()); + Long second = Math.max(eventSimilarityAvro.getEventA(), eventSimilarityAvro.getEventB()); + + EventSimilarity eventSimilarity = eventSimilarityRepository.findByEventAAndEventB(first, second); + + if (eventSimilarity == null) { + eventSimilarity = EventSimilarityMapper.fromAvroToNewEntity(eventSimilarityAvro); + logAction = "Created"; + } else { + eventSimilarity.setScore(eventSimilarityAvro.getScore()); + eventSimilarity.setTimestamp(eventSimilarityAvro.getTimestamp()); + logAction = "Updated"; + } + eventSimilarityRepository.save(eventSimilarity); + log.debug("{} similarity for {} and {} : Set score to {}", + logAction, eventSimilarity.getEventA(), eventSimilarity.getEventB(), eventSimilarity.getScore()); + } + + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/service/SimilarityReportService.java b/stats/analyzer/src/main/java/ru/practicum/service/SimilarityReportService.java new file mode 100644 index 0000000..2fdb0e8 --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/service/SimilarityReportService.java @@ -0,0 +1,89 @@ +package ru.practicum.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.dal.EventSimilarityRepository; +import ru.practicum.dal.UserActionRepository; +import ru.practicum.grpc.similarity.reports.InteractionsCountRequestProto; +import ru.practicum.grpc.similarity.reports.RecommendedEventProto; +import ru.practicum.grpc.similarity.reports.SimilarEventsRequestProto; +import ru.practicum.grpc.similarity.reports.UserPredictionsRequestProto; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SimilarityReportService { + + private final UserActionRepository userActionRepository; + private final EventSimilarityRepository eventSimilarityRepository; + + // поток рекомендованных мероприятий для указанного пользователя + public List getRecommendationsForUser(UserPredictionsRequestProto request) { + long userId = request.getUserId(); + int maxResults = request.getMaxResults(); + + // Выгрузить мероприятия, с которыми пользователь уже взаимодействовал, от новых к старым, первые N + List recentUserEventIds = userActionRepository.findRecentEventIdListByUserId(userId, maxResults); + + // Найти мероприятия, похожие на те, что отобрали, но при этом пользователь с ними не взаимодействовал + // сортировать по коэффициенту подобия от большего к меньшему. Выбрать первые N + List similarEventIds = eventSimilarityRepository.findSimilarByEventIdListNotSeenByUser( + userId, + recentUserEventIds, + maxResults + ).stream() + .map(o -> ((Number) o[0]).longValue()) + .toList(); + + // найдем средневзвешенную оценку для каждого полученного ивента + List averageResult = eventSimilarityRepository.findWeightedAverageListByEventIdList(userId, similarEventIds); + return averageResult.stream() + .map(o -> RecommendedEventProto.newBuilder() + .setEventId(((Number) o[0]).longValue()) + .setScore(((Number) o[1]).doubleValue()) + .build()) + .toList(); + } + + // поток мероприятий, похожих на заданное, с которыми пользователь не взаимодействовал + public List getSimilarEvents(SimilarEventsRequestProto request) { + long eventId = request.getEventId(); + long userId = request.getUserId(); + int maxResults = request.getMaxResults(); + + return eventSimilarityRepository.findSimilarByEventIdListNotSeenByUser( + userId, + List.of(eventId), + maxResults + ).stream() + .map(o -> RecommendedEventProto.newBuilder() + .setEventId(((Number) o[0]).longValue()) + .setScore(((Number) o[1]).doubleValue()) + .build()) + .toList(); + } + + // поток мероприятий с суммой максимальных весов действий всех пользователей с этими мероприятиями + public List getInteractionsCount(InteractionsCountRequestProto request) { + List eventIdList = request.getEventIdList(); + Map sumMap = userActionRepository.findWeightSumListByEventIdList(eventIdList) + .stream() + .collect(Collectors.toMap( + o -> ((Number) o[0]).longValue(), + o -> ((Number) o[1]).doubleValue() + )); + + return eventIdList.stream() + .map(id -> RecommendedEventProto.newBuilder() + .setEventId(id) + .setScore(sumMap.computeIfAbsent(id, k -> 0.0)) + .build()) + .toList(); + } + +} diff --git a/stats/analyzer/src/main/java/ru/practicum/service/UserActionService.java b/stats/analyzer/src/main/java/ru/practicum/service/UserActionService.java new file mode 100644 index 0000000..af88bac --- /dev/null +++ b/stats/analyzer/src/main/java/ru/practicum/service/UserActionService.java @@ -0,0 +1,40 @@ +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.dal.UserAction; +import ru.practicum.dal.UserActionRepository; +import ru.practicum.ewm.stats.avro.UserActionAvro; +import ru.practicum.properties.CustomProperties; + +import java.math.BigDecimal; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserActionService { + + private final CustomProperties customProperties; + + private final UserActionRepository userActionRepository; + + @Transactional + public void handleUserAction(UserActionAvro userActionAvro) { + log.debug("IN: {}", userActionAvro); + + BigDecimal weight = customProperties.getAnalyzer().getWeights().ofUserAction(userActionAvro); + + UserAction userAction = UserAction.builder() + .userId(userActionAvro.getUserId()) + .eventId(userActionAvro.getEventId()) + .weight(weight) + .timestamp(userActionAvro.getTimestamp()) + .build(); + + userActionRepository.save(userAction); + log.debug("Created user action: {}", userAction); + } + +} diff --git a/stats/analyzer/src/main/resources/application.yaml b/stats/analyzer/src/main/resources/application.yaml new file mode 100644 index 0000000..6324942 --- /dev/null +++ b/stats/analyzer/src/main/resources/application.yaml @@ -0,0 +1,25 @@ +spring: + application: + name: analyzer + config: + import: "configserver:" + cloud: + config: + discovery: + enabled: true + serviceId: config-server + fail-fast: true + retry: + useRandomPolicy: true + max-interval: 10000 + max-attempts: 100 + +eureka: + client: + registerWithEureka: true + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + instance: + instance-id: ${spring.application.name}${random.int} + preferIpAddress: false + hostname: localhost diff --git a/stats/analyzer/src/main/resources/schema.sql b/stats/analyzer/src/main/resources/schema.sql new file mode 100644 index 0000000..8738877 --- /dev/null +++ b/stats/analyzer/src/main/resources/schema.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS analyzer; diff --git a/stats/avro-schemas/pom.xml b/stats/avro-schemas/pom.xml new file mode 100644 index 0000000..59c9be8 --- /dev/null +++ b/stats/avro-schemas/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + + + + + + org.apache.avro + avro + + + + org.springframework.kafka + spring-kafka + + + + + + + + + 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 + String + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-source + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources + + + + + + + + + + \ No newline at end of file diff --git a/stats/avro-schemas/src/main/avro/EventSimilarityProtocol.avdl b/stats/avro-schemas/src/main/avro/EventSimilarityProtocol.avdl new file mode 100644 index 0000000..66e6453 --- /dev/null +++ b/stats/avro-schemas/src/main/avro/EventSimilarityProtocol.avdl @@ -0,0 +1,12 @@ +@namespace("ru.practicum.ewm.stats.avro") + +protocol EventSimilarityProtocol { + + record EventSimilarityAvro { + long eventA; + long eventB; + double score; + timestamp_ms timestamp; + } + +} diff --git a/stats/avro-schemas/src/main/avro/UserActionProtocol.avdl b/stats/avro-schemas/src/main/avro/UserActionProtocol.avdl new file mode 100644 index 0000000..186b24f --- /dev/null +++ b/stats/avro-schemas/src/main/avro/UserActionProtocol.avdl @@ -0,0 +1,16 @@ +@namespace("ru.practicum.ewm.stats.avro") + +protocol UserActionProtocol { + + record UserActionAvro { + long userId; + long eventId; + ActionTypeAvro actionType; + timestamp_ms timestamp; + } + + enum ActionTypeAvro { + VIEW, REGISTER, LIKE + } + +} diff --git a/stats/avro-schemas/src/main/java/ru/practicum/deserializer/AbstractAvroDeserializer.java b/stats/avro-schemas/src/main/java/ru/practicum/deserializer/AbstractAvroDeserializer.java new file mode 100644 index 0000000..441554d --- /dev/null +++ b/stats/avro-schemas/src/main/java/ru/practicum/deserializer/AbstractAvroDeserializer.java @@ -0,0 +1,34 @@ +package ru.practicum.deserializer; + +import org.apache.avro.Schema; +import org.apache.avro.io.BinaryDecoder; +import org.apache.avro.io.DatumReader; +import org.apache.avro.io.DecoderFactory; +import org.apache.avro.specific.SpecificDatumReader; +import org.apache.avro.specific.SpecificRecordBase; +import org.apache.kafka.common.errors.SerializationException; +import org.apache.kafka.common.serialization.Deserializer; + +public abstract class AbstractAvroDeserializer implements Deserializer { + + private final DecoderFactory decoderFactory = DecoderFactory.get(); + private final Schema schema; + + public AbstractAvroDeserializer(Schema schema) { + this.schema = schema; + } + + @Override + public T deserialize(String topic, byte[] bytes) { + if (bytes == null) return null; + + try { + BinaryDecoder decoder = decoderFactory.binaryDecoder(bytes, null); + DatumReader reader = new SpecificDatumReader<>(schema); + return reader.read(null, decoder); + } catch (Exception e) { + throw new SerializationException("Deserialization Error for topic " + topic, e); + } + } + +} diff --git a/stats/avro-schemas/src/main/java/ru/practicum/deserializer/EventsSimilarityAvroDeserializer.java b/stats/avro-schemas/src/main/java/ru/practicum/deserializer/EventsSimilarityAvroDeserializer.java new file mode 100644 index 0000000..e7b2668 --- /dev/null +++ b/stats/avro-schemas/src/main/java/ru/practicum/deserializer/EventsSimilarityAvroDeserializer.java @@ -0,0 +1,11 @@ +package ru.practicum.deserializer; + +import ru.practicum.ewm.stats.avro.EventSimilarityAvro; + +public class EventsSimilarityAvroDeserializer extends AbstractAvroDeserializer { + + public EventsSimilarityAvroDeserializer() { + super(EventSimilarityAvro.getClassSchema()); + } + +} diff --git a/stats/avro-schemas/src/main/java/ru/practicum/deserializer/UserActionAvroDeserializer.java b/stats/avro-schemas/src/main/java/ru/practicum/deserializer/UserActionAvroDeserializer.java new file mode 100644 index 0000000..6d65bef --- /dev/null +++ b/stats/avro-schemas/src/main/java/ru/practicum/deserializer/UserActionAvroDeserializer.java @@ -0,0 +1,11 @@ +package ru.practicum.deserializer; + +import ru.practicum.ewm.stats.avro.UserActionAvro; + +public class UserActionAvroDeserializer extends AbstractAvroDeserializer { + + public UserActionAvroDeserializer() { + super(UserActionAvro.getClassSchema()); + } + +} diff --git a/stats/avro-schemas/src/main/java/ru/practicum/serializer/GeneralAvroSerializer.java b/stats/avro-schemas/src/main/java/ru/practicum/serializer/GeneralAvroSerializer.java new file mode 100644 index 0000000..4c73120 --- /dev/null +++ b/stats/avro-schemas/src/main/java/ru/practicum/serializer/GeneralAvroSerializer.java @@ -0,0 +1,33 @@ +package ru.practicum.serializer; + +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; + +public class GeneralAvroSerializer implements Serializer { + + private final EncoderFactory encoderFactory = EncoderFactory.get(); + + @Override + public byte[] serialize(String topic, SpecificRecordBase data) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + if (data != null) { + DatumWriter writer = new SpecificDatumWriter<>(data.getSchema()); + BinaryEncoder encoder = encoderFactory.binaryEncoder(out, null); + writer.write(data, encoder); + encoder.flush(); + } + return out.toByteArray(); + } catch (IOException e) { + throw new SerializationException("Serialization Error for topic " + topic, e); + } + } + +} diff --git a/stats/collector/pom.xml b/stats/collector/pom.xml new file mode 100644 index 0000000..ee18225 --- /dev/null +++ b/stats/collector/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + collector + + + + + + + ru.practicum + proto-schemas + + + + ru.practicum + avro-schemas + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + org.springframework.cloud + spring-cloud-starter-config + + + + org.springframework.retry + spring-retry + + + + + + net.devh + grpc-server-spring-boot-starter + + + + + + org.springframework.kafka + spring-kafka + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + + + + 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..c232ec9 --- /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; + +@SpringBootApplication +@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/grpc/GrpcUserActionController.java b/stats/collector/src/main/java/ru/practicum/grpc/GrpcUserActionController.java new file mode 100644 index 0000000..ba7d3c7 --- /dev/null +++ b/stats/collector/src/main/java/ru/practicum/grpc/GrpcUserActionController.java @@ -0,0 +1,32 @@ +package ru.practicum.grpc; + +import com.google.protobuf.Empty; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import net.devh.boot.grpc.server.service.GrpcService; +import ru.practicum.grpc.collector.UserActionControllerGrpc; +import ru.practicum.grpc.user.action.UserActionProto; +import ru.practicum.service.UserActionService; + +@GrpcService +@RequiredArgsConstructor +public class GrpcUserActionController extends UserActionControllerGrpc.UserActionControllerImplBase { + + private final UserActionService userActionService; + + @Override + public void collectUserAction(UserActionProto request, StreamObserver responseObserver) { + try { + userActionService.handleUserAction(request); + responseObserver.onNext(Empty.getDefaultInstance()); + responseObserver.onCompleted(); + } catch (Exception e) { + responseObserver.onError( + new StatusRuntimeException(Status.INTERNAL.withDescription(e.getMessage()).withCause(e)) + ); + } + } + +} diff --git a/stats/collector/src/main/java/ru/practicum/kafka/KafkaProducerInitializer.java b/stats/collector/src/main/java/ru/practicum/kafka/KafkaProducerInitializer.java new file mode 100644 index 0000000..8bcde03 --- /dev/null +++ b/stats/collector/src/main/java/ru/practicum/kafka/KafkaProducerInitializer.java @@ -0,0 +1,21 @@ +package ru.practicum.kafka; + +import lombok.RequiredArgsConstructor; +import org.apache.avro.specific.SpecificRecordBase; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class KafkaProducerInitializer { + + private final KafkaTemplate kafkaTemplate; + + @EventListener(ApplicationReadyEvent.class) + public void initKafkaProducer() { + kafkaTemplate.flush(); + } + +} 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..e426809 --- /dev/null +++ b/stats/collector/src/main/java/ru/practicum/mapper/UserActionMapper.java @@ -0,0 +1,64 @@ +package ru.practicum.mapper; + +import com.google.protobuf.Timestamp; +import ru.practicum.ewm.stats.avro.ActionTypeAvro; +import ru.practicum.ewm.stats.avro.UserActionAvro; +import ru.practicum.grpc.user.action.ActionTypeProto; +import ru.practicum.grpc.user.action.UserActionProto; + +import java.time.Instant; + +public class UserActionMapper { + + // USER ACTION OBJECTS + + public static UserActionProto fromAvroToProto(UserActionAvro avro) { + return UserActionProto.newBuilder() + .setUserId(avro.getUserId()) + .setEventId(avro.getEventId()) + .setActionType(fromAvroToProto(avro.getActionType())) + .setTimestamp(fromAvroToProto(avro.getTimestamp())) + .build(); + } + + public static UserActionAvro fromProtoToAvro(UserActionProto proto) { + return UserActionAvro.newBuilder() + .setUserId(proto.getUserId()) + .setEventId(proto.getEventId()) + .setActionType(fromProtoToAvro(proto.getActionType())) + .setTimestamp(fromProtoToAvro(proto.getTimestamp())) + .build(); + } + + // ENUMS + + public static ActionTypeProto fromAvroToProto(ActionTypeAvro avro) { + return switch (avro) { + case ActionTypeAvro.LIKE -> ActionTypeProto.ACTION_LIKE; + case ActionTypeAvro.REGISTER -> ActionTypeProto.ACTION_REGISTER; + default -> ActionTypeProto.ACTION_VIEW; + }; + } + + public static ActionTypeAvro fromProtoToAvro(ActionTypeProto proto) { + return switch (proto) { + case ActionTypeProto.ACTION_LIKE -> ActionTypeAvro.LIKE; + case ActionTypeProto.ACTION_REGISTER -> ActionTypeAvro.REGISTER; + default -> ActionTypeAvro.VIEW; + }; + } + + // TIMESTAMP + + public static Timestamp fromAvroToProto(Instant instant) { + return Timestamp.newBuilder() + .setSeconds(instant.getEpochSecond()) + .setNanos(instant.getNano()) + .build(); + } + + public static Instant fromProtoToAvro(Timestamp ts) { + return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()); + } + +} diff --git a/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java b/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java new file mode 100644 index 0000000..a6b3dbd --- /dev/null +++ b/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java @@ -0,0 +1,21 @@ +package ru.practicum.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties("my-area-guide") +public class CustomProperties { + + private final Kafka kafka = new Kafka(); + + @Getter + @Setter + public static class Kafka { + private String userActionTopic = "user-actions"; + private String eventsSimilarityTopic = "events-similarity"; + } + +} diff --git a/stats/collector/src/main/java/ru/practicum/service/UserActionService.java b/stats/collector/src/main/java/ru/practicum/service/UserActionService.java new file mode 100644 index 0000000..703f8ef --- /dev/null +++ b/stats/collector/src/main/java/ru/practicum/service/UserActionService.java @@ -0,0 +1,29 @@ +package ru.practicum.service; + +import com.google.protobuf.TextFormat; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.avro.specific.SpecificRecordBase; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import ru.practicum.ewm.stats.avro.UserActionAvro; +import ru.practicum.grpc.user.action.UserActionProto; +import ru.practicum.mapper.UserActionMapper; +import ru.practicum.properties.CustomProperties; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserActionService { + + private final KafkaTemplate kafkaTemplate; + private final CustomProperties customProperties; + + public void handleUserAction(UserActionProto userActionProto) { + log.debug("Received Proto: {}", TextFormat.printer().emittingSingleLine(true).printToString(userActionProto)); + UserActionAvro userActionAvro = UserActionMapper.fromProtoToAvro(userActionProto); + kafkaTemplate.send(customProperties.getKafka().getUserActionTopic(), userActionAvro); + log.debug("Sent Avro: {}", userActionAvro); + } + +} diff --git a/stats/collector/src/main/resources/application.yaml b/stats/collector/src/main/resources/application.yaml new file mode 100644 index 0000000..09ecae0 --- /dev/null +++ b/stats/collector/src/main/resources/application.yaml @@ -0,0 +1,25 @@ +spring: + application: + name: collector + config: + import: "configserver:" + cloud: + config: + discovery: + enabled: true + serviceId: config-server + fail-fast: true + retry: + useRandomPolicy: true + max-interval: 10000 + max-attempts: 100 + +eureka: + client: + registerWithEureka: true + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + instance: + instance-id: ${spring.application.name}${random.int} + preferIpAddress: false + hostname: localhost diff --git a/stats/pom.xml b/stats/pom.xml index 6687c3f..eed482c 100644 --- a/stats/pom.xml +++ b/stats/pom.xml @@ -16,6 +16,11 @@ stats-common stats-server stats-client + collector + aggregator + analyzer + avro-schemas + proto-schemas \ No newline at end of file diff --git a/stats/proto-schemas/pom.xml b/stats/proto-schemas/pom.xml new file mode 100644 index 0000000..6acd7a2 --- /dev/null +++ b/stats/proto-schemas/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + ru.practicum + stats + 0.0.1-SNAPSHOT + + + proto-schemas + + + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + + io.grpc + grpc-stub + + + + io.grpc + grpc-protobuf + + + + + + + + + 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 + + + add-source + generate-sources + + add-source + + + + ${project.build.directory}/generated-sources/protobuf + + + + + + + + + + + \ No newline at end of file diff --git a/stats/proto-schemas/src/main/proto/message/similarity_reports.proto b/stats/proto-schemas/src/main/proto/message/similarity_reports.proto new file mode 100644 index 0000000..47ba354 --- /dev/null +++ b/stats/proto-schemas/src/main/proto/message/similarity_reports.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package grpc.message.similarity.reports; +import "google/protobuf/timestamp.proto"; + +option java_multiple_files = true; +option java_package = "ru.practicum.grpc.similarity.reports"; + +// messages + +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; +} diff --git a/stats/proto-schemas/src/main/proto/message/user_action.proto b/stats/proto-schemas/src/main/proto/message/user_action.proto new file mode 100644 index 0000000..c502010 --- /dev/null +++ b/stats/proto-schemas/src/main/proto/message/user_action.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package grpc.message.user.action; +import "google/protobuf/timestamp.proto"; + +option java_multiple_files = true; +option java_package = "ru.practicum.grpc.user.action"; + +// messages + +message UserActionProto { + int64 user_id = 1; + int64 event_id = 2; + ActionTypeProto action_type = 3; + google.protobuf.Timestamp timestamp = 4; +} + + +enum ActionTypeProto { + ACTION_VIEW = 0; + ACTION_REGISTER = 1; + ACTION_LIKE = 2; +} diff --git a/stats/proto-schemas/src/main/proto/service/collector_controller.proto b/stats/proto-schemas/src/main/proto/service/collector_controller.proto new file mode 100644 index 0000000..a151bc3 --- /dev/null +++ b/stats/proto-schemas/src/main/proto/service/collector_controller.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +package stats.service.collector; + +import "google/protobuf/empty.proto"; + +import "message/user_action.proto"; + +option java_package = "ru.practicum.grpc.collector"; +option java_multiple_files = true; + +service UserActionController { + rpc CollectUserAction (grpc.message.user.action.UserActionProto) returns (google.protobuf.Empty); +} diff --git a/stats/proto-schemas/src/main/proto/service/recommendations_controller.proto b/stats/proto-schemas/src/main/proto/service/recommendations_controller.proto new file mode 100644 index 0000000..6c22188 --- /dev/null +++ b/stats/proto-schemas/src/main/proto/service/recommendations_controller.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; +package stats.service.dashboard; + +import "google/protobuf/empty.proto"; + +import "message/similarity_reports.proto"; + +option java_package = "ru.practicum.grpc.collector"; +option java_multiple_files = true; + +service RecommendationsController { + + rpc GetRecommendationsForUser (grpc.message.similarity.reports.UserPredictionsRequestProto) + returns (stream grpc.message.similarity.reports.RecommendedEventProto); + + rpc GetSimilarEvents (grpc.message.similarity.reports.SimilarEventsRequestProto) + returns (stream grpc.message.similarity.reports.RecommendedEventProto); + + rpc GetInteractionsCount (grpc.message.similarity.reports.InteractionsCountRequestProto) + returns (stream grpc.message.similarity.reports.RecommendedEventProto); + +} diff --git a/stats/stats-client/pom.xml b/stats/stats-client/pom.xml index b278e0d..f49721c 100644 --- a/stats/stats-client/pom.xml +++ b/stats/stats-client/pom.xml @@ -14,17 +14,27 @@ + + + + ru.practicum + proto-schemas + + ru.practicum stats-common + + org.projectlombok lombok - provided + + org.springframework.cloud spring-cloud-commons @@ -35,6 +45,13 @@ spring-retry + + + + net.devh + grpc-client-spring-boot-starter + + diff --git a/stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcConfiguration.java b/stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcConfiguration.java new file mode 100644 index 0000000..8356965 --- /dev/null +++ b/stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcConfiguration.java @@ -0,0 +1,78 @@ +package ru.practicum.ewm.client; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.NameResolverRegistry; +import jakarta.annotation.PostConstruct; +import net.devh.boot.grpc.client.nameresolver.DiscoveryClientResolverFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.practicum.grpc.collector.RecommendationsControllerGrpc; +import ru.practicum.grpc.collector.UserActionControllerGrpc; + +@Configuration +public class GrpcConfiguration { + + private final String collectorDiscoveryName; + private final String analyzerDiscoveryName; + + private final DiscoveryClient discoveryClient; + + public GrpcConfiguration( + @Value("${explore-with-me.collector.discovery.name:collector}") String collectorDiscoveryName, + @Value("${explore-with-me.analyzer.discovery.name:analyzer}") String analyzerDiscoveryName, + DiscoveryClient discoveryClient + ) { + this.collectorDiscoveryName = collectorDiscoveryName; + this.analyzerDiscoveryName = analyzerDiscoveryName; + this.discoveryClient = discoveryClient; + } + + @PostConstruct + public void init() { + DiscoveryClientResolverFactory resolverFactory = new DiscoveryClientResolverFactory(discoveryClient); + NameResolverRegistry.getDefaultRegistry().register(resolverFactory); + } + + // COLLECTOR + + @Bean(destroyMethod = "shutdownNow") + public ManagedChannel collectorChannel() { + return ManagedChannelBuilder.forTarget("discovery:///" + collectorDiscoveryName) + .defaultLoadBalancingPolicy("round_robin") + .usePlaintext() + .enableRetry() + .keepAliveWithoutCalls(true) + .build(); + } + + @Bean + public UserActionControllerGrpc.UserActionControllerBlockingStub userActionControllerBlockingStub( + @Qualifier("collectorChannel") ManagedChannel channel + ) { + return UserActionControllerGrpc.newBlockingStub(channel); + } + + // ANALYZER + + @Bean(destroyMethod = "shutdownNow") + public ManagedChannel analyzerChannel() { + return ManagedChannelBuilder.forTarget("discovery:///" + analyzerDiscoveryName) + .defaultLoadBalancingPolicy("round_robin") + .usePlaintext() + .enableRetry() + .keepAliveWithoutCalls(true) + .build(); + } + + @Bean + public RecommendationsControllerGrpc.RecommendationsControllerBlockingStub recommendationsControllerBlockingStub( + @Qualifier("analyzerChannel") ManagedChannel channel + ) { + return RecommendationsControllerGrpc.newBlockingStub(channel); + } + +} diff --git a/stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcStatClient.java b/stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcStatClient.java new file mode 100644 index 0000000..bf08d43 --- /dev/null +++ b/stats/stats-client/src/main/java/ru/practicum/ewm/client/GrpcStatClient.java @@ -0,0 +1,128 @@ +package ru.practicum.ewm.client; + +import com.google.protobuf.Timestamp; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; +import ru.practicum.EventHitDto; +import ru.practicum.EventStatsResponseDto; +import ru.practicum.grpc.collector.RecommendationsControllerGrpc; +import ru.practicum.grpc.collector.UserActionControllerGrpc; +import ru.practicum.grpc.similarity.reports.InteractionsCountRequestProto; +import ru.practicum.grpc.similarity.reports.RecommendedEventProto; +import ru.practicum.grpc.similarity.reports.UserPredictionsRequestProto; +import ru.practicum.grpc.user.action.ActionTypeProto; +import ru.practicum.grpc.user.action.UserActionProto; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +@Primary +@Slf4j +@Component +@RequiredArgsConstructor +public class GrpcStatClient implements StatClient { + + private final UserActionControllerGrpc.UserActionControllerBlockingStub userActionStub; + private final RecommendationsControllerGrpc.RecommendationsControllerBlockingStub recommendationsStub; + + + @Override + public void hit(EventHitDto eventHitDto) { + throw new UnsupportedOperationException("Method hit() is not supported"); + } + + @Override + public Collection stats(LocalDateTime start, LocalDateTime end, List uris, Boolean unique) { + throw new UnsupportedOperationException("Method stats() is not supported"); + } + + @Override + public String sendView(Long userId, Long eventId) { + return sendAction(userId, eventId, ActionTypeProto.ACTION_VIEW); + } + + @Override + public String sendRegister(Long userId, Long eventId) { + return sendAction(userId, eventId, ActionTypeProto.ACTION_REGISTER); + } + + @Override + public String sendLike(Long userId, Long eventId) { + return sendAction(userId, eventId, ActionTypeProto.ACTION_LIKE); + } + + @Override + public Map getUserRecommendations(Long userId, Integer size) { + UserPredictionsRequestProto requestProto = UserPredictionsRequestProto.newBuilder() + .setUserId(userId) + .setMaxResults(size) + .build(); + try { + Iterator recommendations = recommendationsStub.getRecommendationsForUser(requestProto); + + Map result = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(recommendations, Spliterator.ORDERED), + false + ).collect(Collectors.toMap( + RecommendedEventProto::getEventId, + RecommendedEventProto::getScore + )); + log.debug("Received {} recommendations for user {}", result.size(), userId); + return result; + } catch (Exception e) { + log.warn("Failed getting User Recommendations by GRPC: {}", e.getMessage()); + return Map.of(); + } + } + + @Override + public Map getRatingsByEventIdList(List eventIdList) { + InteractionsCountRequestProto requestProto = InteractionsCountRequestProto.newBuilder() + .addAllEventId(eventIdList) + .build(); + try { + Iterator ratingIterator = recommendationsStub.getInteractionsCount(requestProto); + + Map result = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(ratingIterator, Spliterator.ORDERED), + false + ).collect(Collectors.toMap( + RecommendedEventProto::getEventId, + RecommendedEventProto::getScore + )); + log.debug("Received {} ratings for {} events", result.size(), eventIdList.size()); + return result; + } catch (Exception e) { + log.warn("Failed getting Event Ratings by GRPC: {}", e.getMessage()); + return Map.of(); + } + } + + private String sendAction(Long userId, Long eventId, ActionTypeProto action) { + Instant instant = Instant.now(); + Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(instant.getEpochSecond()) + .setNanos(instant.getNano()) + .build(); + UserActionProto userActionProto = UserActionProto.newBuilder() + .setUserId(userId) + .setEventId(eventId) + .setActionType(action) + .setTimestamp(timestamp) + .build(); + try { + userActionStub.collectUserAction(userActionProto); + log.debug("Sent Event View action: {}", userActionProto); + return "true"; + } catch (Exception e) { + log.warn("Failed sending Event View action by GRPC: {}", e.getMessage()); + return "false"; + } + } + +} diff --git a/stats/stats-client/src/main/java/ru/practicum/ewm/client/RestStatClient.java b/stats/stats-client/src/main/java/ru/practicum/ewm/client/RestStatClient.java index 7634cd7..7607d64 100644 --- a/stats/stats-client/src/main/java/ru/practicum/ewm/client/RestStatClient.java +++ b/stats/stats-client/src/main/java/ru/practicum/ewm/client/RestStatClient.java @@ -21,10 +21,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Random; +import java.util.*; @Slf4j @@ -139,4 +136,29 @@ public Collection stats(LocalDateTime start, LocalDateTim } } + @Override + public String sendView(Long userId, Long eventId) { + throw new UnsupportedOperationException("Method sendView() is not supported"); + } + + @Override + public String sendRegister(Long userId, Long eventId) { + throw new UnsupportedOperationException("Method sendRegister() is not supported"); + } + + @Override + public String sendLike(Long userId, Long eventId) { + throw new UnsupportedOperationException("Method sendLike() is not supported"); + } + + @Override + public Map getUserRecommendations(Long userId, Integer size) { + throw new UnsupportedOperationException("Method getUserRecommendations() is not supported"); + } + + @Override + public Map getRatingsByEventIdList(List eventIdList) { + throw new UnsupportedOperationException("Method getRatingsByEventIdList() is not supported"); + } + } diff --git a/stats/stats-client/src/main/java/ru/practicum/ewm/client/StatClient.java b/stats/stats-client/src/main/java/ru/practicum/ewm/client/StatClient.java index 31d0071..8dc367b 100644 --- a/stats/stats-client/src/main/java/ru/practicum/ewm/client/StatClient.java +++ b/stats/stats-client/src/main/java/ru/practicum/ewm/client/StatClient.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.Collection; import java.util.List; +import java.util.Map; public interface StatClient { @@ -18,4 +19,14 @@ Collection stats( Boolean unique ); + String sendView(Long userId, Long eventId); + + String sendRegister(Long userId, Long eventId); + + String sendLike(Long userId, Long eventId); + + Map getUserRecommendations(Long userId, Integer size); + + Map getRatingsByEventIdList(List eventIdList); + } From 7a7f65b9193f475d433493ed739b7c618a577365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 10:39:24 +0400 Subject: [PATCH 2/9] pom fix --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 55d116f..702daa2 100644 --- a/pom.xml +++ b/pom.xml @@ -10,10 +10,10 @@ - My Area Guide + Explore With Me ru.practicum - my-area-guide + explore-with-me 0.0.1-SNAPSHOT pom From 5110dd522831408c6728cf2904306cce83f5a35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 10:48:07 +0400 Subject: [PATCH 3/9] pom fix --- stats/analyzer/pom.xml | 2 +- stats/avro-schemas/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stats/analyzer/pom.xml b/stats/analyzer/pom.xml index a7364b0..3f45efd 100644 --- a/stats/analyzer/pom.xml +++ b/stats/analyzer/pom.xml @@ -121,4 +121,4 @@ - + \ No newline at end of file diff --git a/stats/avro-schemas/pom.xml b/stats/avro-schemas/pom.xml index 59c9be8..84f57d0 100644 --- a/stats/avro-schemas/pom.xml +++ b/stats/avro-schemas/pom.xml @@ -10,7 +10,7 @@ 0.0.1-SNAPSHOT - + avro-schemas From 50a87e1f40f9d327a25a60078d4975d247096306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 11:11:25 +0400 Subject: [PATCH 4/9] update gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 64703cb..977bbab 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,13 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ +tester-0.0.1.jar +/tester.bat +tester.txt + ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr +/execution-report.txt \ No newline at end of file From 5d35d2155fcefe0691f72c40a3a3bbbdeb221c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 12:11:12 +0400 Subject: [PATCH 5/9] try test fix --- core/comment-service/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/comment-service/pom.xml b/core/comment-service/pom.xml index b42ffae..22b7c9d 100644 --- a/core/comment-service/pom.xml +++ b/core/comment-service/pom.xml @@ -12,6 +12,7 @@ comment-service + From 394a52a184664eeb9ec29a09b048e93e1fc3341f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 13:15:30 +0400 Subject: [PATCH 6/9] try test fix --- core/comment-service/pom.xml | 1 - .../practicum/CommentServiceApplication.java | 2 +- .../java/ru/practicum/client/UserClient.java | 2 +- .../controller/CommentAdminController.java | 2 +- .../comment/service/CommentAdminService.java | 2 +- .../service/CommentAdminServiceImpl.java | 8 ++- .../src/main/resources/application.yaml | 2 +- .../src/main/resources/schema.sql | 2 +- core/core-common/pom.xml | 53 ------------------- .../api/comment/CommentAdminApi.java | 2 +- .../api/compilation/CompilationPublicApi.java | 2 +- .../ru/practicum/api/event/EventAdminApi.java | 2 +- .../ru/practicum/api/event/EventAllApi.java | 2 +- .../practicum/api/event/EventPrivateApi.java | 2 +- .../client/EventClientAbstractHelper.java | 2 +- .../practicum/dto/event/EventAdminParams.java | 2 +- .../practicum/dto/event/EventCommentDto.java | 2 +- .../ru/practicum/dto/event/EventFullDto.java | 2 +- .../dto/event/EventInteractionDto.java | 2 +- .../ru/practicum/dto/event/EventParams.java | 2 +- .../EventRequestStatusUpdateRequest.java | 2 +- .../event/EventRequestStatusUpdateResult.java | 2 +- .../ru/practicum/dto/event/EventShortDto.java | 2 +- .../ru/practicum/dto/event/EventSort.java | 2 +- .../ru/practicum/dto/event/LocationDto.java | 2 +- .../ru/practicum/dto/event/NewEventDto.java | 2 +- .../java/ru/practicum/dto/event/State.java | 2 +- .../ru/practicum/dto/event/StateAction.java | 2 +- .../practicum/dto/event/UpdateEventDto.java | 2 +- .../EventRequestStatusUpdateRequestDto.java | 2 +- .../EventRequestStatusUpdateResultDto.java | 2 +- .../dto/request/ParticipationRequestDto.java | 2 +- .../request/ParticipationRequestStatus.java | 2 +- .../practicum/dto/user/NewUserRequestDto.java | 2 +- .../java/ru/practicum/dto/user/UserDto.java | 2 +- docker-compose.yml | 2 +- .../properties/CustomProperties.java | 2 +- 37 files changed, 37 insertions(+), 93 deletions(-) diff --git a/core/comment-service/pom.xml b/core/comment-service/pom.xml index 22b7c9d..b42ffae 100644 --- a/core/comment-service/pom.xml +++ b/core/comment-service/pom.xml @@ -12,7 +12,6 @@ comment-service - diff --git a/core/comment-service/src/main/java/ru/practicum/CommentServiceApplication.java b/core/comment-service/src/main/java/ru/practicum/CommentServiceApplication.java index 5b05018..a468853 100644 --- a/core/comment-service/src/main/java/ru/practicum/CommentServiceApplication.java +++ b/core/comment-service/src/main/java/ru/practicum/CommentServiceApplication.java @@ -12,4 +12,4 @@ public static void main(String[] args) { SpringApplication.run(CommentServiceApplication.class, args); } -} +} \ No newline at end of file diff --git a/core/comment-service/src/main/java/ru/practicum/client/UserClient.java b/core/comment-service/src/main/java/ru/practicum/client/UserClient.java index 50ca2b2..35907ef 100644 --- a/core/comment-service/src/main/java/ru/practicum/client/UserClient.java +++ b/core/comment-service/src/main/java/ru/practicum/client/UserClient.java @@ -5,4 +5,4 @@ @FeignClient(name = "user-service") public interface UserClient extends UserApi { -} +} \ No newline at end of file diff --git a/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java b/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java index f39aa50..c6f0aa7 100644 --- a/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java +++ b/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java @@ -27,7 +27,7 @@ public Collection get(Long userId, int from, int size) { } @Override - public boolean delete(Long comId) { + public String delete(Long comId) { return commentAdminService.delete(comId); } diff --git a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java index 2744230..bfa01e5 100644 --- a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java +++ b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java @@ -6,7 +6,7 @@ public interface CommentAdminService { - boolean delete(Long comId); + String delete(Long comId); List search(String text, int from, int size); diff --git a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java index b6a2ba3..0b7e42f 100644 --- a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java +++ b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java @@ -34,12 +34,10 @@ public class CommentAdminServiceImpl implements CommentAdminService { @Override @Transactional - public boolean delete(Long comId) { - if (!commentRepository.existsById(comId)) { - throw new NotFoundException("Not found Comment " + comId); - } + public String delete(Long comId) { + if (!commentRepository.existsById(comId)) throw new NotFoundException("Not found Comment " + comId); commentRepository.deleteById(comId); - return true; + return "deleted comment " + comId; } @Override diff --git a/core/comment-service/src/main/resources/application.yaml b/core/comment-service/src/main/resources/application.yaml index 931c1b1..e6db48a 100644 --- a/core/comment-service/src/main/resources/application.yaml +++ b/core/comment-service/src/main/resources/application.yaml @@ -22,4 +22,4 @@ eureka: instance: instance-id: ${spring.application.name}${random.int} preferIpAddress: false - hostname: localhost + hostname: localhost \ No newline at end of file diff --git a/core/comment-service/src/main/resources/schema.sql b/core/comment-service/src/main/resources/schema.sql index 0e28872..84b18e6 100644 --- a/core/comment-service/src/main/resources/schema.sql +++ b/core/comment-service/src/main/resources/schema.sql @@ -1 +1 @@ -CREATE SCHEMA IF NOT EXISTS comment_service; +CREATE SCHEMA IF NOT EXISTS comment_service; \ No newline at end of file diff --git a/core/core-common/pom.xml b/core/core-common/pom.xml index 09963fa..10d86be 100644 --- a/core/core-common/pom.xml +++ b/core/core-common/pom.xml @@ -14,13 +14,6 @@ - - - - - - - @@ -34,11 +27,6 @@ lombok - - - - - @@ -57,24 +45,6 @@ 2.0.1.Final - - - - - - - - - - - - - - - - - - @@ -87,29 +57,6 @@ spring-cloud-starter-circuitbreaker-resilience4j - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java b/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java index 9738b88..35eb626 100644 --- a/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java @@ -28,7 +28,7 @@ Collection get( @DeleteMapping("/admin/comments/{comId}") @ResponseStatus(HttpStatus.NO_CONTENT) - boolean delete( + String delete( @PathVariable @Positive Long comId ); diff --git a/core/core-common/src/main/java/ru/practicum/api/compilation/CompilationPublicApi.java b/core/core-common/src/main/java/ru/practicum/api/compilation/CompilationPublicApi.java index 071559d..a707e38 100644 --- a/core/core-common/src/main/java/ru/practicum/api/compilation/CompilationPublicApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/compilation/CompilationPublicApi.java @@ -27,4 +27,4 @@ CompilationDto getCompilationById( @PathVariable Long compId ); -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/api/event/EventAdminApi.java b/core/core-common/src/main/java/ru/practicum/api/event/EventAdminApi.java index 24022a1..a5df78e 100644 --- a/core/core-common/src/main/java/ru/practicum/api/event/EventAdminApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/event/EventAdminApi.java @@ -37,4 +37,4 @@ EventFullDto updateEventByAdmin( @RequestBody @Valid UpdateEventDto updateEventDto ); -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/api/event/EventAllApi.java b/core/core-common/src/main/java/ru/practicum/api/event/EventAllApi.java index dc37691..fc1c48b 100644 --- a/core/core-common/src/main/java/ru/practicum/api/event/EventAllApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/event/EventAllApi.java @@ -1,4 +1,4 @@ package ru.practicum.api.event; public interface EventAllApi extends EventPublicApi, EventPrivateApi, EventAdminApi { -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/api/event/EventPrivateApi.java b/core/core-common/src/main/java/ru/practicum/api/event/EventPrivateApi.java index 8302fef..926d11a 100644 --- a/core/core-common/src/main/java/ru/practicum/api/event/EventPrivateApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/event/EventPrivateApi.java @@ -47,4 +47,4 @@ EventFullDto updateEventByUserIdAndEventId( @Valid @RequestBody UpdateEventDto updateEventDto ); -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/client/EventClientAbstractHelper.java b/core/core-common/src/main/java/ru/practicum/client/EventClientAbstractHelper.java index ba6620d..a5e9196 100644 --- a/core/core-common/src/main/java/ru/practicum/client/EventClientAbstractHelper.java +++ b/core/core-common/src/main/java/ru/practicum/client/EventClientAbstractHelper.java @@ -86,4 +86,4 @@ private boolean isNotFoundCode(RuntimeException e) { return false; } -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventAdminParams.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventAdminParams.java index 35dab99..a98c969 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventAdminParams.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventAdminParams.java @@ -28,4 +28,4 @@ public class EventAdminParams { private Integer size; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventCommentDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventCommentDto.java index 8a4ada5..a1126fc 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventCommentDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventCommentDto.java @@ -23,4 +23,4 @@ public static EventCommentDto makeDummy(Long id) { return dto; } -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventFullDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventFullDto.java index 81771d9..e7ac990 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventFullDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventFullDto.java @@ -47,4 +47,4 @@ public class EventFullDto { private List comments; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventInteractionDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventInteractionDto.java index 706ba0d..3254fb5 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventInteractionDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventInteractionDto.java @@ -51,4 +51,4 @@ public static EventInteractionDto makeDummy(Long id) { return dto; } -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventParams.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventParams.java index 73e141f..35cb098 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventParams.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventParams.java @@ -32,4 +32,4 @@ public class EventParams { private Integer size; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateRequest.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateRequest.java index 859a768..6cedfb0 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateRequest.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateRequest.java @@ -11,4 +11,4 @@ public class EventRequestStatusUpdateRequest { private State state; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateResult.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateResult.java index 3be6bc9..f47028b 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateResult.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventRequestStatusUpdateResult.java @@ -12,4 +12,4 @@ public class EventRequestStatusUpdateResult { private List rejectedRequests; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java index b0e56c7..250a8dc 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventShortDto.java @@ -32,4 +32,4 @@ public class EventShortDto { private Long confirmedRequests; private Double rating; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java b/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java index 0a14767..830c6b0 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/EventSort.java @@ -2,4 +2,4 @@ public enum EventSort { EVENT_DATE, VIEWS, RATING -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/LocationDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/LocationDto.java index 3b6c1b9..864323a 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/LocationDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/LocationDto.java @@ -14,4 +14,4 @@ public class LocationDto { private Float lat; private Float lon; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/NewEventDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/NewEventDto.java index 7e43c7e..043ba29 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/NewEventDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/NewEventDto.java @@ -39,4 +39,4 @@ public class NewEventDto { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime eventDate; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/State.java b/core/core-common/src/main/java/ru/practicum/dto/event/State.java index 976062e..c664a68 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/State.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/State.java @@ -2,4 +2,4 @@ public enum State { PENDING, PUBLISHED, CANCELED -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/StateAction.java b/core/core-common/src/main/java/ru/practicum/dto/event/StateAction.java index c35bda8..8c691a9 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/StateAction.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/StateAction.java @@ -5,4 +5,4 @@ public enum StateAction { CANCEL_REVIEW, PUBLISH_EVENT, REJECT_EVENT -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/event/UpdateEventDto.java b/core/core-common/src/main/java/ru/practicum/dto/event/UpdateEventDto.java index 1725e05..47215b5 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/event/UpdateEventDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/event/UpdateEventDto.java @@ -48,4 +48,4 @@ public class UpdateEventDto { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime eventDate; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateRequestDto.java b/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateRequestDto.java index 4af1b9b..2a3d7f0 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateRequestDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateRequestDto.java @@ -21,4 +21,4 @@ public class EventRequestStatusUpdateRequestDto { @NotNull(message = "Field 'status' shouldn't be null") private ParticipationRequestStatus status; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateResultDto.java b/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateResultDto.java index b678c65..81fd5a0 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateResultDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/request/EventRequestStatusUpdateResultDto.java @@ -20,4 +20,4 @@ public class EventRequestStatusUpdateResultDto { @Builder.Default private List rejectedRequests = new ArrayList<>(); -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestDto.java b/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestDto.java index 25b8afb..b46c319 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestDto.java @@ -25,4 +25,4 @@ public class ParticipationRequestDto { @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS") private LocalDateTime created; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestStatus.java b/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestStatus.java index e2f335e..88b0d23 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestStatus.java +++ b/core/core-common/src/main/java/ru/practicum/dto/request/ParticipationRequestStatus.java @@ -4,4 +4,4 @@ public enum ParticipationRequestStatus { PENDING, CONFIRMED, CANCELED, REJECTED -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/user/NewUserRequestDto.java b/core/core-common/src/main/java/ru/practicum/dto/user/NewUserRequestDto.java index c2fecdf..5ea5305 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/user/NewUserRequestDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/user/NewUserRequestDto.java @@ -23,4 +23,4 @@ public class NewUserRequestDto { @Size(min = 2, max = 250, message = "Field 'name' should be from 2 to 250 characters") private String name; -} +} \ No newline at end of file diff --git a/core/core-common/src/main/java/ru/practicum/dto/user/UserDto.java b/core/core-common/src/main/java/ru/practicum/dto/user/UserDto.java index 88b1dd5..bbee84c 100644 --- a/core/core-common/src/main/java/ru/practicum/dto/user/UserDto.java +++ b/core/core-common/src/main/java/ru/practicum/dto/user/UserDto.java @@ -23,4 +23,4 @@ public static UserDto makeDummy(Long id) { return dto; } -} +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 934f644..4ea052b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -173,7 +173,7 @@ services: KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT, EXTERNAL:PLAINTEXT, CONTROLLER:PLAINTEXT KAFKA_CONTROLLER_QUORUM_VOTERS: '1@stats-kafka:29093' - CLUSTER_ID: 'aaaaabb78bccc32ddd21ee' + CLUSTER_ID: 'test-cluster-9e8f7d6c-5b4a-3z2y' KAFKA_DEFAULT_REPLICATION_FACTOR: 1 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 diff --git a/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java b/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java index a6b3dbd..7eda395 100644 --- a/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java +++ b/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java @@ -6,7 +6,7 @@ @Getter @Setter -@ConfigurationProperties("my-area-guide") +@ConfigurationProperties("explore-with-me") public class CustomProperties { private final Kafka kafka = new Kafka(); From efec139432c10d74b13ca206deefb7136c279978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 13:46:15 +0400 Subject: [PATCH 7/9] try test fix --- docker-compose.yml | 4 ++-- infra/config-server/src/main/resources/application.yaml | 4 ++-- .../src/main/resources/config/aggregator/application.yaml | 6 ++---- .../src/main/resources/config/analyzer/application.yaml | 4 ++-- .../src/main/resources/config/collector/application.yaml | 5 ++--- .../main/resources/config/comment-service/application.yaml | 4 ++-- .../main/resources/config/event-service/application.yaml | 4 ++-- .../main/resources/config/gateway-server/application.yaml | 2 +- .../main/resources/config/request-service/application.yaml | 4 ++-- .../src/main/resources/config/stats-server/application.yaml | 6 +++--- .../src/main/resources/config/user-service/application.yaml | 4 ++-- .../main/java/ru/practicum/properties/CustomProperties.java | 4 ++-- stats/aggregator/src/main/resources/application.yaml | 2 +- .../main/java/ru/practicum/properties/CustomProperties.java | 4 ++-- .../main/java/ru/practicum/properties/CustomProperties.java | 4 ++-- stats/stats-server/src/main/resources/application.yaml | 4 ++-- 16 files changed, 31 insertions(+), 34 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4ea052b..2cfb6d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -173,7 +173,7 @@ services: KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT, EXTERNAL:PLAINTEXT, CONTROLLER:PLAINTEXT KAFKA_CONTROLLER_QUORUM_VOTERS: '1@stats-kafka:29093' - CLUSTER_ID: 'test-cluster-9e8f7d6c-5b4a-3z2y' + CLUSTER_ID: 'aaaaabb78bccc32ddd21ee' KAFKA_DEFAULT_REPLICATION_FACTOR: 1 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 @@ -190,4 +190,4 @@ services: --bootstrap-server stats-kafka:29092 --topic stats.user-actions.v1 && kafka-topics --create --partitions 1 --replication-factor 1 --if-not-exists --bootstrap-server stats-kafka:29092 --topic stats.events-similarity.v1 ' - init: true + init: true \ No newline at end of file diff --git a/infra/config-server/src/main/resources/application.yaml b/infra/config-server/src/main/resources/application.yaml index 8eae556..45f8b95 100644 --- a/infra/config-server/src/main/resources/application.yaml +++ b/infra/config-server/src/main/resources/application.yaml @@ -1,7 +1,7 @@ info: app: name: config-server - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -40,4 +40,4 @@ eureka: instance: instance-id: ${spring.application.name}${random.int} preferIpAddress: false - hostname: localhost + hostname: localhost \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/aggregator/application.yaml b/infra/config-server/src/main/resources/config/aggregator/application.yaml index 5b6c012..ba1af07 100644 --- a/infra/config-server/src/main/resources/config/aggregator/application.yaml +++ b/infra/config-server/src/main/resources/config/aggregator/application.yaml @@ -1,7 +1,7 @@ info: app: name: aggregator - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -56,6 +56,4 @@ spring: listener: auto-startup: false type: single - ack-mode: RECORD - - + ack-mode: RECORD \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/analyzer/application.yaml b/infra/config-server/src/main/resources/config/analyzer/application.yaml index 8b5534e..627c47b 100644 --- a/infra/config-server/src/main/resources/config/analyzer/application.yaml +++ b/infra/config-server/src/main/resources/config/analyzer/application.yaml @@ -1,7 +1,7 @@ info: app: name: analyzer - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -54,4 +54,4 @@ spring: grpc: server: - port: 0 + port: 0 \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/collector/application.yaml b/infra/config-server/src/main/resources/config/collector/application.yaml index e9aa32b..76dd8a7 100644 --- a/infra/config-server/src/main/resources/config/collector/application.yaml +++ b/infra/config-server/src/main/resources/config/collector/application.yaml @@ -1,7 +1,7 @@ info: app: name: collector - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -42,5 +42,4 @@ spring: grpc: server: - port: 0 - + port: 0 \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/comment-service/application.yaml b/infra/config-server/src/main/resources/config/comment-service/application.yaml index f79d92a..fd2da57 100644 --- a/infra/config-server/src/main/resources/config/comment-service/application.yaml +++ b/infra/config-server/src/main/resources/config/comment-service/application.yaml @@ -1,7 +1,7 @@ info: app: name: comment-service - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -55,4 +55,4 @@ resilience4j.circuitbreaker: minimumNumberOfCalls: 5 automaticTransitionFromOpenToHalfOpenEnabled: true ignoreExceptions: - - feign.FeignException.FeignClientException + - feign.FeignException.FeignClientException \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/event-service/application.yaml b/infra/config-server/src/main/resources/config/event-service/application.yaml index d150ee7..ad2e2a0 100644 --- a/infra/config-server/src/main/resources/config/event-service/application.yaml +++ b/infra/config-server/src/main/resources/config/event-service/application.yaml @@ -1,7 +1,7 @@ info: app: name: event-service - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -57,4 +57,4 @@ resilience4j.circuitbreaker: minimumNumberOfCalls: 5 automaticTransitionFromOpenToHalfOpenEnabled: true ignoreExceptions: - - feign.FeignException.FeignClientException + - feign.FeignException.FeignClientException \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/gateway-server/application.yaml b/infra/config-server/src/main/resources/config/gateway-server/application.yaml index 7660638..eec085a 100644 --- a/infra/config-server/src/main/resources/config/gateway-server/application.yaml +++ b/infra/config-server/src/main/resources/config/gateway-server/application.yaml @@ -66,4 +66,4 @@ spring: /admin/categories/**, /compilations/**, /admin/compilations/**, - /events/*/like + /events/*/like \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/request-service/application.yaml b/infra/config-server/src/main/resources/config/request-service/application.yaml index ea7616a..aa949f4 100644 --- a/infra/config-server/src/main/resources/config/request-service/application.yaml +++ b/infra/config-server/src/main/resources/config/request-service/application.yaml @@ -1,7 +1,7 @@ info: app: name: request-service - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -55,4 +55,4 @@ resilience4j.circuitbreaker: minimumNumberOfCalls: 5 automaticTransitionFromOpenToHalfOpenEnabled: true ignoreExceptions: - - feign.FeignException.FeignClientException + - feign.FeignException.FeignClientException \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/stats-server/application.yaml b/infra/config-server/src/main/resources/config/stats-server/application.yaml index cad3bfb..9eee093 100644 --- a/infra/config-server/src/main/resources/config/stats-server/application.yaml +++ b/infra/config-server/src/main/resources/config/stats-server/application.yaml @@ -1,7 +1,7 @@ info: app: - name: stats-service - description: Explore With Me + name: stats-server + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -37,4 +37,4 @@ spring: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/stat_db username: postgres - password: 12345 + password: 12345 \ No newline at end of file diff --git a/infra/config-server/src/main/resources/config/user-service/application.yaml b/infra/config-server/src/main/resources/config/user-service/application.yaml index 8197886..81a0772 100644 --- a/infra/config-server/src/main/resources/config/user-service/application.yaml +++ b/infra/config-server/src/main/resources/config/user-service/application.yaml @@ -1,7 +1,7 @@ info: app: name: main-service - description: Explore With Me + description: My Area Guide version: 1.0-SNAPSHOT company: name: Somekind Software @@ -40,4 +40,4 @@ spring: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5433/main_db?currentSchema=user_service username: postgres - password: 12345 + password: 12345 \ No newline at end of file diff --git a/stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java b/stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java index 3c265c2..3129526 100644 --- a/stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java +++ b/stats/aggregator/src/main/java/ru/practicum/properties/CustomProperties.java @@ -10,7 +10,7 @@ @Getter @Setter -@ConfigurationProperties("explore-with-me") +@ConfigurationProperties("my-area-guide") @Component public class CustomProperties { @@ -47,4 +47,4 @@ public BigDecimal ofUserAction(UserActionAvro userActionAvro) { } } -} +} \ No newline at end of file diff --git a/stats/aggregator/src/main/resources/application.yaml b/stats/aggregator/src/main/resources/application.yaml index 59e9d9d..f057e85 100644 --- a/stats/aggregator/src/main/resources/application.yaml +++ b/stats/aggregator/src/main/resources/application.yaml @@ -22,4 +22,4 @@ eureka: instance: instance-id: ${spring.application.name}${random.int} preferIpAddress: false - hostname: localhost + hostname: localhost \ No newline at end of file diff --git a/stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java b/stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java index 60c601a..30caf3f 100644 --- a/stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java +++ b/stats/analyzer/src/main/java/ru/practicum/properties/CustomProperties.java @@ -10,7 +10,7 @@ @Getter @Setter -@ConfigurationProperties("explore-with-me") +@ConfigurationProperties("my-area-guide") @Component public class CustomProperties { @@ -52,4 +52,4 @@ public BigDecimal ofUserAction(UserActionAvro userActionAvro) { } } -} +} \ No newline at end of file diff --git a/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java b/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java index 7eda395..8cfd779 100644 --- a/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java +++ b/stats/collector/src/main/java/ru/practicum/properties/CustomProperties.java @@ -6,7 +6,7 @@ @Getter @Setter -@ConfigurationProperties("explore-with-me") +@ConfigurationProperties("my-area-guide") public class CustomProperties { private final Kafka kafka = new Kafka(); @@ -18,4 +18,4 @@ public static class Kafka { private String eventsSimilarityTopic = "events-similarity"; } -} +} \ No newline at end of file diff --git a/stats/stats-server/src/main/resources/application.yaml b/stats/stats-server/src/main/resources/application.yaml index 392170a..0cf2bad 100644 --- a/stats/stats-server/src/main/resources/application.yaml +++ b/stats/stats-server/src/main/resources/application.yaml @@ -1,6 +1,6 @@ spring: application: - name: stats-service + name: stats-server config: import: "configserver:" cloud: @@ -22,4 +22,4 @@ eureka: instance: instance-id: ${spring.application.name}${random.int} preferIpAddress: false - hostname: localhost + hostname: localhost \ No newline at end of file From c6988a98eefaf2465db4540595b0f15f2544c16d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 14:01:02 +0400 Subject: [PATCH 8/9] Comment service fix --- .../comment/controller/CommentAdminController.java | 2 +- .../ru/practicum/comment/service/CommentAdminService.java | 2 +- .../comment/service/CommentAdminServiceImpl.java | 8 +++++--- .../java/ru/practicum/api/comment/CommentAdminApi.java | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java b/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java index c6f0aa7..f39aa50 100644 --- a/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java +++ b/core/comment-service/src/main/java/ru/practicum/comment/controller/CommentAdminController.java @@ -27,7 +27,7 @@ public Collection get(Long userId, int from, int size) { } @Override - public String delete(Long comId) { + public boolean delete(Long comId) { return commentAdminService.delete(comId); } diff --git a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java index bfa01e5..2744230 100644 --- a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java +++ b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminService.java @@ -6,7 +6,7 @@ public interface CommentAdminService { - String delete(Long comId); + boolean delete(Long comId); List search(String text, int from, int size); diff --git a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java index 0b7e42f..b6a2ba3 100644 --- a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java +++ b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentAdminServiceImpl.java @@ -34,10 +34,12 @@ public class CommentAdminServiceImpl implements CommentAdminService { @Override @Transactional - public String delete(Long comId) { - if (!commentRepository.existsById(comId)) throw new NotFoundException("Not found Comment " + comId); + public boolean delete(Long comId) { + if (!commentRepository.existsById(comId)) { + throw new NotFoundException("Not found Comment " + comId); + } commentRepository.deleteById(comId); - return "deleted comment " + comId; + return true; } @Override diff --git a/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java b/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java index 35eb626..9738b88 100644 --- a/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java +++ b/core/core-common/src/main/java/ru/practicum/api/comment/CommentAdminApi.java @@ -28,7 +28,7 @@ Collection get( @DeleteMapping("/admin/comments/{comId}") @ResponseStatus(HttpStatus.NO_CONTENT) - String delete( + boolean delete( @PathVariable @Positive Long comId ); From 6efdff045229c9d6065000cf1b204ab6ec43261f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BB=D0=B0=D0=B2=D0=B0?= Date: Thu, 27 Nov 2025 23:04:46 +0400 Subject: [PATCH 9/9] Comment service fix --- .../ru/practicum/comment/service/CommentPublicServiceImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentPublicServiceImpl.java b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentPublicServiceImpl.java index 0c5a1f2..405d397 100644 --- a/core/comment-service/src/main/java/ru/practicum/comment/service/CommentPublicServiceImpl.java +++ b/core/comment-service/src/main/java/ru/practicum/comment/service/CommentPublicServiceImpl.java @@ -87,5 +87,4 @@ public CommentDto getCommentByEventAndCommentId(Long eventId, Long comId) { return CommentMapper.toCommentDto(comment, userDto, eventCommentDto); } - } \ No newline at end of file