From a6de3932514d4cd491def9c156b5484c11d37b56 Mon Sep 17 00:00:00 2001 From: AleksOBM Date: Tue, 24 Feb 2026 07:02:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BE=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D1=83=D1=8E=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=BE=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D0=BA=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filmorate/controller/FilmController.java | 6 +++ .../practicum/filmorate/dal/FilmStorage.java | 4 ++ .../filmorate/dal/database/FilmDbStorage.java | 44 ++++++++++++++++++- .../dal/database/sql/FilmQueryes.java | 18 ++++++-- .../dal/inmemory/InMemoryFilmStorage.java | 7 +++ .../dal/rowmappers/FilmRowMapper.java | 1 + .../dal/rowmappers/LikeRowMapper.java | 20 +++++++++ .../practicum/filmorate/dto/FilmDto.java | 3 ++ .../filmorate/mapper/FilmMapper.java | 3 ++ .../practicum/filmorate/model/Assessment.java | 26 +++++++++++ .../practicum/filmorate/model/Film.java | 29 +++++++++++- .../practicum/filmorate/model/Like.java | 13 ++++++ .../filmorate/service/FilmService.java | 28 ++++++++++++ src/main/resources/schema.sql | 16 ++++++- .../filmorate/FilmoRateApplicationTests.java | 18 ++++++-- 15 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/LikeRowMapper.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Assessment.java create mode 100644 src/main/java/ru/yandex/practicum/filmorate/model/Like.java diff --git a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java index 2362097..9407437 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java +++ b/src/main/java/ru/yandex/practicum/filmorate/controller/FilmController.java @@ -7,6 +7,7 @@ import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest; import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest; +import ru.yandex.practicum.filmorate.model.Assessment; import ru.yandex.practicum.filmorate.service.FilmService; import ru.yandex.practicum.filmorate.service.util.LikeAction; @@ -66,6 +67,11 @@ public FilmDto update(@RequestBody FilmUpdateRequest request) { return filmService.update(request); } + @PutMapping("/{filmId}/assessment/{assessment}/user/{userId}") + public void setAssessment(@PathVariable long filmId,@PathVariable int assessment, @PathVariable long userId) { + filmService.changeAssessment(LikeAction.SET, filmId, assessment, userId); + } + @PutMapping("/{filmId}/like/{userId}") public void setLike(@PathVariable long filmId, @PathVariable long userId) { filmService.changeLike(LikeAction.SET, filmId, userId); diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java index 1fe9ca9..6bbe7e2 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/FilmStorage.java @@ -1,6 +1,8 @@ package ru.yandex.practicum.filmorate.dal; +import ru.yandex.practicum.filmorate.model.Assessment; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Like; import java.util.Collection; import java.util.Map; @@ -52,4 +54,6 @@ public interface FilmStorage { void removeFilm(long filmId); Collection getFilmsByIds(Collection ids); + + Like createLike(long filmId, int assessmentValue, long userId); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java index eac5594..f4fc82b 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/FilmDbStorage.java @@ -7,12 +7,17 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import ru.yandex.practicum.filmorate.dal.FilmStorage; +import ru.yandex.practicum.filmorate.dal.rowmappers.LikeRowMapper; import ru.yandex.practicum.filmorate.exception.InternalServerException; +import ru.yandex.practicum.filmorate.model.Assessment; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Like; import java.sql.SQLException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; import static ru.yandex.practicum.filmorate.dal.database.sql.FilmQueryes.*; @@ -34,7 +39,8 @@ public Film createFilm(Film film) { film.getDescription(), film.getReleaseDate(), film.getDuration().toMinutes(), - film.getMpaId() + film.getMpaId(), + film.getAverageAssessment() ); film.setId(id); insertGenreIds(film); @@ -52,6 +58,7 @@ public Film updateFilm(Film film) { film.getReleaseDate(), film.getDuration().toMinutes(), film.getMpaId(), + film.getAverageAssessment(), film.getId() ); insertGenreIds(film); @@ -65,9 +72,15 @@ public Optional findById(long filmId) { Optional filmOptional = findOneByIdInTable(filmId, "films"); filmOptional.ifPresent(film -> film.setGenreIds(getGenreIdsByFilmId(filmId))); filmOptional.ifPresent(film -> film.setDirectorIds(getDirectorIdsByFilmId(filmId))); + filmOptional.ifPresent(film -> film.setAssessments(getLikesByFilmId(filmId))); return filmOptional; } + private Set getLikesByFilmId(long filmId) { + return jdbc.query(SQL_FILMS_FIND_LIKES_BY_FILM_ID, new LikeRowMapper(), filmId).stream() + .collect(Collectors.toUnmodifiableSet()); + } + @Override public Collection findAll() { return findManyFilms(SQL_FILMS_FIND_ALL); @@ -193,6 +206,8 @@ private Collection findManyFilms(String query, Object... params) { if (!rs.wasNull() && film != null) film.addGenreId(genreId); int directorId = rs.getInt("director_id"); if (!rs.wasNull() && film != null) film.addDirectorId(directorId); + float assessment = rs.getFloat("assessment"); + if (!rs.wasNull() && film != null) film.setAverageAssessment(assessment); } return new ArrayList<>(films.values()); }, params); @@ -226,4 +241,31 @@ WHERE f.id IN (%s) return findManyFilms(sql, ids.toArray()); } + + @Transactional + @Override + public Like createLike(long filmId, int assessmentValue, long userId) { + long likeId = insertWithKeyHolder( + "INSERT INTO assessments (film_id, user_id, assessment) VALUES (?, ?, ?)", + filmId, + userId, + assessmentValue + ); + updateAverageAssessment(filmId); + return Like.builder() + .id(likeId) + .assessment(Assessment.of(assessmentValue)) + .userId(userId) + .build(); + } + + private void updateAverageAssessment(long filmId) { + Optional optionalFilm = findOneByIdInTable(filmId, "films"); + optionalFilm.ifPresent(film -> { + film.setAssessments(getLikesByFilmId(filmId)); + film.getAverageAssessment(); + updateFilm(film); + }); + } + } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java b/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java index 6b21bf1..b293e18 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/database/sql/FilmQueryes.java @@ -9,6 +9,7 @@ public class FilmQueryes { mrg.RELEASE_DATE, mrg.DURATION, mrg.MPA_ID, + mrg.ASSESSMENT, gof.GENRE_ID, dof.DIRECTOR_ID, user_id @@ -19,6 +20,7 @@ public class FilmQueryes { RELEASE_DATE, DURATION, MPA_ID, + f.ASSESSMENT, l.USER_ID FROM FILMS f LEFT JOIN LIKES l ON f.ID = l.FILM_ID @@ -35,6 +37,7 @@ public class FilmQueryes { mrg.RELEASE_DATE, mrg.DURATION, mrg.MPA_ID, + mrg.ASSESSMENT, gof.GENRE_ID, dof.DIRECTOR_ID, user_id @@ -45,6 +48,7 @@ public class FilmQueryes { RELEASE_DATE, DURATION, MPA_ID, + f.ASSESSMENT, l.USER_ID FROM FILMS f LEFT JOIN LIKES l ON f.ID = l.FILM_ID @@ -129,6 +133,12 @@ MERGE INTO GENRES_OF_FILMS (FILM_ID, GENRE_ID) KEY (FILM_ID, GENRE_ID) VALUES WHERE dof.FILM_ID = ? """; + public static final String SQL_FILMS_FIND_LIKES_BY_FILM_ID = """ + SELECT * + FROM ASSESSMENTS + WHERE FILM_ID = ? + """; + public static final String SQL_FILMS_INSERT_DIRECTORIDS = """ MERGE INTO DIRECTORS_OF_FILMS (FILM_ID, DIRECTOR_ID) KEY (FILM_ID, DIRECTOR_ID) VALUES """; @@ -139,9 +149,10 @@ INSERT INTO films ( description, release_date, duration, - mpa_id + mpa_id, + assessment ) - VALUES (?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?) """; public static final String SQL_FILMS_UPDATE = """ @@ -150,7 +161,8 @@ INSERT INTO films ( description = ?, release_date = ?, duration = ?, - mpa_id = ? + mpa_id = ?, + assessment = ? WHERE id = ? """; diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java b/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java index bdbb0ed..b254b8e 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/inmemory/InMemoryFilmStorage.java @@ -3,7 +3,9 @@ import org.springframework.stereotype.Component; import ru.yandex.practicum.filmorate.dal.FilmStorage; import ru.yandex.practicum.filmorate.exception.MethodNotImplementedException; +import ru.yandex.practicum.filmorate.model.Assessment; import ru.yandex.practicum.filmorate.model.Film; +import ru.yandex.practicum.filmorate.model.Like; import java.util.Collection; import java.util.Map; @@ -87,4 +89,9 @@ public void removeFilm(long filmId) { public Collection getFilmsByIds(Collection ids) { throw new MethodNotImplementedException(); } + + @Override + public Like createLike(long filmId, int assessmentValue, long userId) { + throw new MethodNotImplementedException(); + } } \ No newline at end of file diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/FilmRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/FilmRowMapper.java index 13224bb..74c43ed 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/FilmRowMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/FilmRowMapper.java @@ -21,6 +21,7 @@ public Film mapRow(ResultSet resultSet, int rowNum) throws SQLException { .releaseDate(resultSet.getDate("release_date").toLocalDate()) .duration(Duration.ofMinutes(resultSet.getInt("duration"))) .mpaId(resultSet.getInt("mpa_id")) + .averageAssessment(resultSet.getFloat("assessment")) .build(); } diff --git a/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/LikeRowMapper.java b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/LikeRowMapper.java new file mode 100644 index 0000000..9559054 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/dal/rowmappers/LikeRowMapper.java @@ -0,0 +1,20 @@ +package ru.yandex.practicum.filmorate.dal.rowmappers; + +import org.springframework.jdbc.core.RowMapper; +import ru.yandex.practicum.filmorate.model.Assessment; +import ru.yandex.practicum.filmorate.model.Like; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class LikeRowMapper implements RowMapper { + @Override + public Like mapRow(ResultSet resultSet, int rowNum) throws SQLException { + return Like.builder() + .id(resultSet.getLong("id")) + .filmId(resultSet.getLong("film_id")) + .userId(resultSet.getLong("user_id")) + .assessment(Assessment.of(resultSet.getInt("assessment"))) + .build(); + } +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java index ba35e8f..827e9a4 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java +++ b/src/main/java/ru/yandex/practicum/filmorate/dto/FilmDto.java @@ -24,6 +24,9 @@ public class FilmDto { @JsonProperty("rate") int likesCount; + @JsonProperty("assessment") + float averageAssessment; + @Builder.Default private MpaDto mpa = new MpaDto(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java index 056a560..3f9dd35 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java +++ b/src/main/java/ru/yandex/practicum/filmorate/mapper/FilmMapper.java @@ -54,6 +54,7 @@ public static FilmDto mapToFilmDto(Film film, Mpa mpa, Set genres, Set new DirectorDto(director.getId(), director.getName())) .toList() ) + .averageAssessment(film.getAverageAssessment()) .build(); } @@ -87,6 +88,8 @@ public static Film updateFilmFields(Film film, FilmUpdateRequest request) { .map(Director::getId) .collect(Collectors.toSet())); + film.getAverageAssessment(); + return film; } diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Assessment.java b/src/main/java/ru/yandex/practicum/filmorate/model/Assessment.java new file mode 100644 index 0000000..f232ee7 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Assessment.java @@ -0,0 +1,26 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Getter; + +@Getter +public enum Assessment { + UNDEFINED, ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN; + + private int value; + + public static Assessment of(int value) { + Assessment result = Assessment.UNDEFINED; + if (value < 1 || value > 10) { + result.setValue(value); + return result; + } + result = values()[value]; + result.setValue(result.ordinal()); + return result; + } + + private void setValue(int value) { + this.value = value; + } +} + diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java index a16f4e6..b1f7a8f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/model/Film.java +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Film.java @@ -10,6 +10,7 @@ import java.time.LocalDate; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; @Data @Builder(toBuilder = true) @@ -37,8 +38,34 @@ public class Film { private Set likes = new HashSet<>(); @JsonProperty(value = "rate") + private int likesCount; + @Builder.Default - private int likesCount = 0; + private Set assessments = new HashSet<>(); + + @JsonProperty(value = "assessment") + private float averageAssessment; + + public float getAverageAssessment() { + if (assessments.isEmpty()) { + return 0f; + } else if (assessments.size() == 1) { + return assessments.iterator().next().getAssessment().getValue(); + } + AtomicLong count = new AtomicLong(); + AtomicLong sum = new AtomicLong(); + assessments.stream() + .map(Like::getAssessment) + .filter(assessment -> !assessment.equals(Assessment.UNDEFINED)) + .map(Assessment::getValue).forEach(value -> { + sum.addAndGet(value); + count.getAndIncrement(); + }); + + float average = (float) sum.get() / count.get(); + averageAssessment = Math.round(average * 100) / 100.0f; + return averageAssessment; + } public int getLikesCount() { likesCount = likes.size(); diff --git a/src/main/java/ru/yandex/practicum/filmorate/model/Like.java b/src/main/java/ru/yandex/practicum/filmorate/model/Like.java new file mode 100644 index 0000000..a454fd8 --- /dev/null +++ b/src/main/java/ru/yandex/practicum/filmorate/model/Like.java @@ -0,0 +1,13 @@ +package ru.yandex.practicum.filmorate.model; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class Like { + long id; + long filmId; + long userId; + Assessment assessment; +} diff --git a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java index 569e0c5..751618f 100644 --- a/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java +++ b/src/main/java/ru/yandex/practicum/filmorate/service/FilmService.java @@ -10,6 +10,7 @@ import ru.yandex.practicum.filmorate.dto.FilmDto; import ru.yandex.practicum.filmorate.dto.request.create.FilmCreateRequest; import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest; +import ru.yandex.practicum.filmorate.exception.MethodNotImplementedException; import ru.yandex.practicum.filmorate.exception.NotFoundException; import ru.yandex.practicum.filmorate.exception.ParameterNotValidException; import ru.yandex.practicum.filmorate.exception.ValidationException; @@ -368,4 +369,31 @@ public Collection getFilmsByIds(Collection ids) { )) .toList(); } + + public void changeAssessment(LikeAction likeAction, long filmId, int assessmentValue, long userId) { + + if (Assessment.of(assessmentValue).equals(Assessment.UNDEFINED)) { + throw new ParameterNotValidException("assessment = " + assessmentValue + + ".", "Значение должно быть от 1 до 10"); + } + easyCheckFilm(filmId); + easyCheckUser(userId); + + switch (likeAction) { + case SET -> filmStorage.createLike(filmId, assessmentValue, userId); + case REMOVE -> throw new MethodNotImplementedException(); + } + } + + private void easyCheckFilm(long filmId) { + if (filmStorage.checkFilmIsNotPresent(filmId)) { + throw new NotFoundException("Фильм с id=" + filmId + " не найден."); + } + } + + private void easyCheckUser(long userId) { + if (userStorage.checkUserIsNotPresent(userId)) { + throw new NotFoundException("Пользователь с id=" + userId + " не найден."); + } + } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index ac774a1..d16ce17 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -20,7 +20,8 @@ CREATE TABLE films ( description varchar(200) NOT NULL, release_date date NOT NULL, duration int NOT NULL, - mpa_id int NOT NULL + mpa_id int NOT NULL, + assessment real ); CREATE TABLE genres ( @@ -56,6 +57,13 @@ CREATE TABLE likes ( film_id bigint NOT NULL ); +CREATE TABLE assessments ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + film_id bigint NOT NULL, + user_id bigint NOT NULL, + assessment int NOT NULL +); + CREATE TABLE reviews ( id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, content varchar(2048) NOT NULL, @@ -102,6 +110,12 @@ REFERENCES users (id) ON DELETE CASCADE; ALTER TABLE likes ADD CONSTRAINT fk_likes_film_id FOREIGN KEY(film_id) REFERENCES films (id) ON DELETE CASCADE; +ALTER TABLE assessments ADD CONSTRAINT fk_assessments_user_id FOREIGN KEY(user_id) +REFERENCES users (id) ON DELETE CASCADE; + +ALTER TABLE assessments ADD CONSTRAINT fk_assessments_film_id FOREIGN KEY(film_id) +REFERENCES films (id) ON DELETE CASCADE; + ALTER TABLE DIRECTORS_OF_FILMS ADD CONSTRAINT fk_directors_of_films_director_id FOREIGN KEY(director_id) REFERENCES directors (id) ON DELETE CASCADE; diff --git a/src/test/java/ru/yandex/practicum/filmorate/FilmoRateApplicationTests.java b/src/test/java/ru/yandex/practicum/filmorate/FilmoRateApplicationTests.java index 46e71d6..3005454 100644 --- a/src/test/java/ru/yandex/practicum/filmorate/FilmoRateApplicationTests.java +++ b/src/test/java/ru/yandex/practicum/filmorate/FilmoRateApplicationTests.java @@ -18,10 +18,7 @@ import ru.yandex.practicum.filmorate.dto.request.update.FilmUpdateRequest; import ru.yandex.practicum.filmorate.mapper.FilmMapper; import ru.yandex.practicum.filmorate.mapper.UserMapper; -import ru.yandex.practicum.filmorate.model.Film; -import ru.yandex.practicum.filmorate.model.Genre; -import ru.yandex.practicum.filmorate.model.Mpa; -import ru.yandex.practicum.filmorate.model.User; +import ru.yandex.practicum.filmorate.model.*; import java.time.Duration; import java.time.LocalDate; @@ -309,6 +306,19 @@ public void likes() { .hasSize(4); } + @Test + void likes2() { +// Assessment assessment1 = Assessment.of(3); +// Assessment assessment2 = Assessment.of(4); +// Assessment assessment3 = Assessment.of(7); +// Assessment assessment4 = Assessment.of(999); +// +// Film film1 = addRandomFilm(); +// film1.setAssessments(Set.of(assessment1, assessment2, assessment3, assessment4)); +// +// assertThat(film1.getAverageAssessment()).isEqualTo(4.67f); + } + @Test void genres() { Film film1 = addRandomFilm();