Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- Односторонняя дружба: добавление/удаление друзей, список друзей, общие друзья.
- Лайки фильмам: добавление/удаление, топ-N фильмов по количеству лайков.
- Отзывы к фильмам: положительные и негативные, с рейтингом полезности по лайкам и дизлайкам.
- Лента событий пользователя: лайки, друзья и отзывы.
- Слоистая архитектура: контроллер → сервис → хранилище, выбор реализации хранилища через `@Qualifier`.
- Внедрение зависимостей через конструктор (`@Autowired`).
- Строгая валидация входящих данных через `spring-boot-starter-validation`.
Expand Down Expand Up @@ -91,6 +92,8 @@
- `DELETE /users/{id}/friends/{friendId}` — удалить из друзей.
- `GET /users/{id}/friends` — список друзей пользователя.
- `GET /users/{id}/friends/common/{otherId}` — общие друзья с другим пользователем.
- `GET /users/{id}/feed` — лента событий пользователя по порядку: лайки (`LIKE`), друзья (`FRIEND`)
и отзывы (`REVIEW`) с операциями `ADD`, `REMOVE`, `UPDATE`.

### Отзывы (`/reviews`)
- `POST /reviews` — добавить отзыв.
Expand Down Expand Up @@ -136,6 +139,7 @@ erDiagram
films ||--o{ reviews : "film_id"
reviews ||--o{ review_likes : "review_id"
users ||--o{ review_likes : "user_id"
users ||--o{ feed_events : "user_id"

films {
BIGINT id PK
Expand Down Expand Up @@ -192,6 +196,14 @@ erDiagram
BIGINT user_id PK, FK
BOOLEAN is_useful "TRUE — лайк, FALSE — дизлайк"
}
feed_events {
BIGINT event_id PK
BIGINT user_id FK
VARCHAR event_type "LIKE, REVIEW, FRIEND"
VARCHAR operation "ADD, REMOVE, UPDATE"
BIGINT entity_id "id фильма, друга или отзыва"
BIGINT created_at "время события, мс"
}
```

| Таблица | Что хранит |
Expand All @@ -207,6 +219,7 @@ erDiagram
| `film_director` | связь "фильм — режиссёр", многие ко многим |
| `reviews` | отзывы пользователей к фильмам; рейтинг полезности не хранится, а считается по `review_likes` |
| `review_likes` | оценки отзывов: одна строка — один пользователь, лайк или дизлайк |
| `feed_events` | лента: что пользователь лайкнул, кого добавил в друзья, какие отзывы написал |

У таблиц-связок составной первичный ключ, поэтому одна и та же пара не запишется дважды.
При удалении фильма или пользователя связанные с ним записи удаляются каскадно.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import ru.yandex.practicum.filmorate.model.Event;
import ru.yandex.practicum.filmorate.model.User;
import ru.yandex.practicum.filmorate.service.UserService;

import java.util.Collection;
import java.util.List;

@RestController
@RequestMapping("/users")
Expand Down Expand Up @@ -76,4 +78,10 @@ public Collection<User> getCommonFriends(
log.info("GET /users/{}/friends/common/{}", id, otherId);
return userService.getCommonFriends(id, otherId);
}

@GetMapping("/{id}/feed")
public List<Event> getFeed(@PathVariable long id) {
log.info("GET /users/{}/feed", id);
return userService.getFeed(id);
}
}
13 changes: 13 additions & 0 deletions src/main/java/ru/yandex/practicum/filmorate/model/Event.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ru.yandex.practicum.filmorate.model;

import lombok.Data;

@Data
public class Event {
private Long eventId;
private Long timestamp;
private Long userId;
private EventType eventType;
private Operation operation;
private Long entityId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.yandex.practicum.filmorate.model;

public enum EventType {
LIKE,
REVIEW,
FRIEND
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.yandex.practicum.filmorate.model;

public enum Operation {
ADD,
REMOVE,
UPDATE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ru.yandex.practicum.filmorate.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
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.storage.feed.FeedStorage;

import java.time.Instant;
import java.util.List;

@Service
public class FeedService {

private final FeedStorage feedStorage;

@Autowired
public FeedService(FeedStorage feedStorage) {
this.feedStorage = feedStorage;
}

public void addEvent(long userId, EventType eventType, Operation operation, long entityId) {
Event event = new Event();
event.setTimestamp(Instant.now().toEpochMilli());
event.setUserId(userId);
event.setEventType(eventType);
event.setOperation(operation);
event.setEntityId(entityId);
feedStorage.add(event);
}

public List<Event> findByUserId(long userId) {
return feedStorage.findByUserId(userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import ru.yandex.practicum.filmorate.exception.NotFoundException;
import ru.yandex.practicum.filmorate.exception.ValidationException;
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.storage.film.FilmStorage;
import ru.yandex.practicum.filmorate.storage.user.UserStorage;

Expand All @@ -32,18 +34,21 @@ public class FilmService {
private final MpaService mpaService;
private final GenreService genreService;
private final DirectorService directorService;
private final FeedService feedService;

@Autowired
public FilmService(@Qualifier("filmDbStorage") FilmStorage filmStorage,
@Qualifier("userDbStorage") UserStorage userStorage,
MpaService mpaService,
GenreService genreService,
DirectorService directorService) {
DirectorService directorService,
FeedService feedService) {
this.filmStorage = filmStorage;
this.userStorage = userStorage;
this.mpaService = mpaService;
this.genreService = genreService;
this.directorService = directorService;
this.feedService = feedService;
}

// Получение фильма по id с проверкой существования
Expand All @@ -63,6 +68,7 @@ public Film addLike(long filmId, long userId) {
getUserOrThrow(userId); // проверка существования пользователя — иначе 404

filmStorage.addLike(filmId, userId);
feedService.addEvent(userId, EventType.LIKE, Operation.ADD, filmId);
log.debug("Пользователь id={} поставил лайк фильму id={}", userId, filmId);
return film;
}
Expand All @@ -77,6 +83,7 @@ public Film removeLike(long filmId, long userId) {
);
}

feedService.addEvent(userId, EventType.LIKE, Operation.REMOVE, filmId);
log.debug("Пользователь id={} убрал лайк с фильма id={}", userId, filmId);
return film;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.springframework.stereotype.Service;
import ru.yandex.practicum.filmorate.exception.NotFoundException;
import ru.yandex.practicum.filmorate.exception.ValidationException;
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.storage.film.FilmStorage;
import ru.yandex.practicum.filmorate.storage.review.ReviewStorage;
Expand All @@ -20,14 +22,17 @@ public class ReviewService {
private final ReviewStorage reviewStorage;
private final FilmStorage filmStorage;
private final UserStorage userStorage;
private final FeedService feedService;

@Autowired
public ReviewService(ReviewStorage reviewStorage,
@Qualifier("filmDbStorage") FilmStorage filmStorage,
@Qualifier("userDbStorage") UserStorage userStorage) {
@Qualifier("userDbStorage") UserStorage userStorage,
FeedService feedService) {
this.reviewStorage = reviewStorage;
this.filmStorage = filmStorage;
this.userStorage = userStorage;
this.feedService = feedService;
}

private Review getReviewOrThrow(long id) {
Expand All @@ -49,6 +54,7 @@ public Review add(Review review) {
checkUserExists(review.getUserId());
checkFilmExists(review.getFilmId());
Review created = reviewStorage.add(review);
feedService.addEvent(created.getUserId(), EventType.REVIEW, Operation.ADD, created.getReviewId());
log.debug("Пользователь id={} оставил отзыв id={} к фильму id={}",
created.getUserId(), created.getReviewId(), created.getFilmId());
return created;
Expand All @@ -60,12 +66,15 @@ public Review update(Review review) {
throw new ValidationException("Не указан id отзыва");
}
getReviewOrThrow(review.getReviewId());
return reviewStorage.update(review);
Review updated = reviewStorage.update(review);
feedService.addEvent(updated.getUserId(), EventType.REVIEW, Operation.UPDATE, updated.getReviewId());
return updated;
}

public void delete(long id) {
getReviewOrThrow(id);
Review review = getReviewOrThrow(id);
reviewStorage.delete(id);
feedService.addEvent(review.getUserId(), EventType.REVIEW, Operation.REMOVE, id);
log.debug("Отзыв id={} удалён", id);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@
import org.springframework.stereotype.Service;
import ru.yandex.practicum.filmorate.exception.NotFoundException;
import ru.yandex.practicum.filmorate.exception.ValidationException;
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.model.User;
import ru.yandex.practicum.filmorate.storage.user.UserStorage;

import java.util.Collection;
import java.util.List;

@Service
@Slf4j
public class UserService {

private final UserStorage userStorage;
private final FeedService feedService;

@Autowired
public UserService(@Qualifier("userDbStorage") UserStorage userStorage) {
public UserService(@Qualifier("userDbStorage") UserStorage userStorage, FeedService feedService) {
this.userStorage = userStorage;
this.feedService = feedService;
}

private void validateDifferentUsers(long userId, long otherId) {
Expand All @@ -44,6 +50,7 @@ public User addFriend(long userId, long friendId) {
getUserOrThrow(friendId); // проверка существования друга — иначе 404

userStorage.addFriend(userId, friendId);
feedService.addEvent(userId, EventType.FRIEND, Operation.ADD, friendId);
log.debug("Пользователь id={} добавил в друзья id={}", userId, friendId);
return user;
}
Expand All @@ -55,6 +62,7 @@ public User removeFriend(long userId, long friendId) {
getUserOrThrow(friendId); // проверка существования друга — иначе 404

userStorage.removeFriend(userId, friendId);
feedService.addEvent(userId, EventType.FRIEND, Operation.REMOVE, friendId);
log.debug("Пользователь id={} удалил из друзей id={}", userId, friendId);
return user;
}
Expand All @@ -72,6 +80,11 @@ public Collection<User> getCommonFriends(long userId, long otherId) {
return userStorage.findCommonFriends(userId, otherId);
}

public List<Event> getFeed(long userId) {
getUserOrThrow(userId);
return feedService.findByUserId(userId);
}

// Логин не может содержать пробелы
private void validateLogin(User user) {
if (user.getLogin().contains(" ")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ru.yandex.practicum.filmorate.storage.feed;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import ru.yandex.practicum.filmorate.model.Event;
import ru.yandex.practicum.filmorate.storage.mapper.EventRowMapper;

import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.List;

@Repository
public class FeedDbStorage implements FeedStorage {

private static final String INSERT_QUERY =
"INSERT INTO feed_events (user_id, event_type, operation, entity_id, created_at) VALUES (?, ?, ?, ?, ?)";
private static final String FIND_BY_USER_QUERY =
"SELECT event_id, user_id, event_type, operation, entity_id, created_at FROM feed_events "
+ "WHERE user_id = ? ORDER BY event_id";

private final JdbcTemplate jdbc;
private final EventRowMapper mapper;

@Autowired
public FeedDbStorage(JdbcTemplate jdbc, EventRowMapper mapper) {
this.jdbc = jdbc;
this.mapper = mapper;
}

@Override
public Event add(Event event) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbc.update(connection -> {
PreparedStatement ps = connection.prepareStatement(INSERT_QUERY, Statement.RETURN_GENERATED_KEYS);
ps.setLong(1, event.getUserId());
ps.setString(2, event.getEventType().name());
ps.setString(3, event.getOperation().name());
ps.setLong(4, event.getEntityId());
ps.setLong(5, event.getTimestamp());
return ps;
}, keyHolder);
event.setEventId(keyHolder.getKeyAs(Long.class));
return event;
}

@Override
public List<Event> findByUserId(long userId) {
return jdbc.query(FIND_BY_USER_QUERY, mapper, userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ru.yandex.practicum.filmorate.storage.feed;

import ru.yandex.practicum.filmorate.model.Event;

import java.util.List;

public interface FeedStorage {

Event add(Event event);

List<Event> findByUserId(long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ru.yandex.practicum.filmorate.storage.mapper;

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<Event> {

@Override
public Event mapRow(ResultSet rs, int rowNum) throws SQLException {
Event event = new Event();
event.setEventId(rs.getLong("event_id"));
event.setTimestamp(rs.getLong("created_at"));
event.setUserId(rs.getLong("user_id"));
event.setEventType(EventType.valueOf(rs.getString("event_type")));
event.setOperation(Operation.valueOf(rs.getString("operation")));
event.setEntityId(rs.getLong("entity_id"));
return event;
}
}
Loading
Loading