diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java new file mode 100644 index 0000000..b5fad4b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FeedController.java @@ -0,0 +1,23 @@ +package ru.yandex.practicum.filmorate.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.service.feed.FeedService; + +import java.util.List; + +@RestController +@RequestMapping("/users") +@Slf4j +@RequiredArgsConstructor +public class FeedController { + private final FeedService feedService; + + @GetMapping("/{id}/feed") + public List getUserFeed(@PathVariable long id) { + log.info("GET /users/{}/feed", id); + return feedService.getUserFeed(id); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java new file mode 100644 index 0000000..bcd9a40 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/event/EventDto.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.dto.event; + +import lombok.Builder; +import lombok.Data; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +@Data +@Builder +public class EventDto { + private Long timestamp; + private Long userId; + private EventType eventType; + private Operation operation; + private Long eventId; + private Long entityId; + +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java new file mode 100644 index 0000000..9ac3f6f --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/EventMapper.java @@ -0,0 +1,21 @@ +package ru.yandex.practicum.filmorate.mapper; + +import lombok.experimental.UtilityClass; +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.model.Event; + +@UtilityClass +public class EventMapper { + + public EventDto toEventDto(Event event) { + return EventDto.builder() + .timestamp(event.getTimestamp()) + .userId(event.getUserId()) + .eventType(event.getEventType()) + .operation(event.getOperation()) + .eventId(event.getEventId()) + .entityId(event.getEntityId()) + .build(); + } +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Event.java b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java new file mode 100644 index 0000000..29dad68 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Event.java @@ -0,0 +1,18 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +@Data +@Builder +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Event { + Long eventId; + Long timestamp; + Long userId; + EventType eventType; + Operation operation; + Long entityId; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java new file mode 100644 index 0000000..508d573 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/EventType.java @@ -0,0 +1,5 @@ +package ru.yandex.practicum.filmorate.model; + +public enum EventType { + LIKE, REVIEW, FRIEND +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java new file mode 100644 index 0000000..74ef03b --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Operation.java @@ -0,0 +1,5 @@ +package ru.yandex.practicum.filmorate.model; + +public enum Operation { + REMOVE, ADD, UPDATE +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java new file mode 100644 index 0000000..de91215 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/EventRowMapper.java @@ -0,0 +1,25 @@ +package ru.yandex.practicum.filmorate.repository.feed; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Component +public class EventRowMapper implements RowMapper { + @Override + public Event mapRow(ResultSet rs, int rowNum) throws SQLException { + return Event.builder() + .eventId(rs.getLong("event_id")) + .timestamp(rs.getLong("timestamp")) + .userId(rs.getLong("user_id")) + .eventType(EventType.valueOf(rs.getString("event_type"))) + .operation(Operation.valueOf(rs.getString("operation"))) + .entityId(rs.getLong("entity_id")) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java new file mode 100644 index 0000000..7645f79 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/FeedRepository.java @@ -0,0 +1,10 @@ +package ru.yandex.practicum.filmorate.repository.feed; + +import ru.yandex.practicum.filmorate.model.Event; +import java.util.List; + +public interface FeedRepository { + List findByUserId(long userId); + + Event save(Event event); +} \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java new file mode 100644 index 0000000..c2b17c2 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/repository/feed/JdbcFeedRepository.java @@ -0,0 +1,43 @@ +package ru.yandex.practicum.filmorate.repository.feed; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; +import ru.yandex.practicum.filmorate.model.Event; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class JdbcFeedRepository implements FeedRepository { + private final NamedParameterJdbcOperations jdbc; + private final EventRowMapper eventRowMapper; + + @Override + public List findByUserId(long userId) { + String sql = "SELECT * FROM feed_events WHERE user_id = :user_id ORDER BY timestamp ASC"; + return jdbc.query(sql, + new MapSqlParameterSource("user_id", userId), + eventRowMapper); + } + + @Override + public Event save(Event event) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("timestamp", event.getTimestamp()) + .addValue("user_id", event.getUserId()) + .addValue("event_type", event.getEventType().toString()) + .addValue("operation", event.getOperation().toString()) + .addValue("entity_id", event.getEntityId()); + String sql = """ + INSERT INTO feed_events (timestamp, user_id, event_type, operation, entity_id) + VALUES (:timestamp, :user_id, :event_type, :operation, :entity_id) + """; + jdbc.update(sql, params, keyHolder, new String[]{"event_id"}); + event.setEventId(keyHolder.getKeyAs(Long.class)); + return event; + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java new file mode 100644 index 0000000..d9f9df3 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedService.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.service.feed; + +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; + +import java.util.List; + +public interface FeedService { + List getUserFeed(long userId); + + void addEvent(EventType eventType, Operation operation, long userId, long entityId); +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java new file mode 100644 index 0000000..f941602 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/service/feed/FeedServiceImpl.java @@ -0,0 +1,49 @@ +package ru.yandex.practicum.filmorate.service.feed; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.yandex.practicum.filmorate.dto.event.EventDto; +import ru.yandex.practicum.filmorate.exception.NotFoundException; +import ru.yandex.practicum.filmorate.mapper.EventMapper; +import ru.yandex.practicum.filmorate.model.Event; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; +import ru.yandex.practicum.filmorate.repository.feed.FeedRepository; +import ru.yandex.practicum.filmorate.repository.user.UserRepository; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class FeedServiceImpl implements FeedService { + private final FeedRepository feedRepository; + private final UserRepository userRepository; + + @Override + public List getUserFeed(long userId) { + userRepository.findById(userId) + .orElseThrow(NotFoundException.supplier("User with id %d not found", userId)); + + List events = feedRepository.findByUserId(userId); + log.info("Retrieved {} events for user {}", events.size(), userId); + return events.stream() + .map(EventMapper::toEventDto) + .toList(); + } + + @Override + public void addEvent(EventType eventType, Operation operation, long userId, long entityId) { + Event event = Event.builder() + .timestamp(System.currentTimeMillis()) + .userId(userId) + .eventType(eventType) + .operation(operation) + .entityId(entityId) + .build(); + feedRepository.save(event); + log.debug("Event saved: type={}, operation={}, userId={}, entityId={}", + eventType, operation, userId, entityId); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java index c4397da..c21a947 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/film/FilmServiceImpl.java @@ -14,14 +14,17 @@ import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.FilmMapper; import ru.yandex.practicum.filmorate.model.Director; +import ru.yandex.practicum.filmorate.model.EventType; import ru.yandex.practicum.filmorate.model.Film; import ru.yandex.practicum.filmorate.model.Genre; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.repository.director.DirectorRepository; import ru.yandex.practicum.filmorate.repository.film.FilmRepository; import ru.yandex.practicum.filmorate.repository.genre.GenreRepository; import ru.yandex.practicum.filmorate.repository.like.LikesRepository; import ru.yandex.practicum.filmorate.repository.mparating.MPARatingRepository; import ru.yandex.practicum.filmorate.repository.user.UserRepository; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import java.util.Collection; import java.util.List; @@ -37,6 +40,7 @@ public class FilmServiceImpl implements FilmService { GenreRepository genreRepository; MPARatingRepository mpaRepository; DirectorRepository directorRepository; + FeedService feedService; @Override public Collection findAll() { @@ -137,6 +141,7 @@ public void addLike(long filmId, long userId) { throwIfFilmNotFound(filmId); throwIfUserNotFound(userId); likesRepository.addLike(userId, filmId); + feedService.addEvent(EventType.LIKE, Operation.ADD, userId, filmId); log.info("Like to film with id {} has been added by user {}", filmId, userId); } @@ -145,6 +150,7 @@ public void removeLike(long filmId, long userId) { throwIfFilmNotFound(filmId); throwIfUserNotFound(userId); likesRepository.removeLike(userId, filmId); + feedService.addEvent(EventType.LIKE, Operation.REMOVE, userId, filmId); log.info("Like to film with id {} has been removed by user {}", filmId, userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java index ba5c5a6..d6e29b1 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/review/ReviewServiceImpl.java @@ -3,13 +3,18 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import ru.yandex.practicum.filmorate.dto.review.*; +import ru.yandex.practicum.filmorate.dto.review.NewReviewRequest; +import ru.yandex.practicum.filmorate.dto.review.ReviewDto; +import ru.yandex.practicum.filmorate.dto.review.UpdateReviewRequest; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.ReviewMapper; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.Review; import ru.yandex.practicum.filmorate.repository.film.FilmRepository; import ru.yandex.practicum.filmorate.repository.review.ReviewRepository; import ru.yandex.practicum.filmorate.repository.user.UserRepository; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import java.util.Collection; @@ -20,6 +25,7 @@ public class ReviewServiceImpl implements ReviewService { private final ReviewRepository reviewRepository; private final UserRepository userRepository; private final FilmRepository filmRepository; + private final FeedService feedService; @Override public ReviewDto create(NewReviewRequest request) { @@ -27,6 +33,7 @@ public ReviewDto create(NewReviewRequest request) { throwIfFilmNotFound(request.getFilmId()); Review review = ReviewMapper.toReview(request); review = reviewRepository.save(review); + feedService.addEvent(EventType.REVIEW, Operation.ADD, request.getUserId(), review.getId()); log.info("Review with reviewId {} has been created", review.getId()); return ReviewMapper.toDto(review); } @@ -35,6 +42,7 @@ public ReviewDto create(NewReviewRequest request) { public ReviewDto update(UpdateReviewRequest request) { Review review = getReviewOrThrow(request.getReviewId()); review = ReviewMapper.updateReviewFields(review, request); + feedService.addEvent(EventType.REVIEW, Operation.UPDATE, review.getUserId(), review.getId()); log.info("Review with reviewId {} has been updated", review.getId()); reviewRepository.update(review); return ReviewMapper.toDto(review); @@ -42,7 +50,8 @@ public ReviewDto update(UpdateReviewRequest request) { @Override public void delete(long id) { - getReviewOrThrow(id); + Review review = getReviewOrThrow(id); + feedService.addEvent(EventType.REVIEW, Operation.REMOVE, review.getUserId(), id); log.info("Review with reviewId {} has been deleted", id); reviewRepository.delete(id); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java index ac7ae26..f199adf 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/user/UserServiceImpl.java @@ -8,9 +8,12 @@ import ru.yandex.practicum.filmorate.dto.user.UserDto; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.mapper.UserMapper; +import ru.yandex.practicum.filmorate.model.EventType; +import ru.yandex.practicum.filmorate.model.Operation; import ru.yandex.practicum.filmorate.model.User; import ru.yandex.practicum.filmorate.repository.friendship.FriendshipsRepository; import ru.yandex.practicum.filmorate.repository.user.UserRepository; +import ru.yandex.practicum.filmorate.service.feed.FeedService; import java.util.Collection; @@ -20,6 +23,7 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final FriendshipsRepository friendshipsRepository; + private final FeedService feedService; @Override public Collection findAll() { @@ -68,8 +72,8 @@ public void deleteById(long id) { public void addFriend(long userId, long friendId) { throwIfUserNotFound(userId); throwIfUserNotFound(friendId); - friendshipsRepository.addFriendship(userId, friendId); + feedService.addEvent(EventType.FRIEND, Operation.ADD, userId, friendId); log.info("User with id {} added user with id {} as friend", userId, friendId); } @@ -78,6 +82,7 @@ public void removeFriend(long userId, long friendId) { throwIfUserNotFound(userId); throwIfUserNotFound(friendId); friendshipsRepository.removeFriendship(userId, friendId); + feedService.addEvent(EventType.FRIEND, Operation.REMOVE, userId, friendId); log.info("User with id {} removed user with id {} from friends", userId, friendId); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 0b7b1a0..40ecaab 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -101,4 +101,14 @@ CREATE TABLE IF NOT EXISTS review_likes PRIMARY KEY (review_id, user_id), FOREIGN KEY (review_id) REFERENCES reviews (review_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE -); \ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS feed_events ( + event_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + timestamp BIGINT NOT NULL, + user_id BIGINT NOT NULL, + event_type ENUM('LIKE', 'REVIEW', 'FRIEND') NOT NULL, + operation ENUM('REMOVE', 'ADD', 'UPDATE') NOT NULL, + entity_id BIGINT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (user_id) ON DELETE CASCADE +);