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
122 changes: 65 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,65 @@
# Комментарии — API (техническое задание)

Краткое описание: набор REST-эндпоинтов для управления комментариями. Три уровня доступа:
- Admin — администраторские операции (удаление комментария по его идентификатору).
- Private — операции пользователя над собственными комментариями (создать, изменить, удалить, получить все свои комментарии).
- Public — публичный доступ (просмотр комментариев конкретного события).

---

## AdminCommentController
Цель: операции управления всеми комментариями.

- DELETE `/admin/comments/{commentId}`
- Описание: удалить комментарий (жёсткое удаление) по его идентификатору.
- Ограничения: без модерации комментариев.
- Права: только админ.
- Ответ: `204 No Content` при успехе.

---

## PrivateCommentController
Цель: пользовательские операции над собственными комментариями.

- POST `/users/{userId}/comments/{eventId}`
- Описание: создать новый комментарий от имени пользователя `userId`.
- Ограничения: длина комментария 5000 символов, проверка существования идентификаторов события и пользователя.
- Права: только аутентифицированный пользователь.
- Ответ: `201 Created` с созданным объектом.

- PATCH `/users/{userId}/comments/{eventId}/{commentId}`
- Описание: обновить собственный комментарий.
- Ограничения: длина комментария 5000 символов, проверка существования идентификаторов комментария, события и пользователя.
- Права: только автор комментария.
- Ответ: обновлённый объект комментария.

- DELETE `/users/{userId}/comments/{commentId}`
- Описание: удалить собственный комментарий (жёсткое удаление).
- Ограничения: проверка существования идентификаторов комментария и пользователя.
- Права: только автор комментария.
- Ответ: `204 No Content`.

- GET `/users/{userId}/comments`
- Описание: получить список собственных комментариев.
- Ограничения: организовать пагинацию (по умолчанию по 10 комментариев на странице).
- Права: только аутентифицированный пользователь.
- Ответ: список комментариев.

---

## PublicCommentController
Цель: публичный просмотр комментариев.

- GET `/comments/event/{eventId}`
- Описание: получить публичный список комментариев к событию.
- Ограничения: организовать пагинацию (по умолчанию по 10 комментариев на странице).
- Доступ: публичный (без авторизации).
- Ответ: список публичных комментариев.
## ExploreWithMe

### Общая информация
Это микросервисная реализация приложения ExploreWithMe для поиска событий и участия в них.
Исходный монолит разделен на несколько независимых сервисов, взаимодействующих через API Gateway и Feign-клиенты.

**Технологический стек**: Java 21, Spring Boot, Spring Cloud(Eureka, Gateway, OpenFeign, Config), MapStruct, Lombok, Apache Maven, Docker, PostgreSQL, Postman.

## Основные сервисы
1. **event-service** - сервис управления событиями, категориями, подборками;
2. **user-service** - сервис управления пользователями.;
3. **request-service** - сервис управления запросами на участие в событии;
4. **comment-service** - сервис управления комментариями;
5. **stats-service** - сервис сбора и предоставления статистики;
6. **discovery-server** (Eureka) - регистрация и обнаружение сервисов;
7. **config-server** - централизованное хранение конфигураций для всех сервисов;
8. **gateway-server** - единая точка входа (API Gateway).

### Вспомогательный модуль
- **interaction-api** - общие DTO, Feign-клиенты, исключения, перечисления

### Конфигурация
Все настройки хранятся в **config-server**:
- `gateway-server.yml` - маршрутизация Gateway
- `user-service.yml` - настройки user-service
- `event-service.yml` - настройки event-service
- `request-service.yml` - настройки request-service
- `comment-service.yml` - настройки comment-service
- `stats-service.yml` - настройки stats-service

Приложение имеет общую базу данных. Каждый сервис имеет собственную schema.sql, обеспечивая изоляцию данных.

## Внутренний API для взаимодействия сервисов
Для взаимодействия между микросервисами используются Feign Clients и Internal Endpoints:
- GET /internal/events/{eventId} - возвращает полное описание события;
- GET /internal/requests/event/{eventId}/count/{status} - возвращает количество заявок события с определенным статусом;
- GET /internal/users/{userId} - возвращает краткую информацию о пользователе.

## Внешний API
Взаимодействие с внешними запросами происходит через API Gateway, который перенаправляет внешние запросы к нужным сервисам.<br>
Примеры основных эндопинтов:<br>
### Публичные(доступны всем):
- GET /comments/events/{eventId} - поиск опубликованных комментариев к событию;
- GET /events - поиск событий;
- GET /categories - категории событий;
- GET /compilations/{compId} - поиск подборки по id;
### Приватные(доступны для авторизованных пользователей):
- POST /users/{userId}/comments - добавить комментарий к событию;
- PATCH /users/{userId}/comments/{commentId} - изменить комментарий;
- GET /users/{userId}/events - события пользователя;
- GET /users/{userId}/events/{eventId}/requests - все заявки на участие в событии;
- PATCH /users/{userId}/requests/{requestId}/cancel - отмена заявки на участие;

### Административные(доступны только администраторам):
- GET /admin/comments - поиск комментариев;
- DELETE /admin/comments/{commentId} - удалить комментарий;
- POST /admin/categories - создать категорию;
- PATCH /admin/compilations/{compId} - изменить подборку;
- GET /admin/events - расширенный поиск событий с фильтрами;
- POST /admin/users/ - создать пользователя;
- DELETE /admin/users/{userId} - удалить пользователя.

### Полная спецификация OpenAPI доступна в файлах:
1. [ewm-main-service-spec.json](./ewm-main-service-spec.json) - основная спецификация
2. [ewm-stat-service-spec.json](./ewm-stat-service-spec.json) - спецификация статистики
157 changes: 157 additions & 0 deletions core/comment-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.yandex.practicum</groupId>
<artifactId>core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>comment-service</artifactId>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>ru.yandex.practicum</groupId>
<artifactId>interaction-api</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>ru.yandex.practicum</groupId>
<artifactId>stats-client</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>ru.yandex.practicum</groupId>
<artifactId>stats-dto</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.5</version>
</dependency>

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ru.yandex.practicum;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class CommentServiceApp {
public static void main(String[] args) {
SpringApplication.run(CommentServiceApp.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package ru.yandex.practicum.controller;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import ru.yandex.practicum.dto.comment.*;
import ru.yandex.practicum.enums.CommentStatus;
import ru.yandex.practicum.service.CommentService;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Validated
@RestController
@RequestMapping("/admin/comments")
@RequiredArgsConstructor
public class AdminCommentController {
private final CommentService commentService;

@GetMapping
public List<CommentDto> adminSearch(
@RequestParam(required = false) CommentStatus status,
@RequestParam(required = false) Long eventId,
@RequestParam(required = false) Long userId,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end,
@RequestParam(defaultValue = "0") @Min(0) int from,
@RequestParam(defaultValue = "10") @Min(1) int size) {

log.info("Admin search: status={}, eventId={}, userId={}, start={}, end={}, from={}, size={}",
status, eventId, userId, start, end, from, size);

return commentService.adminSearch(status, eventId, userId, start, end, from, size);
}

@PatchMapping("/{commentId}")
public CommentDto updateCommentByAdmin(@PathVariable Long commentId,
@RequestBody @Valid CommentAdminDto commentAdminDto) {
log.info("Admin update comment: id={}, data={}", commentId, commentAdminDto);
return commentService.updateCommentByAdmin(commentId, commentAdminDto);
}

@DeleteMapping("/{commentId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteCommentByAdmin(@PathVariable Long commentId) {
log.info("Admin delete comment: id={}", commentId);
commentService.deleteCommentByAdmin(commentId);
}

@GetMapping("/events/{eventId}")
public List<CommentDto> getCommentsByEventAndStatus(@PathVariable Long eventId,
@RequestParam CommentStatus status) {
log.info("Get comments by event and status: eventId={}, status={}", eventId, status);
return commentService.getCommentByStatus(eventId, status);
}
}
Loading