diff --git a/build.gradle b/build.gradle index 2513d38..5bfc14a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.7' - id 'io.spring.dependency-management' version '1.1.7' - id 'org.openapi.generator' version '7.18.0' + id 'org.springframework.boot' version "${springBootVersion}" + id 'io.spring.dependency-management' version "${dependencyManagementVersion}" + id 'org.openapi.generator' version "${openapiGeneratorVersion}" } group = 'ru.practicum' @@ -15,6 +15,10 @@ java { } } +ext { + set('springCloudVersion', "2024.0.0") +} + configurations { compileOnly { extendsFrom annotationProcessor @@ -30,8 +34,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' implementation "org.mapstruct:mapstruct:${mapstructVersion}" - implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-database-postgresql' implementation "io.swagger.core.v3:swagger-annotations:${swaggerVersion}" compileOnly 'org.projectlombok:lombok' @@ -45,6 +52,12 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + openApiGenerate { generatorName = "spring" inputSpec = "$rootDir/src/main/resources/openapi/openapi.yml" @@ -76,6 +89,7 @@ openApiGenerate { "TagCreateDto" : "ru.practicum.eventhub.api.dto.request.TagCreateDto", "TagUpdateDto" : "ru.practicum.eventhub.api.dto.request.TagUpdateDto", "TagDto" : "ru.practicum.eventhub.api.dto.response.TagDto", + "TagWithStatsDto": "ru.practicum.eventhub.api.dto.response.TagWithStatsDto", "ErrorResponse" : "ru.practicum.eventhub.api.exception.ErrorResponse" ] diff --git a/docker-compose.yml b/docker-compose.yml index b74800f..92d4b23 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,15 @@ version: '3.7' services: postgres: image: postgres:14 + container_name: event-hub-db environment: - POSTGRES_USER: postgres POSTGRES_DB: postgres + POSTGRES_USER: postgres POSTGRES_PASSWORD: 123456 ports: - - "5432:5432" \ No newline at end of file + - "5432:5432" + + redis: + image: redis:8.4.0 + ports: + - "6379:6379" diff --git a/gradle.properties b/gradle.properties index 924049d..2b885ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,6 @@ +springBootVersion=3.4.2 +dependencyManagementVersion=1.1.7 +openapiGeneratorVersion=7.10.0 + mapstructVersion=1.6.3 swaggerVersion=2.2.38 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da..aaaabb3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/ru/practicum/eventhub/EventHubApplication.java b/src/main/java/ru/practicum/eventhub/EventHubApplication.java index ebdbd65..dc73a4e 100644 --- a/src/main/java/ru/practicum/eventhub/EventHubApplication.java +++ b/src/main/java/ru/practicum/eventhub/EventHubApplication.java @@ -2,10 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableTransactionManagement +@EnableCaching +@EnableFeignClients +@EnableScheduling public class EventHubApplication { public static void main(String[] args) { diff --git a/src/main/java/ru/practicum/eventhub/api/dto/response/CategoryDto.java b/src/main/java/ru/practicum/eventhub/api/dto/response/CategoryDto.java index f7c67c5..bbb8a87 100644 --- a/src/main/java/ru/practicum/eventhub/api/dto/response/CategoryDto.java +++ b/src/main/java/ru/practicum/eventhub/api/dto/response/CategoryDto.java @@ -15,6 +15,6 @@ public record CategoryDto( OffsetDateTime updatedAt, List projects ) { - public record ProjectShortDto(UUID id, String name) { + public record ProjectShortDto(UUID id) { } } diff --git a/src/main/java/ru/practicum/eventhub/api/dto/response/EventDto.java b/src/main/java/ru/practicum/eventhub/api/dto/response/EventDto.java index eb2568d..ca7f31a 100644 --- a/src/main/java/ru/practicum/eventhub/api/dto/response/EventDto.java +++ b/src/main/java/ru/practicum/eventhub/api/dto/response/EventDto.java @@ -17,6 +17,6 @@ public record EventDto( OffsetDateTime updatedAt, List tags ) { - public record TagShortDto(UUID id, String name) { + public record TagShortDto(UUID id) { } } diff --git a/src/main/java/ru/practicum/eventhub/api/dto/response/ProjectDto.java b/src/main/java/ru/practicum/eventhub/api/dto/response/ProjectDto.java index fea34e8..49ac5ad 100644 --- a/src/main/java/ru/practicum/eventhub/api/dto/response/ProjectDto.java +++ b/src/main/java/ru/practicum/eventhub/api/dto/response/ProjectDto.java @@ -14,6 +14,6 @@ public record ProjectDto( OffsetDateTime updatedAt, CategoryShortDto category ) { - public record CategoryShortDto(UUID id, String name) { + public record CategoryShortDto(UUID id) { } } diff --git a/src/main/java/ru/practicum/eventhub/api/dto/response/TagDto.java b/src/main/java/ru/practicum/eventhub/api/dto/response/TagDto.java index 5727f03..bfc3deb 100644 --- a/src/main/java/ru/practicum/eventhub/api/dto/response/TagDto.java +++ b/src/main/java/ru/practicum/eventhub/api/dto/response/TagDto.java @@ -12,6 +12,6 @@ public record TagDto( String description, List events ) { - public record EventShortDto(UUID id, String title) { + public record EventShortDto(UUID id) { } } diff --git a/src/main/java/ru/practicum/eventhub/api/dto/response/TagStatsDto.java b/src/main/java/ru/practicum/eventhub/api/dto/response/TagStatsDto.java new file mode 100644 index 0000000..a49c50d --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/api/dto/response/TagStatsDto.java @@ -0,0 +1,10 @@ +package ru.practicum.eventhub.api.dto.response; + +import java.time.OffsetDateTime; + +public record TagStatsDto( + long usageCount, + OffsetDateTime createdAt, + OffsetDateTime updatedAt +) { +} diff --git a/src/main/java/ru/practicum/eventhub/api/dto/response/TagWithStatsDto.java b/src/main/java/ru/practicum/eventhub/api/dto/response/TagWithStatsDto.java new file mode 100644 index 0000000..a4b228b --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/api/dto/response/TagWithStatsDto.java @@ -0,0 +1,17 @@ +package ru.practicum.eventhub.api.dto.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import ru.practicum.eventhub.api.model.EventShortDto; + +import java.util.List; +import java.util.UUID; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record TagWithStatsDto( + UUID id, + String name, + String description, + List events, + TagStatsDto stats +) { +} diff --git a/src/main/java/ru/practicum/eventhub/api/dto/response/UserMetadataDto.java b/src/main/java/ru/practicum/eventhub/api/dto/response/UserMetadataDto.java index 2b7eeda..425a220 100644 --- a/src/main/java/ru/practicum/eventhub/api/dto/response/UserMetadataDto.java +++ b/src/main/java/ru/practicum/eventhub/api/dto/response/UserMetadataDto.java @@ -16,6 +16,6 @@ public record UserMetadataDto( OffsetDateTime createdAt, OffsetDateTime updatedAt ) { - public record UserShortDto(UUID id, String username, String email) { + public record UserShortDto(UUID id) { } } diff --git a/src/main/java/ru/practicum/eventhub/api/mapper/CategoryMapper.java b/src/main/java/ru/practicum/eventhub/api/mapper/CategoryMapper.java index 0b94470..61e2fc2 100644 --- a/src/main/java/ru/practicum/eventhub/api/mapper/CategoryMapper.java +++ b/src/main/java/ru/practicum/eventhub/api/mapper/CategoryMapper.java @@ -5,8 +5,8 @@ import ru.practicum.eventhub.api.dto.request.CategoryUpdateDto; import ru.practicum.eventhub.api.dto.request.ProjectCreateDto; import ru.practicum.eventhub.api.dto.response.CategoryDto; -import ru.practicum.eventhub.domain.model.Category; -import ru.practicum.eventhub.domain.model.Project; +import ru.practicum.eventhub.model.Category; +import ru.practicum.eventhub.model.Project; import java.util.Set; diff --git a/src/main/java/ru/practicum/eventhub/api/mapper/EventMapper.java b/src/main/java/ru/practicum/eventhub/api/mapper/EventMapper.java index 2bc52f6..3eaba0d 100644 --- a/src/main/java/ru/practicum/eventhub/api/mapper/EventMapper.java +++ b/src/main/java/ru/practicum/eventhub/api/mapper/EventMapper.java @@ -5,8 +5,8 @@ import ru.practicum.eventhub.api.dto.request.EventUpdateDto; import ru.practicum.eventhub.api.dto.request.TagCreateDto; import ru.practicum.eventhub.api.dto.response.EventDto; -import ru.practicum.eventhub.domain.model.Event; -import ru.practicum.eventhub.domain.model.Tag; +import ru.practicum.eventhub.model.Event; +import ru.practicum.eventhub.model.Tag; import java.util.Set; diff --git a/src/main/java/ru/practicum/eventhub/api/mapper/ProjectMapper.java b/src/main/java/ru/practicum/eventhub/api/mapper/ProjectMapper.java index 0713ced..a3b587e 100644 --- a/src/main/java/ru/practicum/eventhub/api/mapper/ProjectMapper.java +++ b/src/main/java/ru/practicum/eventhub/api/mapper/ProjectMapper.java @@ -6,7 +6,7 @@ import org.mapstruct.MappingTarget; import ru.practicum.eventhub.api.dto.request.ProjectUpdateDto; import ru.practicum.eventhub.api.dto.response.ProjectDto; -import ru.practicum.eventhub.domain.model.Project; +import ru.practicum.eventhub.model.Project; import static org.mapstruct.MappingConstants.ComponentModel.SPRING; import static org.mapstruct.NullValuePropertyMappingStrategy.IGNORE; diff --git a/src/main/java/ru/practicum/eventhub/api/mapper/TagMapper.java b/src/main/java/ru/practicum/eventhub/api/mapper/TagMapper.java index a7a8185..c1fe237 100644 --- a/src/main/java/ru/practicum/eventhub/api/mapper/TagMapper.java +++ b/src/main/java/ru/practicum/eventhub/api/mapper/TagMapper.java @@ -7,7 +7,9 @@ import ru.practicum.eventhub.api.dto.request.TagCreateDto; import ru.practicum.eventhub.api.dto.request.TagUpdateDto; import ru.practicum.eventhub.api.dto.response.TagDto; -import ru.practicum.eventhub.domain.model.Tag; +import ru.practicum.eventhub.api.dto.response.TagStatsDto; +import ru.practicum.eventhub.api.dto.response.TagWithStatsDto; +import ru.practicum.eventhub.model.Tag; import static org.mapstruct.MappingConstants.ComponentModel.SPRING; import static org.mapstruct.NullValuePropertyMappingStrategy.IGNORE; @@ -16,6 +18,8 @@ public interface TagMapper { TagDto toDto(Tag tag); + TagWithStatsDto toDtoWithStats(Tag tag, TagStatsDto stats); + @Mapping(target = "id", ignore = true) @Mapping(target = "version", ignore = true) @Mapping(target = "createdAt", ignore = true) diff --git a/src/main/java/ru/practicum/eventhub/api/mapper/UserMapper.java b/src/main/java/ru/practicum/eventhub/api/mapper/UserMapper.java index 51de3de..8a9e471 100644 --- a/src/main/java/ru/practicum/eventhub/api/mapper/UserMapper.java +++ b/src/main/java/ru/practicum/eventhub/api/mapper/UserMapper.java @@ -6,8 +6,8 @@ import ru.practicum.eventhub.api.dto.request.UserMetadataUpdateDto; import ru.practicum.eventhub.api.dto.request.UserUpdateDto; import ru.practicum.eventhub.api.dto.response.UserDto; -import ru.practicum.eventhub.domain.model.User; -import ru.practicum.eventhub.domain.model.UserMetadata; +import ru.practicum.eventhub.model.User; +import ru.practicum.eventhub.model.UserMetadata; import static org.mapstruct.MappingConstants.ComponentModel.SPRING; import static org.mapstruct.NullValuePropertyMappingStrategy.IGNORE; diff --git a/src/main/java/ru/practicum/eventhub/api/mapper/UserMetadataMapper.java b/src/main/java/ru/practicum/eventhub/api/mapper/UserMetadataMapper.java index b618924..251954c 100644 --- a/src/main/java/ru/practicum/eventhub/api/mapper/UserMetadataMapper.java +++ b/src/main/java/ru/practicum/eventhub/api/mapper/UserMetadataMapper.java @@ -2,7 +2,7 @@ import org.mapstruct.Mapper; import ru.practicum.eventhub.api.dto.response.UserMetadataDto; -import ru.practicum.eventhub.domain.model.UserMetadata; +import ru.practicum.eventhub.model.UserMetadata; import static org.mapstruct.MappingConstants.ComponentModel.SPRING; diff --git a/src/main/java/ru/practicum/eventhub/application/CategoryApplicationService.java b/src/main/java/ru/practicum/eventhub/application/CategoryApplicationService.java deleted file mode 100644 index a651a83..0000000 --- a/src/main/java/ru/practicum/eventhub/application/CategoryApplicationService.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.practicum.eventhub.application; - -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import ru.practicum.eventhub.api.dto.response.CategoryDto; -import ru.practicum.eventhub.api.model.PageOfCategories; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.service.CategoryService; - -@Service -@RequiredArgsConstructor -public class CategoryApplicationService { - private final CategoryService categoryService; - - public PageOfCategories getCategories(Pageable pageable) { - PagedResponse page = categoryService.getCategories(pageable); - return getPageOfCategories(page); - } - - private static @NotNull PageOfCategories getPageOfCategories(PagedResponse page) { - PageOfCategories apiPage = new PageOfCategories(); - apiPage.setPage(page.page()); - apiPage.setSize(page.size()); - apiPage.setTotalElements(page.totalElements()); - apiPage.setTotalPages(page.totalPages()); - apiPage.setContent(page.content()); - return apiPage; - } -} diff --git a/src/main/java/ru/practicum/eventhub/application/EventApplicationService.java b/src/main/java/ru/practicum/eventhub/application/EventApplicationService.java deleted file mode 100644 index 10347b3..0000000 --- a/src/main/java/ru/practicum/eventhub/application/EventApplicationService.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.practicum.eventhub.application; - -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import ru.practicum.eventhub.api.dto.response.EventDto; -import ru.practicum.eventhub.api.model.PageOfEvents; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.service.EventService; - -@Service -@RequiredArgsConstructor -public class EventApplicationService { - private final EventService eventService; - - public PageOfEvents getEvents(Pageable pageable) { - PagedResponse page = eventService.getEvents(pageable); - return getPageOfEvents(page); - } - - private static @NonNull PageOfEvents getPageOfEvents(PagedResponse page) { - PageOfEvents apiPage = new PageOfEvents(); - apiPage.setPage(page.page()); - apiPage.setSize(page.size()); - apiPage.setTotalElements(page.totalElements()); - apiPage.setTotalPages(page.totalPages()); - apiPage.setContent(page.content()); - return apiPage; - } -} diff --git a/src/main/java/ru/practicum/eventhub/application/MetadataApplicationService.java b/src/main/java/ru/practicum/eventhub/application/MetadataApplicationService.java deleted file mode 100644 index 8df19fe..0000000 --- a/src/main/java/ru/practicum/eventhub/application/MetadataApplicationService.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.practicum.eventhub.application; - -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import ru.practicum.eventhub.api.dto.response.UserMetadataDto; -import ru.practicum.eventhub.api.model.PageOfMetadata; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.service.UserMetadataService; - -@Service -@RequiredArgsConstructor -public class MetadataApplicationService { - private final UserMetadataService userMetadataService; - - public PageOfMetadata getUserMetadata(Pageable pageable) { - PagedResponse page = userMetadataService.getUserMetadata(pageable); - return getUserMetadata(page); - } - - private static @NotNull PageOfMetadata getUserMetadata(PagedResponse page) { - PageOfMetadata pageOfMetadata = new PageOfMetadata(); - pageOfMetadata.setPage(page.page()); - pageOfMetadata.setSize(page.size()); - pageOfMetadata.setTotalElements(page.totalElements()); - pageOfMetadata.setTotalPages(page.totalPages()); - pageOfMetadata.setContent(page.content()); - return pageOfMetadata; - } -} diff --git a/src/main/java/ru/practicum/eventhub/application/ProjectApplicationService.java b/src/main/java/ru/practicum/eventhub/application/ProjectApplicationService.java deleted file mode 100644 index 2f9fbb1..0000000 --- a/src/main/java/ru/practicum/eventhub/application/ProjectApplicationService.java +++ /dev/null @@ -1,33 +0,0 @@ -package ru.practicum.eventhub.application; - -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import ru.practicum.eventhub.api.dto.response.ProjectDto; -import ru.practicum.eventhub.api.model.PageOfProjects; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.service.ProjectService; - -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class ProjectApplicationService { - private final ProjectService projectService; - - public PageOfProjects getProjectsByCategory(UUID categoryId, Pageable pageable) { - PagedResponse page = projectService.getProjectsByCategory(categoryId, pageable); - return getPageOfProjects(page); - } - - public static @NotNull PageOfProjects getPageOfProjects(PagedResponse page) { - PageOfProjects apiPage = new PageOfProjects(); - apiPage.setPage(page.page()); - apiPage.setSize(page.size()); - apiPage.setTotalElements(page.totalElements()); - apiPage.setTotalPages(page.totalPages()); - apiPage.setContent(page.content()); - return apiPage; - } -} diff --git a/src/main/java/ru/practicum/eventhub/application/TagApplicationService.java b/src/main/java/ru/practicum/eventhub/application/TagApplicationService.java deleted file mode 100644 index a475b60..0000000 --- a/src/main/java/ru/practicum/eventhub/application/TagApplicationService.java +++ /dev/null @@ -1,38 +0,0 @@ -package ru.practicum.eventhub.application; - -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import ru.practicum.eventhub.api.dto.response.TagDto; -import ru.practicum.eventhub.api.model.PageOfTags; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.service.TagService; - -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class TagApplicationService { - private final TagService tagService; - - public PageOfTags getTags(Pageable pageable) { - PagedResponse page = tagService.getTags(pageable); - return getPageOfTags(page); - } - - public PageOfTags getTagsByEvent(UUID eventId, Pageable pageable) { - PagedResponse page = tagService.getTagsByEvent(eventId, pageable); - return getPageOfTags(page); - } - - private static @NotNull PageOfTags getPageOfTags(PagedResponse page) { - PageOfTags apiPage = new PageOfTags(); - apiPage.setPage(page.page()); - apiPage.setSize(page.size()); - apiPage.setTotalElements(page.totalElements()); - apiPage.setTotalPages(page.totalPages()); - apiPage.setContent(page.content()); - return apiPage; - } -} diff --git a/src/main/java/ru/practicum/eventhub/application/UserApplicationService.java b/src/main/java/ru/practicum/eventhub/application/UserApplicationService.java deleted file mode 100644 index 5903a42..0000000 --- a/src/main/java/ru/practicum/eventhub/application/UserApplicationService.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.practicum.eventhub.application; - -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import ru.practicum.eventhub.api.dto.response.UserDto; -import ru.practicum.eventhub.api.model.PageOfUsers; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.service.UserService; - -@Service -@RequiredArgsConstructor -public class UserApplicationService { - private final UserService userService; - - public PageOfUsers getUsers(Pageable pageable) { - PagedResponse page = userService.getUsers(pageable); - return getPageOfUsers(page); - } - - private static @NotNull PageOfUsers getPageOfUsers(PagedResponse page) { - PageOfUsers pageOfUsers = new PageOfUsers(); - pageOfUsers.setPage(page.page()); - pageOfUsers.setSize(page.size()); - pageOfUsers.setTotalElements(page.totalElements()); - pageOfUsers.setTotalPages(page.totalPages()); - pageOfUsers.setContent(page.content()); - return pageOfUsers; - } -} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/TagAnalyticsClient.java b/src/main/java/ru/practicum/eventhub/config/feign/TagAnalyticsClient.java new file mode 100644 index 0000000..cd2f165 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/TagAnalyticsClient.java @@ -0,0 +1,32 @@ +package ru.practicum.eventhub.config.feign; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.*; +import ru.practicum.eventhub.api.dto.response.TagStatsDto; +import ru.practicum.eventhub.config.feign.config.FeignConfig; +import ru.practicum.eventhub.config.feign.fallback.TagAnalyticsFallbackFactory; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +@FeignClient( + name = "tag-analytics", + url = "${services.tag-analytics.url}", + fallbackFactory = TagAnalyticsFallbackFactory.class, + configuration = FeignConfig.class +) +public interface TagAnalyticsClient { + + @PostMapping("api/v1/tags/{id}") + TagStatsDto createTagAnalytics(@PathVariable UUID id); + + @GetMapping("/api/v1/tags/{id}/stats") + TagStatsDto getTagStats(@PathVariable UUID id); + + @PostMapping("/api/v1/tags/stats") + Map getTagStatsBatch(@RequestBody Set tagIds); + + @DeleteMapping("/api/v1/tags/{id}") + void deleteTagAnalytics(@PathVariable UUID id); +} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/TagAnalyticsFacade.java b/src/main/java/ru/practicum/eventhub/config/feign/TagAnalyticsFacade.java new file mode 100644 index 0000000..d3f1208 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/TagAnalyticsFacade.java @@ -0,0 +1,36 @@ +package ru.practicum.eventhub.config.feign; + +import io.github.resilience4j.retry.annotation.Retry; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import ru.practicum.eventhub.api.dto.response.TagStatsDto; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class TagAnalyticsFacade { + private final TagAnalyticsClient tagAnalyticsClient; + + @Retry(name = "tag-analytics") + public TagStatsDto createTagAnalytics(UUID tagId) { + return tagAnalyticsClient.createTagAnalytics(tagId); + } + + @Retry(name = "tag-analytics") + public TagStatsDto getTagStats(UUID tagId) { + return tagAnalyticsClient.getTagStats(tagId); + } + + @Retry(name = "tag-analytics") + public Map getTagStatsBatch(Set tagIds) { + return tagAnalyticsClient.getTagStatsBatch(tagIds); + } + + @Retry(name = "tag-analytics") + public void deleteTagAnalytics(UUID tagId) { + tagAnalyticsClient.deleteTagAnalytics(tagId); + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/config/FeignConfig.java b/src/main/java/ru/practicum/eventhub/config/feign/config/FeignConfig.java new file mode 100644 index 0000000..dd3611a --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/config/FeignConfig.java @@ -0,0 +1,15 @@ +package ru.practicum.eventhub.config.feign.config; + +import feign.codec.ErrorDecoder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.practicum.eventhub.config.feign.decoder.TagAnalyticsErrorDecoder; + +@Configuration +public class FeignConfig { + + @Bean + public ErrorDecoder errorDecoder() { + return new TagAnalyticsErrorDecoder(); + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/config/FeignRequestIdInterceptor.java b/src/main/java/ru/practicum/eventhub/config/feign/config/FeignRequestIdInterceptor.java new file mode 100644 index 0000000..f1ed51c --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/config/FeignRequestIdInterceptor.java @@ -0,0 +1,20 @@ +package ru.practicum.eventhub.config.feign.config; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +@Component +public class FeignRequestIdInterceptor implements RequestInterceptor { + private static final String REQUEST_ID_HEADER = "X-Request-Id"; + private static final String REQUEST_ID_MDC_KEY = "requestId"; + + @Override + public void apply(RequestTemplate template) { + String requestId = MDC.get(REQUEST_ID_MDC_KEY); + if (requestId != null) { + template.header(REQUEST_ID_HEADER, requestId); + } + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/decoder/TagAnalyticsErrorDecoder.java b/src/main/java/ru/practicum/eventhub/config/feign/decoder/TagAnalyticsErrorDecoder.java new file mode 100644 index 0000000..5a8b63a --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/decoder/TagAnalyticsErrorDecoder.java @@ -0,0 +1,23 @@ +package ru.practicum.eventhub.config.feign.decoder; + +import feign.Response; +import feign.codec.ErrorDecoder; +import ru.practicum.eventhub.config.feign.exception.TagAnalyticsClientException; +import ru.practicum.eventhub.config.feign.exception.TagNotFoundException; + +public class TagAnalyticsErrorDecoder implements ErrorDecoder { + private final ErrorDecoder defaultDecoder = new Default(); + + @Override + public Exception decode(String methodKey, Response response) { + + if (response.status() >= 400 && response.status() < 500) { + if (response.status() == 404) { + return new TagNotFoundException("Ресурс не найден для метода: " + methodKey); + } + return new TagAnalyticsClientException("Ошибка клиента при вызове метода: " + methodKey + " с кодом статуса: " + response.status()); + } + + return defaultDecoder.decode(methodKey, response); + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/exception/TagAnalyticsClientException.java b/src/main/java/ru/practicum/eventhub/config/feign/exception/TagAnalyticsClientException.java new file mode 100644 index 0000000..81cc91a --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/exception/TagAnalyticsClientException.java @@ -0,0 +1,7 @@ +package ru.practicum.eventhub.config.feign.exception; + +public class TagAnalyticsClientException extends RuntimeException { + public TagAnalyticsClientException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/exception/TagNotFoundException.java b/src/main/java/ru/practicum/eventhub/config/feign/exception/TagNotFoundException.java new file mode 100644 index 0000000..1686665 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/exception/TagNotFoundException.java @@ -0,0 +1,7 @@ +package ru.practicum.eventhub.config.feign.exception; + +public class TagNotFoundException extends RuntimeException { + public TagNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/feign/fallback/TagAnalyticsFallbackFactory.java b/src/main/java/ru/practicum/eventhub/config/feign/fallback/TagAnalyticsFallbackFactory.java new file mode 100644 index 0000000..0e7bc59 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/feign/fallback/TagAnalyticsFallbackFactory.java @@ -0,0 +1,93 @@ +package ru.practicum.eventhub.config.feign.fallback; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.openfeign.FallbackFactory; +import org.springframework.stereotype.Component; +import ru.practicum.eventhub.api.dto.response.TagStatsDto; +import ru.practicum.eventhub.config.feign.TagAnalyticsClient; +import ru.practicum.eventhub.config.feign.exception.TagAnalyticsClientException; +import ru.practicum.eventhub.config.feign.exception.TagNotFoundException; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +@Slf4j +@Component +public class TagAnalyticsFallbackFactory implements FallbackFactory { + + @Override + public TagAnalyticsClient create(Throwable cause) { + return new TagAnalyticsClientFallback(cause); + } + + @Slf4j + private static class TagAnalyticsClientFallback implements TagAnalyticsClient { + private final Throwable cause; + + public TagAnalyticsClientFallback(Throwable cause) { + this.cause = cause; + } + + @Override + public TagStatsDto createTagAnalytics(UUID id) { + if (cause instanceof TagAnalyticsClientException) { + log.warn("Ошибка клиента при создании статистики для тега id={}: {}", id, cause.getMessage()); + throw (TagAnalyticsClientException) cause; + } + + log.warn("Сервис Tag Analytics недоступен. Не удалось создать статистику для тега с id={}. " + + "Причина: {}", id, cause.getClass().getSimpleName(), cause); + return new TagStatsDto(0, null, null); + } + + @Override + public TagStatsDto getTagStats(UUID id) { + if (cause instanceof TagNotFoundException) { + log.debug("Тег с id={} не найден в Tag Analytics (404). Возвращаем пустую статистику.", id); + return new TagStatsDto(0, null, null); + } + + if (cause instanceof TagAnalyticsClientException) { + log.warn("Ошибка клиента при получении статистики для тега id={}: {}", id, cause.getMessage()); + throw (TagAnalyticsClientException) cause; + } + + log.warn("Сервис Tag Analytics недоступен. Не удалось получить статистику для тега с id={}. " + + "Причина: {}", id, cause.getClass().getSimpleName(), cause); + return new TagStatsDto(0, null, null); + } + + @Override + public Map getTagStatsBatch(Set tagIds) { + if (cause instanceof TagNotFoundException) { + log.debug("Некоторые теги с id={} не найдены в Tag Analytics (404).", tagIds); + throw (TagNotFoundException) cause; + } + + if (cause instanceof TagAnalyticsClientException) { + log.warn("Ошибка клиента при получении статистики для тегов id={}: {}", tagIds, cause.getMessage()); + throw (TagAnalyticsClientException) cause; + } + + log.warn("Сервис Tag Analytics недоступен. Не удалось получить статистику для тегов с id={}. " + + "Причина: {}", tagIds, cause.getClass().getSimpleName(), cause); + return Map.of(); + } + + @Override + public void deleteTagAnalytics(UUID id) { + if (cause instanceof TagNotFoundException) { + log.debug("Тег с id={} не найден в Tag Analytics (404). Возвращаем пустую статистику.", id); + } + + if (cause instanceof TagAnalyticsClientException) { + log.warn("Ошибка клиента при получении статистики для тега id={}: {}", id, cause.getMessage()); + throw (TagAnalyticsClientException) cause; + } + + log.warn("Сервис Tag Analytics недоступен. Не удалось удалить статистику для тега с id={}. " + + "Причина: {}", id, cause.getClass().getSimpleName(), cause); + } + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/redis/ManyToManyCacheIndexService.java b/src/main/java/ru/practicum/eventhub/config/redis/ManyToManyCacheIndexService.java new file mode 100644 index 0000000..e22539c --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/redis/ManyToManyCacheIndexService.java @@ -0,0 +1,54 @@ +package ru.practicum.eventhub.config.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ManyToManyCacheIndexService { + private final RedisTemplate redisTemplate; + + private String leftKey(String relation, UUID leftId) { + return relation + ":left:" + leftId; + } + + private String rightKey(String relation, UUID rightId) { + return relation + ":right:" + rightId; + } + + public void add(String relation, UUID leftId, UUID rightId) { + redisTemplate.opsForSet().add(leftKey(relation, leftId), rightId.toString()); + redisTemplate.opsForSet().add(rightKey(relation, rightId), leftId.toString()); + } + + public void remove(String relation, UUID leftId, UUID rightId) { + redisTemplate.opsForSet().remove(leftKey(relation, leftId), rightId.toString()); + redisTemplate.opsForSet().remove(rightKey(relation, rightId), leftId.toString()); + } + + public Set getRightIds(String relation, UUID leftId) { + Set ids = redisTemplate.opsForSet().members(leftKey(relation, leftId)); + if (ids == null) return Collections.emptySet(); + return ids.stream().map(UUID::fromString).collect(Collectors.toSet()); + } + + public Set getLeftIds(String relation, UUID rightId) { + Set ids = redisTemplate.opsForSet().members(rightKey(relation, rightId)); + if (ids == null) return Collections.emptySet(); + return ids.stream().map(UUID::fromString).collect(Collectors.toSet()); + } + + public void deleteLeft(String relation, UUID leftId) { + redisTemplate.delete(leftKey(relation, leftId)); + } + + public void deleteRight(String relation, UUID rightId) { + redisTemplate.delete(rightKey(relation, rightId)); + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/redis/ProjectCacheIndexService.java b/src/main/java/ru/practicum/eventhub/config/redis/ProjectCacheIndexService.java new file mode 100644 index 0000000..3eaa25d --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/redis/ProjectCacheIndexService.java @@ -0,0 +1,38 @@ +package ru.practicum.eventhub.config.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Set; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class ProjectCacheIndexService { + private final RedisTemplate redisTemplate; + private static final String CATEGORY_PROJECTS_PREFIX = "category_projects:"; + + private String buildKey(UUID categoryId) { + return CATEGORY_PROJECTS_PREFIX + categoryId; + } + + public void addProject(UUID categoryId, UUID projectId) { + redisTemplate.opsForSet() + .add(buildKey(categoryId), projectId.toString()); + } + + public void removeProject(UUID categoryId, UUID projectId) { + redisTemplate.opsForSet() + .remove(buildKey(categoryId), projectId.toString()); + } + + public Set getProjects(UUID categoryId) { + return redisTemplate.opsForSet() + .members(buildKey(categoryId)); + } + + public void deleteCategoryIndex(UUID categoryId) { + redisTemplate.delete(buildKey(categoryId)); + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/redis/RedisCacheService.java b/src/main/java/ru/practicum/eventhub/config/redis/RedisCacheService.java new file mode 100644 index 0000000..10bb152 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/redis/RedisCacheService.java @@ -0,0 +1,23 @@ +package ru.practicum.eventhub.config.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +@Component +@RequiredArgsConstructor +public class RedisCacheService { + private final CacheManager cacheManager; + + public void evictAll(String cacheName, Collection keys) { + Cache cache = cacheManager.getCache(cacheName); + if (cache != null) { + for (Object key : keys) { + cache.evict(key); + } + } + } +} diff --git a/src/main/java/ru/practicum/eventhub/config/redis/config/RedisCacheConfig.java b/src/main/java/ru/practicum/eventhub/config/redis/config/RedisCacheConfig.java new file mode 100644 index 0000000..f971d33 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/config/redis/config/RedisCacheConfig.java @@ -0,0 +1,30 @@ +package ru.practicum.eventhub.config.redis.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +public class RedisCacheConfig { + + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { + RedisCacheConfiguration config = RedisCacheConfiguration + .defaultCacheConfig() + .entryTtl(Duration.ofHours(1)) + .disableCachingNullValues() + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .build(); + } +} diff --git a/src/main/java/ru/practicum/eventhub/controller/CategoryProjectController.java b/src/main/java/ru/practicum/eventhub/controller/CategoryProjectController.java index bc6d125..e9552c3 100644 --- a/src/main/java/ru/practicum/eventhub/controller/CategoryProjectController.java +++ b/src/main/java/ru/practicum/eventhub/controller/CategoryProjectController.java @@ -13,18 +13,14 @@ import ru.practicum.eventhub.api.dto.response.ProjectDto; import ru.practicum.eventhub.api.model.PageOfCategories; import ru.practicum.eventhub.api.model.PageOfProjects; -import ru.practicum.eventhub.application.CategoryApplicationService; -import ru.practicum.eventhub.application.ProjectApplicationService; -import ru.practicum.eventhub.domain.service.CategoryService; -import ru.practicum.eventhub.domain.service.ProjectService; +import ru.practicum.eventhub.service.CategoryService; +import ru.practicum.eventhub.service.ProjectService; import java.util.UUID; @RestController @RequiredArgsConstructor public class CategoryProjectController implements CategoriesApi { - private final CategoryApplicationService categoryApplicationService; - private final ProjectApplicationService projectApplicationService; private final CategoryService categoryService; private final ProjectService projectService; @@ -54,7 +50,7 @@ public ResponseEntity deleteProjectForCategory(UUID categoryId, UUID proje @Override public ResponseEntity getCategories(Integer page, Integer size) { - PageOfCategories pageOfCategories = categoryApplicationService.getCategories(PageRequest.of(page, size)); + PageOfCategories pageOfCategories = categoryService.getCategories(PageRequest.of(page, size)); return ResponseEntity.ok(pageOfCategories); } @@ -72,7 +68,7 @@ public ResponseEntity getProjectByCategoryId(UUID categoryId, UUID p @Override public ResponseEntity getProjectsByCategoryId(UUID categoryId, Integer page, Integer size) { - PageOfProjects pageOfProjects = projectApplicationService.getProjectsByCategory(categoryId, PageRequest.of(page, size)); + PageOfProjects pageOfProjects = projectService.getProjectsByCategory(categoryId, PageRequest.of(page, size)); return ResponseEntity.ok(pageOfProjects); } diff --git a/src/main/java/ru/practicum/eventhub/controller/EventTagController.java b/src/main/java/ru/practicum/eventhub/controller/EventTagController.java index ea38f19..c62d9a4 100644 --- a/src/main/java/ru/practicum/eventhub/controller/EventTagController.java +++ b/src/main/java/ru/practicum/eventhub/controller/EventTagController.java @@ -12,29 +12,26 @@ import ru.practicum.eventhub.api.dto.request.TagUpdateDto; import ru.practicum.eventhub.api.dto.response.EventDto; import ru.practicum.eventhub.api.dto.response.TagDto; +import ru.practicum.eventhub.api.dto.response.TagWithStatsDto; import ru.practicum.eventhub.api.model.PageOfEvents; import ru.practicum.eventhub.api.model.PageOfTags; -import ru.practicum.eventhub.application.EventApplicationService; -import ru.practicum.eventhub.application.TagApplicationService; -import ru.practicum.eventhub.domain.service.EventService; -import ru.practicum.eventhub.domain.service.TagService; +import ru.practicum.eventhub.service.EventService; +import ru.practicum.eventhub.service.TagService; import java.util.UUID; @RestController @RequiredArgsConstructor public class EventTagController implements EventsApi { - private final EventApplicationService eventApplicationService; - private final TagApplicationService tagApplicationService; private final EventService eventService; private final TagService tagService; @Override - public ResponseEntity addTagToEvent(UUID eventId, UUID tagId) { - TagDto tagDto = tagService.addTagToEvent(eventId, tagId); + public ResponseEntity addTagToEvent(UUID eventId, UUID tagId) { + TagWithStatsDto tagWithStatsDto = tagService.addTagToEvent(eventId, tagId); return ResponseEntity .status(HttpStatus.CREATED) - .body(tagDto); + .body(tagWithStatsDto); } @Override @@ -85,25 +82,25 @@ public ResponseEntity getEventById(UUID eventId) { @Override public ResponseEntity getEvents(Integer page, Integer size) { - PageOfEvents pageOfEvents = eventApplicationService.getEvents(PageRequest.of(page, size)); + PageOfEvents pageOfEvents = eventService.getEvents(PageRequest.of(page, size)); return ResponseEntity.ok(pageOfEvents); } @Override - public ResponseEntity getTagByEventId(UUID eventId, UUID tagId) { - TagDto tagDto = tagService.getTagByEvent(eventId, tagId); + public ResponseEntity getTagByEventId(UUID eventId, UUID tagId) { + TagWithStatsDto tagDto = tagService.getTagByEvent(eventId, tagId); return ResponseEntity.ok(tagDto); } @Override public ResponseEntity getTags(Integer page, Integer size) { - PageOfTags pageOfTags = tagApplicationService.getTags(PageRequest.of(page, size)); + PageOfTags pageOfTags = tagService.getTags(PageRequest.of(page, size)); return ResponseEntity.ok(pageOfTags); } @Override public ResponseEntity getTagsByEventId(UUID eventId, Integer page, Integer size) { - PageOfTags pageOfTags = tagApplicationService.getTagsByEvent(eventId, PageRequest.of(page, size)); + PageOfTags pageOfTags = tagService.getTagsByEvent(eventId, PageRequest.of(page, size)); return ResponseEntity.ok(pageOfTags); } diff --git a/src/main/java/ru/practicum/eventhub/controller/UserController.java b/src/main/java/ru/practicum/eventhub/controller/UserController.java index 71d2edb..adf004b 100644 --- a/src/main/java/ru/practicum/eventhub/controller/UserController.java +++ b/src/main/java/ru/practicum/eventhub/controller/UserController.java @@ -12,18 +12,14 @@ import ru.practicum.eventhub.api.dto.response.UserMetadataDto; import ru.practicum.eventhub.api.model.PageOfMetadata; import ru.practicum.eventhub.api.model.PageOfUsers; -import ru.practicum.eventhub.application.MetadataApplicationService; -import ru.practicum.eventhub.application.UserApplicationService; -import ru.practicum.eventhub.domain.service.UserMetadataService; -import ru.practicum.eventhub.domain.service.UserService; +import ru.practicum.eventhub.service.UserMetadataService; +import ru.practicum.eventhub.service.UserService; import java.util.UUID; @RestController @RequiredArgsConstructor public class UserController implements UsersApi { - private final UserApplicationService userApplicationService; - private final MetadataApplicationService metadataApplicationService; private final UserService userService; private final UserMetadataService userMetadataService; @@ -51,7 +47,7 @@ public ResponseEntity getUserById(UUID userId) { @Override public ResponseEntity getUserMetadata(Integer page, Integer size) { - PageOfMetadata pageOfUsers = metadataApplicationService.getUserMetadata(PageRequest.of(page, size)); + PageOfMetadata pageOfUsers = userMetadataService.getUserMetadata(PageRequest.of(page, size)); return ResponseEntity.ok(pageOfUsers); } @@ -63,7 +59,7 @@ public ResponseEntity getUserMetadataById(UUID userId) { @Override public ResponseEntity getUsers(Integer page, Integer size) { - PageOfUsers pageOfUsers = userApplicationService.getUsers(PageRequest.of(page, size)); + PageOfUsers pageOfUsers = userService.getUsers(PageRequest.of(page, size)); return ResponseEntity.ok(pageOfUsers); } diff --git a/src/main/java/ru/practicum/eventhub/controller/advice/GlobalControllerAdvice.java b/src/main/java/ru/practicum/eventhub/controller/advice/GlobalControllerAdvice.java index 160ce11..f4a1822 100644 --- a/src/main/java/ru/practicum/eventhub/controller/advice/GlobalControllerAdvice.java +++ b/src/main/java/ru/practicum/eventhub/controller/advice/GlobalControllerAdvice.java @@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.MessageSourceResolvable; import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -14,11 +15,14 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.HandlerMethodValidationException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; import ru.practicum.eventhub.api.exception.BadRequestException; import ru.practicum.eventhub.api.exception.ConflictException; import ru.practicum.eventhub.api.exception.ErrorResponse; import ru.practicum.eventhub.api.exception.ForbiddenException; +import ru.practicum.eventhub.config.feign.exception.TagAnalyticsClientException; +import ru.practicum.eventhub.config.feign.exception.TagNotFoundException; import java.util.stream.Collectors; @@ -29,25 +33,43 @@ public class GlobalControllerAdvice { @ExceptionHandler(EntityNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFoundException(Exception exception, HttpServletRequest request) { - return buildResponse(HttpStatus.NOT_FOUND, exception, request); + return buildResponse(HttpStatus.NOT_FOUND, exception.getMessage(), exception, request); } @ExceptionHandler(BadRequestException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleBadRequestException(BadRequestException exception, HttpServletRequest request) { - return buildResponse(HttpStatus.BAD_REQUEST, exception, request); + return buildResponse(HttpStatus.BAD_REQUEST, exception.getMessage(), exception, request); } @ExceptionHandler(ForbiddenException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ErrorResponse handleForbiddenException(ForbiddenException exception, HttpServletRequest request) { - return buildResponse(HttpStatus.FORBIDDEN, exception, request); + return buildResponse(HttpStatus.FORBIDDEN, exception.getMessage(), exception, request); } @ExceptionHandler(ConflictException.class) @ResponseStatus(HttpStatus.CONFLICT) public ErrorResponse handleConflictException(ConflictException exception, HttpServletRequest request) { - return buildResponse(HttpStatus.CONFLICT, exception, request); + return buildResponse(HttpStatus.CONFLICT, exception.getMessage(), exception, request); + } + + @ExceptionHandler(TagNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleTagNotFoundException(TagNotFoundException exception, HttpServletRequest request) { + return buildResponse(HttpStatus.NOT_FOUND, exception.getMessage(), exception, request); + } + + @ExceptionHandler(TagAnalyticsClientException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleTagAnalyticsClientException(TagAnalyticsClientException exception, HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, exception.getMessage(), exception, request); + } + + @ExceptionHandler({IllegalArgumentException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleBadInput(Exception exception, HttpServletRequest request) { + return buildResponse(HttpStatus.BAD_REQUEST, exception.getMessage(), exception, request); } @ExceptionHandler(ConstraintViolationException.class) @@ -58,7 +80,7 @@ public ErrorResponse handleConstraintViolation(ConstraintViolationException ex, .collect(Collectors.joining("; ")); String message = violations.isEmpty() ? "Constraint violation" : violations; - return buildResponse(HttpStatus.BAD_REQUEST, message, null, request); + return buildResponse(HttpStatus.BAD_REQUEST, message, ex, request); } @ExceptionHandler(MethodArgumentNotValidException.class) @@ -70,7 +92,7 @@ public ErrorResponse handleMethodArgumentNotValid(MethodArgumentNotValidExceptio String message = validationErrors.isEmpty() ? "Validation failed" : validationErrors; - return buildResponse(HttpStatus.BAD_REQUEST, message, null, request); + return buildResponse(HttpStatus.BAD_REQUEST, message, ex, request); } @ExceptionHandler(HandlerMethodValidationException.class) @@ -82,41 +104,37 @@ public ErrorResponse handleHandlerMethodValidation(HandlerMethodValidationExcept String message = validationErrors.isEmpty() ? "Validation failed" : validationErrors; - return buildResponse(HttpStatus.BAD_REQUEST, message, null, request); + return buildResponse(HttpStatus.BAD_REQUEST, message, ex, request); } @ExceptionHandler(ServletRequestBindingException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleServletRequestBindingException(ServletRequestBindingException ex, HttpServletRequest request) { - return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), null, request); + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) public ErrorResponse handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) { - return buildResponse(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), null, request); + return buildResponse(HttpStatus.METHOD_NOT_ALLOWED, ex.getMessage(), ex, request); } @ExceptionHandler(HttpMediaTypeNotSupportedException.class) @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE) public ErrorResponse handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpServletRequest request) { - return buildResponse(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getMessage(), null, request); + return buildResponse(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getMessage(), ex, request); } @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request) { - return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), null, request); + return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage(), ex, request); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleUnexpected(Exception exception, HttpServletRequest request) { - return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception, request); - } - - private ErrorResponse buildResponse(HttpStatus httpStatus, Exception exception, HttpServletRequest request) { - return buildResponse(httpStatus, exception.getMessage(), exception, request); + return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Внутренняя ошибка сервиса", exception, request); } private ErrorResponse buildResponse(HttpStatus httpStatus, String message, Exception exception, HttpServletRequest request) { diff --git a/src/main/java/ru/practicum/eventhub/domain/dto/PagedResponse.java b/src/main/java/ru/practicum/eventhub/domain/dto/PagedResponse.java deleted file mode 100644 index 6be2f7e..0000000 --- a/src/main/java/ru/practicum/eventhub/domain/dto/PagedResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.practicum.eventhub.domain.dto; - -import org.springframework.data.domain.Page; - -import java.util.List; -import java.util.function.Function; - -public record PagedResponse(List content, int page, int size, long totalElements, int totalPages) { - - public static PagedResponse from(Page page, Function mapper) { - return new PagedResponse<>( - page.getContent().stream().map(mapper).toList(), - page.getNumber(), - page.getSize(), - page.getTotalElements(), - page.getTotalPages() - ); - } -} diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/EventServiceImpl.java b/src/main/java/ru/practicum/eventhub/domain/service/impl/EventServiceImpl.java deleted file mode 100644 index f6f5a3c..0000000 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/EventServiceImpl.java +++ /dev/null @@ -1,82 +0,0 @@ -package ru.practicum.eventhub.domain.service.impl; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import ru.practicum.eventhub.api.dto.request.EventCreateDto; -import ru.practicum.eventhub.api.dto.request.EventUpdateDto; -import ru.practicum.eventhub.api.dto.response.EventDto; -import ru.practicum.eventhub.api.mapper.EventMapper; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.model.Event; -import ru.practicum.eventhub.domain.repository.EventRepository; -import ru.practicum.eventhub.domain.service.EventService; -import ru.practicum.eventhub.domain.util.PageValidator; -import ru.practicum.eventhub.domain.validation.EventValidationService; - -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -public class EventServiceImpl implements EventService { - private final EventRepository eventRepository; - private final EventReadService eventReadService; - private final EventMapper eventMapper; - private final EventValidationService eventValidationService; - - @Override - @Transactional - public EventDto createEvent(EventCreateDto dto) { - eventValidationService.validateCreate(dto); - - Event event = eventMapper.fromCreateDto(dto); - - event = eventRepository.save(event); - log.info("Создано событие с id={}", event.getId()); - return eventMapper.toDto(event); - } - - @Override - @Transactional(readOnly = true) - public PagedResponse getEvents(Pageable pageable) { - Page page = eventRepository.findAll(pageable); - PageValidator.validatePage(page); - - log.info("Запрошены события: страница {}, размер {}", pageable.getPageNumber(), pageable.getPageSize()); - return PagedResponse.from(page, eventMapper::toDto); - } - - @Override - @Transactional(readOnly = true) - public EventDto getEventById(UUID id) { - Event event = eventReadService.findById(id); - log.info("Запрошено событие с id={}", id); - return eventMapper.toDto(event); - } - - @Override - @Transactional - public EventDto updateEvent(UUID id, EventUpdateDto dto) { - Event event = eventReadService.findByIdForUpdate(id); - - eventValidationService.validateUpdate(dto); - - event = eventMapper.updateEventFromDto(dto, event); - - event = eventRepository.save(event); - log.info("Обновлено событие с id={}", id); - return eventMapper.toDto(event); - } - - @Override - @Transactional - public void deleteEvent(UUID id) { - eventReadService.findById(id); - eventRepository.deleteById(id); - log.info("Удалено событие с id={}", id); - } -} diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/TagServiceImpl.java b/src/main/java/ru/practicum/eventhub/domain/service/impl/TagServiceImpl.java deleted file mode 100644 index 868a14b..0000000 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/TagServiceImpl.java +++ /dev/null @@ -1,132 +0,0 @@ -package ru.practicum.eventhub.domain.service.impl; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import ru.practicum.eventhub.api.dto.request.TagCreateDto; -import ru.practicum.eventhub.api.dto.request.TagUpdateDto; -import ru.practicum.eventhub.api.dto.response.TagDto; -import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.api.mapper.TagMapper; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.model.Event; -import ru.practicum.eventhub.domain.model.Tag; -import ru.practicum.eventhub.domain.repository.TagRepository; -import ru.practicum.eventhub.domain.service.TagService; -import ru.practicum.eventhub.domain.util.PageValidator; -import ru.practicum.eventhub.domain.validation.TagValidationService; - -import java.util.Iterator; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -public class TagServiceImpl implements TagService { - private final TagRepository tagRepository; - private final TagMapper tagMapper; - private final TagReadService tagReadService; - private final EventReadService eventReadService; - private final TagValidationService tagValidationService; - - @Override - @Transactional - public TagDto createTag(TagCreateDto dto) { - tagValidationService.validateCreate(dto); - Tag tag = tagMapper.fromCreateDto(dto); - - tag = tagRepository.save(tag); - log.info("Создан тег с id={}", tag.getId()); - return tagMapper.toDto(tag); - } - - @Override - @Transactional - public TagDto addTagToEvent(UUID eventId, UUID tagId) { - Event event = eventReadService.findById(eventId); - Tag tag = tagReadService.findById(tagId); - - if (tagReadService.existsByIdAndEventsId(tagId, eventId)) { - log.error("Тег с id={} уже добавлен к событию с id={}", tagId, eventId); - throw new ConflictException("Тег с id=" + tagId + " уже добавлен к событию с id=" + eventId); - } - - event.addTag(tag); - log.info("Тег с id={} добавлен к событию с id={}", tagId, eventId); - return tagMapper.toDto(tag); - } - - @Override - @Transactional(readOnly = true) - public PagedResponse getTags(Pageable pageable) { - Page page = tagRepository.findAll(pageable); - PageValidator.validatePage(page); - - log.info("Запрошены теги: страница {}, размер {}", pageable.getPageNumber(), pageable.getPageSize()); - return PagedResponse.from(page, tagMapper::toDto); - } - - @Override - @Transactional(readOnly = true) - public PagedResponse getTagsByEvent(UUID eventId, Pageable pageable) { - eventReadService.findById(eventId); - Page page = tagRepository.findAllByEventsId(eventId, pageable); - PageValidator.validatePage(page); - - log.info("Запрошены теги для события с id={}: страница {}, размер {}", - eventId, pageable.getPageNumber(), pageable.getPageSize()); - return PagedResponse.from(page, tagMapper::toDto); - } - - @Override - @Transactional(readOnly = true) - public TagDto getTagByEvent(UUID eventId, UUID tagId) { - eventReadService.findById(eventId); - Tag tag = tagReadService.findByIdAndEventsId(tagId, eventId); - log.info("Запрошен тег с id={} для события с id={}", tagId, eventId); - return tagMapper.toDto(tag); - } - - @Override - @Transactional - public TagDto updateTag(UUID tagId, TagUpdateDto dto) { - Tag tag = tagReadService.findById(tagId); - - tagValidationService.validateUpdate(dto, tag); - - tag = tagMapper.updateTagFromDto(dto, tag); - - tag = tagRepository.save(tag); - log.info("Обновлен тег с id={}", tagId); - return tagMapper.toDto(tag); - } - - @Override - @Transactional - public void deleteForEvent(UUID eventId, UUID tagId) { - Event event = eventReadService.findById(eventId); - Tag tag = tagReadService.findByIdAndEventsId(tagId, eventId); - - event.removeTag(tag); - log.info("Тег с id={} отвязан от события с id={}", tagId, eventId); - } - - @Override - @Transactional - public void deleteTag(UUID tagId) { - Tag tag = tagReadService.findById(tagId); - - Iterator iterator = tag.getEvents().iterator(); - while (iterator.hasNext()) { - Event event = iterator.next(); - iterator.remove(); - event.getTags().remove(tag); - } - - tagRepository.deleteById(tagId); - log.info("Удален тег с id={}", tagId); - } -} diff --git a/src/main/java/ru/practicum/eventhub/model/ActionType.java b/src/main/java/ru/practicum/eventhub/model/ActionType.java new file mode 100644 index 0000000..19a04ad --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/model/ActionType.java @@ -0,0 +1,5 @@ +package ru.practicum.eventhub.model; + +public enum ActionType { + DELETE_ANALYTICS +} diff --git a/src/main/java/ru/practicum/eventhub/domain/model/Category.java b/src/main/java/ru/practicum/eventhub/model/Category.java similarity index 96% rename from src/main/java/ru/practicum/eventhub/domain/model/Category.java rename to src/main/java/ru/practicum/eventhub/model/Category.java index d74d94a..070db6e 100644 --- a/src/main/java/ru/practicum/eventhub/domain/model/Category.java +++ b/src/main/java/ru/practicum/eventhub/model/Category.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.model; +package ru.practicum.eventhub.model; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; diff --git a/src/main/java/ru/practicum/eventhub/model/CompensationAction.java b/src/main/java/ru/practicum/eventhub/model/CompensationAction.java new file mode 100644 index 0000000..bd65c7a --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/model/CompensationAction.java @@ -0,0 +1,40 @@ +package ru.practicum.eventhub.model; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "compensation_actions") +@Getter +@Setter +@EqualsAndHashCode(of = "id") +public class CompensationAction { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(nullable = false, length = 50) + @Enumerated(EnumType.STRING) + private ActionType actionType; + + @Column(nullable = false) + private String payload; + + private int retries = 0; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private OffsetDateTime createdAt; + + @UpdateTimestamp + @Column(name = "last_attempt") + private OffsetDateTime lastAttempt; +} diff --git a/src/main/java/ru/practicum/eventhub/domain/model/Event.java b/src/main/java/ru/practicum/eventhub/model/Event.java similarity index 97% rename from src/main/java/ru/practicum/eventhub/domain/model/Event.java rename to src/main/java/ru/practicum/eventhub/model/Event.java index fea01cd..5f761e2 100644 --- a/src/main/java/ru/practicum/eventhub/domain/model/Event.java +++ b/src/main/java/ru/practicum/eventhub/model/Event.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.model; +package ru.practicum.eventhub.model; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; diff --git a/src/main/java/ru/practicum/eventhub/domain/model/Project.java b/src/main/java/ru/practicum/eventhub/model/Project.java similarity index 95% rename from src/main/java/ru/practicum/eventhub/domain/model/Project.java rename to src/main/java/ru/practicum/eventhub/model/Project.java index 488bdc8..f9f6f54 100644 --- a/src/main/java/ru/practicum/eventhub/domain/model/Project.java +++ b/src/main/java/ru/practicum/eventhub/model/Project.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.model; +package ru.practicum.eventhub.model; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; diff --git a/src/main/java/ru/practicum/eventhub/domain/model/Tag.java b/src/main/java/ru/practicum/eventhub/model/Tag.java similarity index 95% rename from src/main/java/ru/practicum/eventhub/domain/model/Tag.java rename to src/main/java/ru/practicum/eventhub/model/Tag.java index 76a7c72..88763ce 100644 --- a/src/main/java/ru/practicum/eventhub/domain/model/Tag.java +++ b/src/main/java/ru/practicum/eventhub/model/Tag.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.model; +package ru.practicum.eventhub.model; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; diff --git a/src/main/java/ru/practicum/eventhub/domain/model/User.java b/src/main/java/ru/practicum/eventhub/model/User.java similarity index 96% rename from src/main/java/ru/practicum/eventhub/domain/model/User.java rename to src/main/java/ru/practicum/eventhub/model/User.java index d269c9d..777a7b9 100644 --- a/src/main/java/ru/practicum/eventhub/domain/model/User.java +++ b/src/main/java/ru/practicum/eventhub/model/User.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.model; +package ru.practicum.eventhub.model; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; diff --git a/src/main/java/ru/practicum/eventhub/domain/model/UserMetadata.java b/src/main/java/ru/practicum/eventhub/model/UserMetadata.java similarity index 96% rename from src/main/java/ru/practicum/eventhub/domain/model/UserMetadata.java rename to src/main/java/ru/practicum/eventhub/model/UserMetadata.java index abb7163..b2af67d 100644 --- a/src/main/java/ru/practicum/eventhub/domain/model/UserMetadata.java +++ b/src/main/java/ru/practicum/eventhub/model/UserMetadata.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.model; +package ru.practicum.eventhub.model; import jakarta.persistence.*; import lombok.EqualsAndHashCode; diff --git a/src/main/java/ru/practicum/eventhub/domain/repository/CategoryRepository.java b/src/main/java/ru/practicum/eventhub/repository/CategoryRepository.java similarity index 89% rename from src/main/java/ru/practicum/eventhub/domain/repository/CategoryRepository.java rename to src/main/java/ru/practicum/eventhub/repository/CategoryRepository.java index 6d44c27..fd82312 100644 --- a/src/main/java/ru/practicum/eventhub/domain/repository/CategoryRepository.java +++ b/src/main/java/ru/practicum/eventhub/repository/CategoryRepository.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.repository; +package ru.practicum.eventhub.repository; import jakarta.persistence.LockModeType; import lombok.NonNull; @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import ru.practicum.eventhub.domain.model.Category; +import ru.practicum.eventhub.model.Category; import java.util.Optional; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/repository/CompensationActionRepository.java b/src/main/java/ru/practicum/eventhub/repository/CompensationActionRepository.java new file mode 100644 index 0000000..97425f1 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/repository/CompensationActionRepository.java @@ -0,0 +1,9 @@ +package ru.practicum.eventhub.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.practicum.eventhub.model.CompensationAction; + +import java.util.UUID; + +public interface CompensationActionRepository extends JpaRepository { +} diff --git a/src/main/java/ru/practicum/eventhub/domain/repository/EventRepository.java b/src/main/java/ru/practicum/eventhub/repository/EventRepository.java similarity index 88% rename from src/main/java/ru/practicum/eventhub/domain/repository/EventRepository.java rename to src/main/java/ru/practicum/eventhub/repository/EventRepository.java index a1e6315..eb91b68 100644 --- a/src/main/java/ru/practicum/eventhub/domain/repository/EventRepository.java +++ b/src/main/java/ru/practicum/eventhub/repository/EventRepository.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.repository; +package ru.practicum.eventhub.repository; import jakarta.persistence.LockModeType; import lombok.NonNull; @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import ru.practicum.eventhub.domain.model.Event; +import ru.practicum.eventhub.model.Event; import java.util.Optional; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/domain/repository/ProjectRepository.java b/src/main/java/ru/practicum/eventhub/repository/ProjectRepository.java similarity index 86% rename from src/main/java/ru/practicum/eventhub/domain/repository/ProjectRepository.java rename to src/main/java/ru/practicum/eventhub/repository/ProjectRepository.java index 06aa60d..f5f1e95 100644 --- a/src/main/java/ru/practicum/eventhub/domain/repository/ProjectRepository.java +++ b/src/main/java/ru/practicum/eventhub/repository/ProjectRepository.java @@ -1,10 +1,10 @@ -package ru.practicum.eventhub.domain.repository; +package ru.practicum.eventhub.repository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import ru.practicum.eventhub.domain.model.Project; +import ru.practicum.eventhub.model.Project; import java.util.Optional; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/domain/repository/TagRepository.java b/src/main/java/ru/practicum/eventhub/repository/TagRepository.java similarity index 78% rename from src/main/java/ru/practicum/eventhub/domain/repository/TagRepository.java rename to src/main/java/ru/practicum/eventhub/repository/TagRepository.java index 82a6646..deb0457 100644 --- a/src/main/java/ru/practicum/eventhub/domain/repository/TagRepository.java +++ b/src/main/java/ru/practicum/eventhub/repository/TagRepository.java @@ -1,13 +1,15 @@ -package ru.practicum.eventhub.domain.repository; +package ru.practicum.eventhub.repository; import lombok.NonNull; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import ru.practicum.eventhub.domain.model.Tag; +import ru.practicum.eventhub.model.Tag; +import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.UUID; public interface TagRepository extends JpaRepository { @@ -24,4 +26,6 @@ public interface TagRepository extends JpaRepository { boolean existsByName(String name); boolean existsByIdAndEventsId(UUID tagId, UUID eventId); + + List findAllByIdInAndEventsId(Set tagIds, UUID eventId); } diff --git a/src/main/java/ru/practicum/eventhub/domain/repository/UserMetadataRepository.java b/src/main/java/ru/practicum/eventhub/repository/UserMetadataRepository.java similarity index 85% rename from src/main/java/ru/practicum/eventhub/domain/repository/UserMetadataRepository.java rename to src/main/java/ru/practicum/eventhub/repository/UserMetadataRepository.java index 808c959..97669c2 100644 --- a/src/main/java/ru/practicum/eventhub/domain/repository/UserMetadataRepository.java +++ b/src/main/java/ru/practicum/eventhub/repository/UserMetadataRepository.java @@ -1,11 +1,11 @@ -package ru.practicum.eventhub.domain.repository; +package ru.practicum.eventhub.repository; import lombok.NonNull; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import ru.practicum.eventhub.domain.model.UserMetadata; +import ru.practicum.eventhub.model.UserMetadata; import java.util.Optional; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/domain/repository/UserRepository.java b/src/main/java/ru/practicum/eventhub/repository/UserRepository.java similarity index 85% rename from src/main/java/ru/practicum/eventhub/domain/repository/UserRepository.java rename to src/main/java/ru/practicum/eventhub/repository/UserRepository.java index 7cbba61..6ad044d 100644 --- a/src/main/java/ru/practicum/eventhub/domain/repository/UserRepository.java +++ b/src/main/java/ru/practicum/eventhub/repository/UserRepository.java @@ -1,10 +1,10 @@ -package ru.practicum.eventhub.domain.repository; +package ru.practicum.eventhub.repository; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; -import ru.practicum.eventhub.domain.model.User; +import ru.practicum.eventhub.model.User; import java.util.Optional; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/domain/service/CategoryService.java b/src/main/java/ru/practicum/eventhub/service/CategoryService.java similarity index 75% rename from src/main/java/ru/practicum/eventhub/domain/service/CategoryService.java rename to src/main/java/ru/practicum/eventhub/service/CategoryService.java index b78d201..5bd1d55 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/CategoryService.java +++ b/src/main/java/ru/practicum/eventhub/service/CategoryService.java @@ -1,17 +1,17 @@ -package ru.practicum.eventhub.domain.service; +package ru.practicum.eventhub.service; import org.springframework.data.domain.Pageable; import ru.practicum.eventhub.api.dto.request.CategoryCreateDto; import ru.practicum.eventhub.api.dto.request.CategoryUpdateDto; import ru.practicum.eventhub.api.dto.response.CategoryDto; -import ru.practicum.eventhub.domain.dto.PagedResponse; +import ru.practicum.eventhub.api.model.PageOfCategories; import java.util.UUID; public interface CategoryService { CategoryDto createCategory(CategoryCreateDto dto); - PagedResponse getCategories(Pageable pageable); + PageOfCategories getCategories(Pageable pageable); CategoryDto getCategoryById(UUID id); diff --git a/src/main/java/ru/practicum/eventhub/domain/service/EventService.java b/src/main/java/ru/practicum/eventhub/service/EventService.java similarity index 74% rename from src/main/java/ru/practicum/eventhub/domain/service/EventService.java rename to src/main/java/ru/practicum/eventhub/service/EventService.java index 6611971..4833197 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/EventService.java +++ b/src/main/java/ru/practicum/eventhub/service/EventService.java @@ -1,17 +1,17 @@ -package ru.practicum.eventhub.domain.service; +package ru.practicum.eventhub.service; import org.springframework.data.domain.Pageable; import ru.practicum.eventhub.api.dto.request.EventCreateDto; import ru.practicum.eventhub.api.dto.request.EventUpdateDto; import ru.practicum.eventhub.api.dto.response.EventDto; -import ru.practicum.eventhub.domain.dto.PagedResponse; +import ru.practicum.eventhub.api.model.PageOfEvents; import java.util.UUID; public interface EventService { EventDto createEvent(EventCreateDto dto); - PagedResponse getEvents(Pageable pageable); + PageOfEvents getEvents(Pageable pageable); EventDto getEventById(UUID id); diff --git a/src/main/java/ru/practicum/eventhub/domain/service/ProjectService.java b/src/main/java/ru/practicum/eventhub/service/ProjectService.java similarity index 70% rename from src/main/java/ru/practicum/eventhub/domain/service/ProjectService.java rename to src/main/java/ru/practicum/eventhub/service/ProjectService.java index d1efb5b..16e345c 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/ProjectService.java +++ b/src/main/java/ru/practicum/eventhub/service/ProjectService.java @@ -1,14 +1,14 @@ -package ru.practicum.eventhub.domain.service; +package ru.practicum.eventhub.service; import org.springframework.data.domain.Pageable; import ru.practicum.eventhub.api.dto.request.ProjectUpdateDto; import ru.practicum.eventhub.api.dto.response.ProjectDto; -import ru.practicum.eventhub.domain.dto.PagedResponse; +import ru.practicum.eventhub.api.model.PageOfProjects; import java.util.UUID; public interface ProjectService { - PagedResponse getProjectsByCategory(UUID categoryId, Pageable pageable); + PageOfProjects getProjectsByCategory(UUID categoryId, Pageable pageable); ProjectDto getProjectByCategory(UUID categoryId, UUID projectId); diff --git a/src/main/java/ru/practicum/eventhub/domain/service/TagService.java b/src/main/java/ru/practicum/eventhub/service/TagService.java similarity index 50% rename from src/main/java/ru/practicum/eventhub/domain/service/TagService.java rename to src/main/java/ru/practicum/eventhub/service/TagService.java index 1c83cc4..4be32b6 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/TagService.java +++ b/src/main/java/ru/practicum/eventhub/service/TagService.java @@ -1,27 +1,30 @@ -package ru.practicum.eventhub.domain.service; +package ru.practicum.eventhub.service; import org.springframework.data.domain.Pageable; import ru.practicum.eventhub.api.dto.request.TagCreateDto; import ru.practicum.eventhub.api.dto.request.TagUpdateDto; import ru.practicum.eventhub.api.dto.response.TagDto; -import ru.practicum.eventhub.domain.dto.PagedResponse; +import ru.practicum.eventhub.api.dto.response.TagWithStatsDto; +import ru.practicum.eventhub.api.model.PageOfTags; import java.util.UUID; public interface TagService { - TagDto createTag(TagCreateDto dto); + TagWithStatsDto addTagToEvent(UUID eventId, UUID tagId); - TagDto addTagToEvent(UUID eventId, UUID tagId); + TagDto createTag(TagCreateDto dto); - PagedResponse getTags(Pageable pageable); + PageOfTags getTags(Pageable pageable); - PagedResponse getTagsByEvent(UUID eventId, Pageable pageable); + PageOfTags getTagsByEvent(UUID eventId, Pageable pageable); - TagDto getTagByEvent(UUID eventId, UUID tagId); + TagWithStatsDto getTagByEvent(UUID eventId, UUID tagId); TagDto updateTag(UUID tagId, TagUpdateDto dto); void deleteForEvent(UUID eventId, UUID tagId); void deleteTag(UUID tagId); + + void cacheRefreshAfterAddTag(UUID eventId, UUID tagId); } diff --git a/src/main/java/ru/practicum/eventhub/domain/service/UserMetadataService.java b/src/main/java/ru/practicum/eventhub/service/UserMetadataService.java similarity index 56% rename from src/main/java/ru/practicum/eventhub/domain/service/UserMetadataService.java rename to src/main/java/ru/practicum/eventhub/service/UserMetadataService.java index 297efa4..65edca1 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/UserMetadataService.java +++ b/src/main/java/ru/practicum/eventhub/service/UserMetadataService.java @@ -1,13 +1,13 @@ -package ru.practicum.eventhub.domain.service; +package ru.practicum.eventhub.service; import org.springframework.data.domain.Pageable; import ru.practicum.eventhub.api.dto.response.UserMetadataDto; -import ru.practicum.eventhub.domain.dto.PagedResponse; +import ru.practicum.eventhub.api.model.PageOfMetadata; import java.util.UUID; public interface UserMetadataService { - PagedResponse getUserMetadata(Pageable pageable); + PageOfMetadata getUserMetadata(Pageable pageable); UserMetadataDto getByUserId(UUID userId); } diff --git a/src/main/java/ru/practicum/eventhub/domain/service/UserService.java b/src/main/java/ru/practicum/eventhub/service/UserService.java similarity index 75% rename from src/main/java/ru/practicum/eventhub/domain/service/UserService.java rename to src/main/java/ru/practicum/eventhub/service/UserService.java index b144f20..eb98cb2 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/UserService.java +++ b/src/main/java/ru/practicum/eventhub/service/UserService.java @@ -1,18 +1,18 @@ -package ru.practicum.eventhub.domain.service; +package ru.practicum.eventhub.service; import jakarta.validation.Valid; import org.springframework.data.domain.Pageable; import ru.practicum.eventhub.api.dto.request.UserCreateDto; import ru.practicum.eventhub.api.dto.request.UserUpdateDto; import ru.practicum.eventhub.api.dto.response.UserDto; -import ru.practicum.eventhub.domain.dto.PagedResponse; +import ru.practicum.eventhub.api.model.PageOfUsers; import java.util.UUID; public interface UserService { UserDto createUser(@Valid UserCreateDto dto); - PagedResponse getUsers(Pageable pageable); + PageOfUsers getUsers(Pageable pageable); UserDto getUserById(UUID id); diff --git a/src/main/java/ru/practicum/eventhub/service/cache/CategoryProjectCacheService.java b/src/main/java/ru/practicum/eventhub/service/cache/CategoryProjectCacheService.java new file mode 100644 index 0000000..9fa50ff --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/service/cache/CategoryProjectCacheService.java @@ -0,0 +1,32 @@ +package ru.practicum.eventhub.service.cache; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.practicum.eventhub.config.redis.ProjectCacheIndexService; +import ru.practicum.eventhub.config.redis.RedisCacheService; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CategoryProjectCacheService { + private final ProjectCacheIndexService cacheIndexService; + private final RedisCacheService cacheService; + + public void evictProjectsByCategoryId(UUID categoryId) { + Set projectIds = cacheIndexService.getProjects(categoryId); + + List keys = projectIds.stream() + .map(projectId -> categoryId + ":" + projectId) + .toList(); + + cacheService.evictAll("projects", keys); + + cacheIndexService.deleteCategoryIndex(categoryId); + log.info("Полностью очищен кэш и индекс для категории id={}", categoryId); + } +} diff --git a/src/main/java/ru/practicum/eventhub/service/cache/EventTagCacheService.java b/src/main/java/ru/practicum/eventhub/service/cache/EventTagCacheService.java new file mode 100644 index 0000000..8729340 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/service/cache/EventTagCacheService.java @@ -0,0 +1,87 @@ +package ru.practicum.eventhub.service.cache; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import ru.practicum.eventhub.api.dto.response.TagStatsDto; +import ru.practicum.eventhub.api.dto.response.TagWithStatsDto; +import ru.practicum.eventhub.api.mapper.TagMapper; +import ru.practicum.eventhub.config.feign.TagAnalyticsFacade; +import ru.practicum.eventhub.config.redis.ManyToManyCacheIndexService; +import ru.practicum.eventhub.config.redis.RedisCacheService; +import ru.practicum.eventhub.model.Tag; +import ru.practicum.eventhub.service.impl.TagReadService; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EventTagCacheService { + private static final String EVENT_TAG_RELATION = "event_tag"; + + private final TagReadService tagReadService; + private final TagMapper tagMapper; + private final ManyToManyCacheIndexService relationIndexService; + private final CacheManager cacheManager; + private final TagAnalyticsFacade tagAnalyticsFacade; + private final RedisCacheService cacheService; + + public void refreshByEventBatch(UUID eventId, Set tagIds) { + if (tagIds == null || tagIds.isEmpty()) return; + + Cache cache = cacheManager.getCache("tags_by_event"); + if (cache == null) return; + + List tags = tagReadService.findAllByIdInAndEventsId(tagIds, eventId); + + Map statsMap = tagAnalyticsFacade.getTagStatsBatch(tagIds); + + for (Tag tag : tags) { + TagStatsDto stats = statsMap.get(tag.getId()); + TagWithStatsDto dto = tagMapper.toDtoWithStats(tag, stats); + cache.put(eventId + ":" + tag.getId(), dto); + log.debug("Обновлен кеш ивента с id={} для тега с id={}", eventId, tag.getId()); + } + + log.info("Обновлен кеш ивента с id={} для всех связанных тегов", eventId); + } + + public void refreshCompositeCacheForTag(Tag tag, Set eventIds) { + if (eventIds == null || eventIds.isEmpty()) return; + + Cache cache = cacheManager.getCache("tags_by_event"); + if (cache == null) return; + + UUID tagId = tag.getId(); + TagStatsDto stats = tagAnalyticsFacade.getTagStats(tagId); + + TagWithStatsDto dto = tagMapper.toDtoWithStats(tag, stats); + + for (UUID eventId : eventIds) { + cache.put(eventId + ":" + tagId, dto); + log.debug("Обновлен кэш тега с id={} для события с id={}", tagId, eventId); + } + + log.info("Обновлен кэш тега с id={} для всех связанных событий", tagId); + } + + public void evictEventTagCache(UUID tagId) { + Set eventIds = relationIndexService.getLeftIds(EVENT_TAG_RELATION, tagId); + + List keys = eventIds.stream() + .map(eventId -> eventId + ":" + tagId) + .toList(); + + cacheService.evictAll("tags_by_event", keys); + + relationIndexService.deleteRight(EVENT_TAG_RELATION, tagId); + + log.info("Полностью очищен кэш и индекс для тега id={}", tagId); + } +} diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/CategoryReadService.java b/src/main/java/ru/practicum/eventhub/service/impl/CategoryReadService.java similarity index 86% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/CategoryReadService.java rename to src/main/java/ru/practicum/eventhub/service/impl/CategoryReadService.java index 5ace37a..da5d5fd 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/CategoryReadService.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/CategoryReadService.java @@ -1,13 +1,14 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CachePut; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.Category; -import ru.practicum.eventhub.domain.repository.CategoryRepository; +import ru.practicum.eventhub.model.Category; +import ru.practicum.eventhub.repository.CategoryRepository; import java.util.UUID; @@ -17,6 +18,7 @@ public class CategoryReadService { private final CategoryRepository categoryRepository; + @CachePut(value = "categories", key = "#id") @Transactional(readOnly = true) public Category findById(UUID id) { return categoryRepository.findById(id).orElseThrow(() -> { diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/CategoryServiceImpl.java b/src/main/java/ru/practicum/eventhub/service/impl/CategoryServiceImpl.java similarity index 64% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/CategoryServiceImpl.java rename to src/main/java/ru/practicum/eventhub/service/impl/CategoryServiceImpl.java index f86c4dc..38b68d0 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/CategoryServiceImpl.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/CategoryServiceImpl.java @@ -1,7 +1,10 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -10,12 +13,13 @@ import ru.practicum.eventhub.api.dto.request.CategoryUpdateDto; import ru.practicum.eventhub.api.dto.response.CategoryDto; import ru.practicum.eventhub.api.mapper.CategoryMapper; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.model.Category; -import ru.practicum.eventhub.domain.repository.CategoryRepository; -import ru.practicum.eventhub.domain.service.CategoryService; -import ru.practicum.eventhub.domain.util.PageValidator; -import ru.practicum.eventhub.domain.validation.CategoryValidationService; +import ru.practicum.eventhub.api.model.PageOfCategories; +import ru.practicum.eventhub.model.Category; +import ru.practicum.eventhub.repository.CategoryRepository; +import ru.practicum.eventhub.service.CategoryService; +import ru.practicum.eventhub.service.cache.CategoryProjectCacheService; +import ru.practicum.eventhub.util.PageValidator; +import ru.practicum.eventhub.validation.CategoryValidationService; import java.util.UUID; @@ -27,6 +31,7 @@ public class CategoryServiceImpl implements CategoryService { private final CategoryMapper categoryMapper; private final CategoryReadService categoryReadService; private final CategoryValidationService categoryValidationService; + private final CategoryProjectCacheService categoryProjectCacheService; @Override @Transactional @@ -42,17 +47,18 @@ public CategoryDto createCategory(CategoryCreateDto dto) { @Override @Transactional(readOnly = true) - public PagedResponse getCategories(Pageable pageable) { + public PageOfCategories getCategories(Pageable pageable) { Page page = categoryRepository.findAll(pageable); PageValidator.validatePage(page); log.info("Отправлена страница категорий: страница={}, размер={}", pageable.getPageNumber(), pageable.getPageSize()); - return PagedResponse.from(page, categoryMapper::toDto); + return toApiPage(page, categoryMapper); } @Override @Transactional(readOnly = true) + @Cacheable(value = "categories", key = "#id") public CategoryDto getCategoryById(UUID id) { Category category = categoryReadService.findById(id); log.info("Отправлена категория c id={}", id); @@ -61,6 +67,7 @@ public CategoryDto getCategoryById(UUID id) { @Override @Transactional + @CachePut(value = "categories", key = "#id") public CategoryDto updateCategory(UUID id, CategoryUpdateDto dto) { Category category = categoryReadService.findByIdForUpdate(id); @@ -75,9 +82,22 @@ public CategoryDto updateCategory(UUID id, CategoryUpdateDto dto) { @Override @Transactional + @CacheEvict(value = "categories", key = "#id") public void deleteCategory(UUID id) { categoryReadService.findById(id); categoryRepository.deleteById(id); + + categoryProjectCacheService.evictProjectsByCategoryId(id); log.info("Удалена категория с id={}", id); } + + private PageOfCategories toApiPage(Page page, CategoryMapper mapper) { + PageOfCategories apiPage = new PageOfCategories(); + apiPage.setContent(page.getContent().stream().map(mapper::toDto).toList()); + apiPage.setPageNumber(page.getNumber()); + apiPage.setPageSize(page.getSize()); + apiPage.setTotalElements(page.getTotalElements()); + apiPage.setTotalPages(page.getTotalPages()); + return apiPage; + } } diff --git a/src/main/java/ru/practicum/eventhub/service/impl/CompensationScheduler.java b/src/main/java/ru/practicum/eventhub/service/impl/CompensationScheduler.java new file mode 100644 index 0000000..8e108d9 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/service/impl/CompensationScheduler.java @@ -0,0 +1,45 @@ +package ru.practicum.eventhub.service.impl; + +import io.swagger.v3.oas.annotations.servers.Server; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.eventhub.config.feign.TagAnalyticsFacade; +import ru.practicum.eventhub.model.ActionType; +import ru.practicum.eventhub.model.CompensationAction; +import ru.practicum.eventhub.repository.CompensationActionRepository; + +import java.util.List; +import java.util.UUID; + +@Slf4j +@Server +@RequiredArgsConstructor +public class CompensationScheduler { + private final CompensationActionRepository repository; + private final TagAnalyticsFacade analyticsFacade; + + @Scheduled(fixedDelay = 180000) + @Transactional + public void processCompensations() { + List actions = repository.findAll(); + + for (CompensationAction action : actions) { + try { + if (action.getActionType() == ActionType.DELETE_ANALYTICS) { + UUID tagId = UUID.fromString(action.getPayload()); + analyticsFacade.deleteTagAnalytics(tagId); + } + + repository.delete(action); + log.info("Успешно выполнена компенсация для id={}", action.getId()); + + } catch (Exception exception) { + action.setRetries(action.getRetries() + 1); + repository.save(action); + log.warn("Повторная ошибка компенсации для id={}. Попытка №{}", action.getId(), action.getRetries()); + } + } + } +} diff --git a/src/main/java/ru/practicum/eventhub/service/impl/CompensationService.java b/src/main/java/ru/practicum/eventhub/service/impl/CompensationService.java new file mode 100644 index 0000000..0fef565 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/service/impl/CompensationService.java @@ -0,0 +1,25 @@ +package ru.practicum.eventhub.service.impl; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import ru.practicum.eventhub.model.ActionType; +import ru.practicum.eventhub.model.CompensationAction; +import ru.practicum.eventhub.repository.CompensationActionRepository; + +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class CompensationService { + private final CompensationActionRepository repository; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveForLater(UUID tagId, ActionType type) { + CompensationAction action = new CompensationAction(); + action.setPayload(tagId.toString()); + action.setActionType(type); + repository.save(action); + } +} diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/EventReadService.java b/src/main/java/ru/practicum/eventhub/service/impl/EventReadService.java similarity index 63% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/EventReadService.java rename to src/main/java/ru/practicum/eventhub/service/impl/EventReadService.java index 893309d..1ff4091 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/EventReadService.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/EventReadService.java @@ -1,12 +1,15 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CachePut; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ru.practicum.eventhub.domain.model.Event; -import ru.practicum.eventhub.domain.repository.EventRepository; +import ru.practicum.eventhub.api.dto.response.EventDto; +import ru.practicum.eventhub.api.mapper.EventMapper; +import ru.practicum.eventhub.model.Event; +import ru.practicum.eventhub.repository.EventRepository; import java.util.UUID; @@ -15,6 +18,7 @@ @RequiredArgsConstructor public class EventReadService { private final EventRepository eventRepository; + private final EventMapper eventMapper; @Transactional(readOnly = true) public Event findById(UUID id) { @@ -24,6 +28,14 @@ public Event findById(UUID id) { }); } + @CachePut(value = "events", key = "#id") + @Transactional(readOnly = true) + public EventDto findDtoById(UUID id) { + Event event = findById(id); + log.info("Обновлен кеш события с id={}", id); + return eventMapper.toDto(event); + } + @Transactional(readOnly = true) public Event findByIdForUpdate(UUID id) { return eventRepository.findByIdForUpdate(id).orElseThrow(() -> { diff --git a/src/main/java/ru/practicum/eventhub/service/impl/EventServiceImpl.java b/src/main/java/ru/practicum/eventhub/service/impl/EventServiceImpl.java new file mode 100644 index 0000000..561c9ac --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/service/impl/EventServiceImpl.java @@ -0,0 +1,127 @@ +package ru.practicum.eventhub.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import ru.practicum.eventhub.api.dto.request.EventCreateDto; +import ru.practicum.eventhub.api.dto.request.EventUpdateDto; +import ru.practicum.eventhub.api.dto.response.EventDto; +import ru.practicum.eventhub.api.mapper.EventMapper; +import ru.practicum.eventhub.api.model.PageOfEvents; +import ru.practicum.eventhub.config.redis.ManyToManyCacheIndexService; +import ru.practicum.eventhub.model.Event; +import ru.practicum.eventhub.model.Tag; +import ru.practicum.eventhub.repository.EventRepository; +import ru.practicum.eventhub.service.EventService; +import ru.practicum.eventhub.service.cache.EventTagCacheService; +import ru.practicum.eventhub.util.PageValidator; +import ru.practicum.eventhub.validation.EventValidationService; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EventServiceImpl implements EventService { + private static final String EVENT_TAG_RELATION = "event_tag"; + + private final EventRepository eventRepository; + private final EventReadService eventReadService; + private final EventMapper eventMapper; + private final EventValidationService eventValidationService; + private final ManyToManyCacheIndexService relationIndexService; + private final EventTagCacheService tagCacheService; + private final TransactionTemplate transactionTemplate; + + @Override + @Transactional + public EventDto createEvent(EventCreateDto dto) { + eventValidationService.validateCreate(dto); + + Event event = eventMapper.fromCreateDto(dto); + + event = eventRepository.save(event); + log.info("Создано событие с id={}", event.getId()); + return eventMapper.toDto(event); + } + + @Override + @Transactional(readOnly = true) + public PageOfEvents getEvents(Pageable pageable) { + Page page = eventRepository.findAll(pageable); + PageValidator.validatePage(page); + + log.info("Запрошены события: страница {}, размер {}", pageable.getPageNumber(), pageable.getPageSize()); + return toApiPage(page, eventMapper); + } + + @Override + @Transactional(readOnly = true) + @Cacheable(value = "events", key = "#id") + public EventDto getEventById(UUID id) { + Event event = eventReadService.findById(id); + log.info("Запрошено событие с id={}", id); + return eventMapper.toDto(event); + } + + @Override + @CachePut(value = "events", key = "#id") + public EventDto updateEvent(UUID id, EventUpdateDto dto) { + Map.Entry> result = transactionTemplate.execute(status -> { + Event eventEntity = eventReadService.findByIdForUpdate(id); + + eventValidationService.validateUpdate(dto); + + Set oldTagIds = eventEntity.getTags().stream().map(Tag::getId).collect(Collectors.toSet()); + eventEntity = eventMapper.updateEventFromDto(dto, eventEntity); + eventEntity = eventRepository.save(eventEntity); + + Set addedTagIds = eventEntity.getTags().stream().map(Tag::getId).collect(Collectors.toSet()); + addedTagIds.removeAll(oldTagIds); + return Map.entry(eventEntity, addedTagIds); + }); + + Event event = result.getKey(); + Set addedTagIds = result.getValue(); + + for (UUID tagId : addedTagIds) { + relationIndexService.add(EVENT_TAG_RELATION, id, tagId); + } + tagCacheService.refreshByEventBatch(id, addedTagIds); + + log.info("Обновлено событие с id={}", id); + return eventMapper.toDto(event); + } + + @Override + @Transactional + @CacheEvict(value = "events", key = "#id") + public void deleteEvent(UUID id) { + eventReadService.findById(id); + eventRepository.deleteById(id); + + relationIndexService.deleteLeft(EVENT_TAG_RELATION, id); + + log.info("Удалено событие с id={}", id); + } + + private PageOfEvents toApiPage(Page page, EventMapper mapper) { + PageOfEvents apiPage = new PageOfEvents(); + apiPage.setContent(page.getContent().stream().map(mapper::toDto).toList()); + apiPage.setPageNumber(page.getNumber()); + apiPage.setPageSize(page.getSize()); + apiPage.setTotalElements(page.getTotalElements()); + apiPage.setTotalPages(page.getTotalPages()); + return apiPage; + } +} diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/MetadataReadService.java b/src/main/java/ru/practicum/eventhub/service/impl/MetadataReadService.java similarity index 87% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/MetadataReadService.java rename to src/main/java/ru/practicum/eventhub/service/impl/MetadataReadService.java index 033eab8..62a9818 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/MetadataReadService.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/MetadataReadService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; @@ -6,8 +6,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.UserMetadata; -import ru.practicum.eventhub.domain.repository.UserMetadataRepository; +import ru.practicum.eventhub.model.UserMetadata; +import ru.practicum.eventhub.repository.UserMetadataRepository; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/ProjectReadService.java b/src/main/java/ru/practicum/eventhub/service/impl/ProjectReadService.java similarity index 88% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/ProjectReadService.java rename to src/main/java/ru/practicum/eventhub/service/impl/ProjectReadService.java index 4a2db42..1c6d301 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/ProjectReadService.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/ProjectReadService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; @@ -6,8 +6,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.Project; -import ru.practicum.eventhub.domain.repository.ProjectRepository; +import ru.practicum.eventhub.model.Project; +import ru.practicum.eventhub.repository.ProjectRepository; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/ProjectServiceImpl.java b/src/main/java/ru/practicum/eventhub/service/impl/ProjectServiceImpl.java similarity index 61% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/ProjectServiceImpl.java rename to src/main/java/ru/practicum/eventhub/service/impl/ProjectServiceImpl.java index c75c268..012c50d 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/ProjectServiceImpl.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/ProjectServiceImpl.java @@ -1,7 +1,10 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -9,13 +12,14 @@ import ru.practicum.eventhub.api.dto.request.ProjectUpdateDto; import ru.practicum.eventhub.api.dto.response.ProjectDto; import ru.practicum.eventhub.api.mapper.ProjectMapper; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.model.Category; -import ru.practicum.eventhub.domain.model.Project; -import ru.practicum.eventhub.domain.repository.ProjectRepository; -import ru.practicum.eventhub.domain.service.ProjectService; -import ru.practicum.eventhub.domain.util.PageValidator; -import ru.practicum.eventhub.domain.validation.ProjectValidationService; +import ru.practicum.eventhub.api.model.PageOfProjects; +import ru.practicum.eventhub.config.redis.ProjectCacheIndexService; +import ru.practicum.eventhub.model.Category; +import ru.practicum.eventhub.model.Project; +import ru.practicum.eventhub.repository.ProjectRepository; +import ru.practicum.eventhub.service.ProjectService; +import ru.practicum.eventhub.util.PageValidator; +import ru.practicum.eventhub.validation.ProjectValidationService; import java.util.UUID; @@ -28,31 +32,36 @@ public class ProjectServiceImpl implements ProjectService { private final ProjectReadService projectReadService; private final CategoryReadService categoryReadService; private final ProjectValidationService projectValidationService; + private final ProjectCacheIndexService cacheIndexService; @Override @Transactional(readOnly = true) - public PagedResponse getProjectsByCategory(UUID categoryId, Pageable pageable) { + public PageOfProjects getProjectsByCategory(UUID categoryId, Pageable pageable) { categoryReadService.findById(categoryId); Page page = projectRepository.findAllByCategoryId(categoryId, pageable); PageValidator.validatePage(page); log.info("Получены проекты: страница {}, размер {}", pageable.getPageNumber(), pageable.getPageSize()); - return PagedResponse.from(page, projectMapper::toDto); + return toApiPage(page, projectMapper); } @Override @Transactional(readOnly = true) + @Cacheable(value = "projects", key = "#categoryId + ':' + #projectId") public ProjectDto getProjectByCategory(UUID categoryId, UUID projectId) { categoryReadService.findById(categoryId); Project project = projectReadService.findByIdAndCategoryId(projectId, categoryId); + cacheIndexService.addProject(categoryId, projectId); + log.info("Запрошен проект с id={} в категории с id={}", projectId, categoryId); return projectMapper.toDto(project); } @Override @Transactional + @CachePut(value = "projects", key = "#categoryId + ':' + #projectId") public ProjectDto updateForCategory(UUID categoryId, UUID projectId, ProjectUpdateDto dto) { categoryReadService.findById(categoryId); Project project = projectReadService.findByIdAndCategoryId(projectId, categoryId); @@ -67,11 +76,26 @@ public ProjectDto updateForCategory(UUID categoryId, UUID projectId, ProjectUpda @Override @Transactional + @CacheEvict(value = "projects", key = "#categoryId + ':' + #projectId") public void deleteForCategory(UUID categoryId, UUID projectId) { Category category = categoryReadService.findById(categoryId); Project project = projectReadService.findByIdAndCategoryId(projectId, categoryId); category.removeProject(project); + + cacheIndexService.removeProject(categoryId, projectId); + categoryReadService.findById(categoryId); + log.info("Удален проект с id={} в категории с id={}", projectId, categoryId); } + + private PageOfProjects toApiPage(Page page, ProjectMapper mapper) { + PageOfProjects apiPage = new PageOfProjects(); + apiPage.setContent(page.getContent().stream().map(mapper::toDto).toList()); + apiPage.setPageNumber(page.getNumber()); + apiPage.setPageSize(page.getSize()); + apiPage.setTotalElements(page.getTotalElements()); + apiPage.setTotalPages(page.getTotalPages()); + return apiPage; + } } diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/TagReadService.java b/src/main/java/ru/practicum/eventhub/service/impl/TagReadService.java similarity index 67% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/TagReadService.java rename to src/main/java/ru/practicum/eventhub/service/impl/TagReadService.java index b895e35..a77a09e 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/TagReadService.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/TagReadService.java @@ -1,14 +1,19 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CachePut; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import ru.practicum.eventhub.api.dto.response.TagDto; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.Tag; -import ru.practicum.eventhub.domain.repository.TagRepository; +import ru.practicum.eventhub.api.mapper.TagMapper; +import ru.practicum.eventhub.model.Tag; +import ru.practicum.eventhub.repository.TagRepository; +import java.util.List; +import java.util.Set; import java.util.UUID; @Slf4j @@ -16,6 +21,7 @@ @RequiredArgsConstructor public class TagReadService { private final TagRepository tagRepository; + private final TagMapper tagMapper; @Transactional(readOnly = true) public Tag findById(UUID tagId) { @@ -25,6 +31,14 @@ public Tag findById(UUID tagId) { }); } + @CachePut(value = "tags", key = "#tagId") + @Transactional(readOnly = true) + public TagDto findDtoById(UUID tagId) { + Tag tag = findById(tagId); + log.info("Обновлен кеш тега с id={}", tagId); + return tagMapper.toDto(tag); + } + @Transactional(readOnly = true) public Tag findByIdAndEventsId(UUID tagId, UUID eventId) { return tagRepository.findByIdAndEventsId(tagId, eventId).orElseThrow(() -> { @@ -33,6 +47,11 @@ public Tag findByIdAndEventsId(UUID tagId, UUID eventId) { }); } + @Transactional(readOnly = true) + public List findAllByIdInAndEventsId(Set tagIds, UUID eventId) { + return tagRepository.findAllByIdInAndEventsId(tagIds, eventId); + } + @Transactional(readOnly = true) public void checkExistsByName(String name) { if (tagRepository.existsByName(name)) { diff --git a/src/main/java/ru/practicum/eventhub/service/impl/TagServiceImpl.java b/src/main/java/ru/practicum/eventhub/service/impl/TagServiceImpl.java new file mode 100644 index 0000000..335fcb0 --- /dev/null +++ b/src/main/java/ru/practicum/eventhub/service/impl/TagServiceImpl.java @@ -0,0 +1,242 @@ +package ru.practicum.eventhub.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import ru.practicum.eventhub.api.dto.request.TagCreateDto; +import ru.practicum.eventhub.api.dto.request.TagUpdateDto; +import ru.practicum.eventhub.api.dto.response.TagDto; +import ru.practicum.eventhub.api.dto.response.TagStatsDto; +import ru.practicum.eventhub.api.dto.response.TagWithStatsDto; +import ru.practicum.eventhub.api.exception.ConflictException; +import ru.practicum.eventhub.api.mapper.TagMapper; +import ru.practicum.eventhub.api.model.PageOfTags; +import ru.practicum.eventhub.config.feign.TagAnalyticsFacade; +import ru.practicum.eventhub.config.redis.ManyToManyCacheIndexService; +import ru.practicum.eventhub.model.ActionType; +import ru.practicum.eventhub.model.Event; +import ru.practicum.eventhub.model.Tag; +import ru.practicum.eventhub.repository.TagRepository; +import ru.practicum.eventhub.service.TagService; +import ru.practicum.eventhub.service.cache.EventTagCacheService; +import ru.practicum.eventhub.util.PageValidator; +import ru.practicum.eventhub.validation.TagValidationService; + +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TagServiceImpl implements TagService { + private static final String EVENT_TAG_RELATION = "event_tag"; + + private final TagRepository tagRepository; + private final TagMapper tagMapper; + private final TagReadService tagReadService; + private final EventReadService eventReadService; + private final TagValidationService tagValidationService; + private final TagAnalyticsFacade tagAnalyticsFacade; + private final EventTagCacheService eventTagCacheService; + private final ManyToManyCacheIndexService relationIndexService; + private final TransactionTemplate transactionTemplate; + private final CompensationService compensationService; + + @Override + @Transactional + public TagWithStatsDto addTagToEvent(UUID eventId, UUID tagId) { + boolean tagAdded = false; + boolean incrementUsage = false; + + try { + Tag tag = addTagLocal(eventId, tagId); + tagAdded = true; + + TagStatsDto tagStatsDto = tagAnalyticsFacade.createTagAnalytics(tagId); + incrementUsage = true; + + cacheRefreshAfterAddTag(eventId, tagId); + + log.info("Тег с id={} добавлен к событию с id={}", tagId, eventId); + return tagMapper.toDtoWithStats(tag, tagStatsDto); + + } catch (Exception ex) { + log.error("Ошибка при добавлении тега с id={} к событию с id={}: {}", tagId, eventId, ex.getMessage()); + + if (incrementUsage) { + try { + tagAnalyticsFacade.deleteTagAnalytics(tagId); + } catch (Exception exception) { + log.error("Не удалось откатить аналитику сразу, сохраняем в Scheduler"); + compensationService.saveForLater(tagId, ActionType.DELETE_ANALYTICS); + } + } + + if (tagAdded) { + removeTagLocal(eventId, tagId); + } + + throw ex; + } + } + + @Override + @Transactional + public TagDto createTag(TagCreateDto dto) { + tagValidationService.validateCreate(dto); + Tag tag = tagMapper.fromCreateDto(dto); + + tag = tagRepository.save(tag); + log.info("Создан тег с id={}", tag.getId()); + return tagMapper.toDto(tag); + } + + @Override + @Transactional(readOnly = true) + public PageOfTags getTags(Pageable pageable) { + Page page = tagRepository.findAll(pageable); + PageValidator.validatePage(page); + + log.info("Запрошены теги: страница {}, размер {}", pageable.getPageNumber(), pageable.getPageSize()); + return toApiPage(page, tagMapper); + } + + @Override + @Transactional(readOnly = true) + public PageOfTags getTagsByEvent(UUID eventId, Pageable pageable) { + eventReadService.findById(eventId); + Page page = tagRepository.findAllByEventsId(eventId, pageable); + PageValidator.validatePage(page); + + log.info("Запрошены теги для события с id={}: страница {}, размер {}", + eventId, pageable.getPageNumber(), pageable.getPageSize()); + return toApiPage(page, tagMapper); + } + + @Override + @Cacheable(value = "tags_by_event", key = "#eventId + ':' + #tagId") + public TagWithStatsDto getTagByEvent(UUID eventId, UUID tagId) { + this.transactionTemplate.setReadOnly(true); + Tag tag = transactionTemplate.execute(status -> { + eventReadService.findById(eventId); + return tagReadService.findByIdAndEventsId(tagId, eventId); + }); + + TagStatsDto stats = tagAnalyticsFacade.getTagStats(tagId); + + log.info("Запрошен тег с id={} для события с id={}", tagId, eventId); + return tagMapper.toDtoWithStats(tag, stats); + } + + @Override + @CachePut(value = "tags", key = "#tagId") + public TagDto updateTag(UUID tagId, TagUpdateDto dto) { + Tag tag = transactionTemplate.execute(status -> { + Tag tagEntity = tagReadService.findById(tagId); + + tagValidationService.validateUpdate(dto, tagEntity); + + tagEntity = tagMapper.updateTagFromDto(dto, tagEntity); + + tagEntity = tagRepository.save(tagEntity); + return tagEntity; + }); + + Set eventIds = relationIndexService.getLeftIds(EVENT_TAG_RELATION, tagId); + eventTagCacheService.refreshCompositeCacheForTag(tag, eventIds); + + log.info("Обновлен тег с id={}", tagId); + return tagMapper.toDto(tag); + } + + @Override + public void deleteForEvent(UUID eventId, UUID tagId) { + transactionTemplate.executeWithoutResult(status -> { + Event event = eventReadService.findById(eventId); + Tag tag = tagReadService.findByIdAndEventsId(tagId, eventId); + + event.removeTag(tag); + }); + + relationIndexService.remove(EVENT_TAG_RELATION, eventId, tagId); + + eventReadService.findDtoById(eventId); + eventTagCacheService.refreshByEventBatch(eventId, Set.of(tagId)); + + log.info("Тег с id={} отвязан от события с id={}", tagId, eventId); + } + + @Override + @CacheEvict(value = "tags", key = "#tagId") + public void deleteTag(UUID tagId) { + transactionTemplate.executeWithoutResult(status -> { + Tag tag = tagReadService.findById(tagId); + + Iterator iterator = tag.getEvents().iterator(); + while (iterator.hasNext()) { + Event event = iterator.next(); + iterator.remove(); + event.getTags().remove(tag); + } + + tagRepository.deleteById(tagId); + }); + + eventTagCacheService.evictEventTagCache(tagId); + + log.info("Удален тег с id={}", tagId); + } + + @Override + public void cacheRefreshAfterAddTag(UUID eventId, UUID tagId) { + eventReadService.findDtoById(eventId); + tagReadService.findDtoById(tagId); + log.info("Кэш обновлен для события с id={} и тега с id={}", eventId, tagId); + } + + @Transactional + public Tag addTagLocal(UUID eventId, UUID tagId) { + Event event = eventReadService.findById(eventId); + Tag tag = tagReadService.findById(tagId); + + if (tagReadService.existsByIdAndEventsId(tagId, eventId)) { + log.error("Тег с id={} уже добавлен к событию с id={}", tagId, eventId); + throw new ConflictException("Тег с id=" + tagId + " уже добавлен к событию с id=" + eventId); + } + + event.addTag(tag); + relationIndexService.add(EVENT_TAG_RELATION, eventId, tagId); + + return tag; + } + + @Transactional + public void removeTagLocal(UUID eventId, UUID tagId) { + Event event = eventReadService.findById(eventId); + Tag tag = tagReadService.findById(tagId); + + event.removeTag(tag); + + relationIndexService.remove(EVENT_TAG_RELATION, eventId, tagId); + + log.info("Тег с id={} удален из события с id={}", tagId, eventId); + } + + private PageOfTags toApiPage(Page page, TagMapper mapper) { + PageOfTags apiPage = new PageOfTags(); + apiPage.setContent(page.getContent().stream().map(mapper::toDto).toList()); + apiPage.setPageNumber(page.getNumber()); + apiPage.setPageSize(page.getSize()); + apiPage.setTotalElements(page.getTotalElements()); + apiPage.setTotalPages(page.getTotalPages()); + return apiPage; + } +} diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/UserMetadataServiceImpl.java b/src/main/java/ru/practicum/eventhub/service/impl/UserMetadataServiceImpl.java similarity index 60% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/UserMetadataServiceImpl.java rename to src/main/java/ru/practicum/eventhub/service/impl/UserMetadataServiceImpl.java index 6eae5cc..9fdbb4e 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/UserMetadataServiceImpl.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/UserMetadataServiceImpl.java @@ -1,18 +1,19 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.eventhub.api.dto.response.UserMetadataDto; import ru.practicum.eventhub.api.mapper.UserMetadataMapper; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.model.UserMetadata; -import ru.practicum.eventhub.domain.repository.UserMetadataRepository; -import ru.practicum.eventhub.domain.service.UserMetadataService; -import ru.practicum.eventhub.domain.util.PageValidator; +import ru.practicum.eventhub.api.model.PageOfMetadata; +import ru.practicum.eventhub.model.UserMetadata; +import ru.practicum.eventhub.repository.UserMetadataRepository; +import ru.practicum.eventhub.service.UserMetadataService; +import ru.practicum.eventhub.util.PageValidator; import java.util.UUID; @@ -27,17 +28,18 @@ public class UserMetadataServiceImpl implements UserMetadataService { @Override @Transactional(readOnly = true) - public PagedResponse getUserMetadata(Pageable pageable) { + public PageOfMetadata getUserMetadata(Pageable pageable) { Page page = userMetadataRepository.findAll(pageable); PageValidator.validatePage(page); log.info("Получена страница user-metadata: страница={}, размер={}", pageable.getPageNumber(), pageable.getPageSize()); - return PagedResponse.from(page, userMetadataMapper::toDto); + return toApiPage(page, userMetadataMapper); } @Override @Transactional(readOnly = true) + @Cacheable(value = "userMetadata", key = "#userId") public UserMetadataDto getByUserId(UUID userId) { userReadService.findById(userId); UserMetadata userMetadata = metadataReadService.findByUserId(userId); @@ -46,4 +48,14 @@ public UserMetadataDto getByUserId(UUID userId) { log.info("Запрошена user-metadata с id={}, userId={}", metadataId, userId); return userMetadataMapper.toDto(userMetadata); } + + private PageOfMetadata toApiPage(Page page, UserMetadataMapper mapper) { + PageOfMetadata apiPage = new PageOfMetadata(); + apiPage.setContent(page.getContent().stream().map(mapper::toDto).toList()); + apiPage.setPageNumber(page.getNumber()); + apiPage.setPageSize(page.getSize()); + apiPage.setTotalElements(page.getTotalElements()); + apiPage.setTotalPages(page.getTotalPages()); + return apiPage; + } } diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/UserReadService.java b/src/main/java/ru/practicum/eventhub/service/impl/UserReadService.java similarity index 92% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/UserReadService.java rename to src/main/java/ru/practicum/eventhub/service/impl/UserReadService.java index 387730c..622b5a3 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/UserReadService.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/UserReadService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; @@ -6,8 +6,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.User; -import ru.practicum.eventhub.domain.repository.UserRepository; +import ru.practicum.eventhub.model.User; +import ru.practicum.eventhub.repository.UserRepository; import java.util.UUID; diff --git a/src/main/java/ru/practicum/eventhub/domain/service/impl/UserServiceImpl.java b/src/main/java/ru/practicum/eventhub/service/impl/UserServiceImpl.java similarity index 62% rename from src/main/java/ru/practicum/eventhub/domain/service/impl/UserServiceImpl.java rename to src/main/java/ru/practicum/eventhub/service/impl/UserServiceImpl.java index 353a399..0a1c2af 100644 --- a/src/main/java/ru/practicum/eventhub/domain/service/impl/UserServiceImpl.java +++ b/src/main/java/ru/practicum/eventhub/service/impl/UserServiceImpl.java @@ -1,7 +1,11 @@ -package ru.practicum.eventhub.domain.service.impl; +package ru.practicum.eventhub.service.impl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -10,12 +14,12 @@ import ru.practicum.eventhub.api.dto.request.UserUpdateDto; import ru.practicum.eventhub.api.dto.response.UserDto; import ru.practicum.eventhub.api.mapper.UserMapper; -import ru.practicum.eventhub.domain.dto.PagedResponse; -import ru.practicum.eventhub.domain.model.User; -import ru.practicum.eventhub.domain.repository.UserRepository; -import ru.practicum.eventhub.domain.service.UserService; -import ru.practicum.eventhub.domain.util.PageValidator; -import ru.practicum.eventhub.domain.validation.UserValidationService; +import ru.practicum.eventhub.api.model.PageOfUsers; +import ru.practicum.eventhub.model.User; +import ru.practicum.eventhub.repository.UserRepository; +import ru.practicum.eventhub.service.UserService; +import ru.practicum.eventhub.util.PageValidator; +import ru.practicum.eventhub.validation.UserValidationService; import java.util.UUID; @@ -44,24 +48,27 @@ public UserDto createUser(UserCreateDto dto) { @Override @Transactional(readOnly = true) - public PagedResponse getUsers(Pageable pageable) { + public PageOfUsers getUsers(Pageable pageable) { Page page = userRepository.findAll(pageable); PageValidator.validatePage(page); log.info("Получена страница пользователей: страница={}, размер={}", pageable.getPageNumber(), pageable.getPageSize()); - return PagedResponse.from(page, userMapper::toDto); + return toApiPage(page, userMapper); } @Override @Transactional(readOnly = true) + @Cacheable(value = "users", key = "#id") public UserDto getUserById(UUID id) { + User user = userReadService.findById(id); log.info("Запрошен пользователь с id={}", id); - return userMapper.toDto(userReadService.findById(id)); + return userMapper.toDto(user); } @Override @Transactional + @CachePut(value = "users", key = "#id") public UserDto updateUser(UUID id, UserUpdateDto dto) { User user = userReadService.findByIdForUpdate(id); @@ -76,9 +83,23 @@ public UserDto updateUser(UUID id, UserUpdateDto dto) { @Override @Transactional + @Caching(evict = { + @CacheEvict(value = "users", key = "#id"), + @CacheEvict(value = "userMetadata", key = "#id") + }) public void deleteUser(UUID id) { userReadService.findById(id); userRepository.deleteById(id); log.info("Удален пользователь с id={}", id); } + + private PageOfUsers toApiPage(Page page, UserMapper mapper) { + PageOfUsers apiPage = new PageOfUsers(); + apiPage.setContent(page.getContent().stream().map(mapper::toDto).toList()); + apiPage.setPageNumber(page.getNumber()); + apiPage.setPageSize(page.getSize()); + apiPage.setTotalElements(page.getTotalElements()); + apiPage.setTotalPages(page.getTotalPages()); + return apiPage; + } } diff --git a/src/main/java/ru/practicum/eventhub/domain/util/PageValidator.java b/src/main/java/ru/practicum/eventhub/util/PageValidator.java similarity index 94% rename from src/main/java/ru/practicum/eventhub/domain/util/PageValidator.java rename to src/main/java/ru/practicum/eventhub/util/PageValidator.java index 85dea4f..86025cf 100644 --- a/src/main/java/ru/practicum/eventhub/domain/util/PageValidator.java +++ b/src/main/java/ru/practicum/eventhub/util/PageValidator.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.util; +package ru.practicum.eventhub.util; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; diff --git a/src/main/java/ru/practicum/eventhub/domain/validation/CategoryValidationService.java b/src/main/java/ru/practicum/eventhub/validation/CategoryValidationService.java similarity index 88% rename from src/main/java/ru/practicum/eventhub/domain/validation/CategoryValidationService.java rename to src/main/java/ru/practicum/eventhub/validation/CategoryValidationService.java index c373874..64cb869 100644 --- a/src/main/java/ru/practicum/eventhub/domain/validation/CategoryValidationService.java +++ b/src/main/java/ru/practicum/eventhub/validation/CategoryValidationService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.validation; +package ru.practicum.eventhub.validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -8,9 +8,9 @@ import ru.practicum.eventhub.api.dto.request.CategoryUpdateDto; import ru.practicum.eventhub.api.dto.request.ProjectCreateDto; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.Category; -import ru.practicum.eventhub.domain.service.impl.CategoryReadService; -import ru.practicum.eventhub.domain.service.impl.ProjectReadService; +import ru.practicum.eventhub.model.Category; +import ru.practicum.eventhub.service.impl.CategoryReadService; +import ru.practicum.eventhub.service.impl.ProjectReadService; import java.util.Set; diff --git a/src/main/java/ru/practicum/eventhub/domain/validation/EventValidationService.java b/src/main/java/ru/practicum/eventhub/validation/EventValidationService.java similarity index 90% rename from src/main/java/ru/practicum/eventhub/domain/validation/EventValidationService.java rename to src/main/java/ru/practicum/eventhub/validation/EventValidationService.java index d1d5723..9c551d9 100644 --- a/src/main/java/ru/practicum/eventhub/domain/validation/EventValidationService.java +++ b/src/main/java/ru/practicum/eventhub/validation/EventValidationService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.validation; +package ru.practicum.eventhub.validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -7,7 +7,7 @@ import ru.practicum.eventhub.api.dto.request.EventCreateDto; import ru.practicum.eventhub.api.dto.request.EventUpdateDto; import ru.practicum.eventhub.api.dto.request.TagCreateDto; -import ru.practicum.eventhub.domain.service.impl.TagReadService; +import ru.practicum.eventhub.service.impl.TagReadService; import java.util.Set; diff --git a/src/main/java/ru/practicum/eventhub/domain/validation/ProjectValidationService.java b/src/main/java/ru/practicum/eventhub/validation/ProjectValidationService.java similarity index 87% rename from src/main/java/ru/practicum/eventhub/domain/validation/ProjectValidationService.java rename to src/main/java/ru/practicum/eventhub/validation/ProjectValidationService.java index 45f207b..d8ce37d 100644 --- a/src/main/java/ru/practicum/eventhub/domain/validation/ProjectValidationService.java +++ b/src/main/java/ru/practicum/eventhub/validation/ProjectValidationService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.validation; +package ru.practicum.eventhub.validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -6,8 +6,8 @@ import org.springframework.transaction.annotation.Transactional; import ru.practicum.eventhub.api.dto.request.ProjectUpdateDto; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.Project; -import ru.practicum.eventhub.domain.service.impl.ProjectReadService; +import ru.practicum.eventhub.model.Project; +import ru.practicum.eventhub.service.impl.ProjectReadService; import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; diff --git a/src/main/java/ru/practicum/eventhub/domain/validation/TagValidationService.java b/src/main/java/ru/practicum/eventhub/validation/TagValidationService.java similarity index 89% rename from src/main/java/ru/practicum/eventhub/domain/validation/TagValidationService.java rename to src/main/java/ru/practicum/eventhub/validation/TagValidationService.java index e448e2b..6deb22e 100644 --- a/src/main/java/ru/practicum/eventhub/domain/validation/TagValidationService.java +++ b/src/main/java/ru/practicum/eventhub/validation/TagValidationService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.validation; +package ru.practicum.eventhub.validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -7,8 +7,8 @@ import ru.practicum.eventhub.api.dto.request.TagCreateDto; import ru.practicum.eventhub.api.dto.request.TagUpdateDto; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.Tag; -import ru.practicum.eventhub.domain.service.impl.TagReadService; +import ru.practicum.eventhub.model.Tag; +import ru.practicum.eventhub.service.impl.TagReadService; import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; diff --git a/src/main/java/ru/practicum/eventhub/domain/validation/UserValidationService.java b/src/main/java/ru/practicum/eventhub/validation/UserValidationService.java similarity index 92% rename from src/main/java/ru/practicum/eventhub/domain/validation/UserValidationService.java rename to src/main/java/ru/practicum/eventhub/validation/UserValidationService.java index 31afe7f..284e966 100644 --- a/src/main/java/ru/practicum/eventhub/domain/validation/UserValidationService.java +++ b/src/main/java/ru/practicum/eventhub/validation/UserValidationService.java @@ -1,4 +1,4 @@ -package ru.practicum.eventhub.domain.validation; +package ru.practicum.eventhub.validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -7,9 +7,9 @@ import ru.practicum.eventhub.api.dto.request.UserCreateDto; import ru.practicum.eventhub.api.dto.request.UserUpdateDto; import ru.practicum.eventhub.api.exception.ConflictException; -import ru.practicum.eventhub.domain.model.User; -import ru.practicum.eventhub.domain.service.impl.MetadataReadService; -import ru.practicum.eventhub.domain.service.impl.UserReadService; +import ru.practicum.eventhub.model.User; +import ru.practicum.eventhub.service.impl.MetadataReadService; +import ru.practicum.eventhub.service.impl.UserReadService; import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; diff --git a/src/main/java/ru/practicum/eventhub/web/filter/RequestIdFilter.java b/src/main/java/ru/practicum/eventhub/web/filter/RequestIdFilter.java index 2b89a82..e8b6814 100644 --- a/src/main/java/ru/practicum/eventhub/web/filter/RequestIdFilter.java +++ b/src/main/java/ru/practicum/eventhub/web/filter/RequestIdFilter.java @@ -4,21 +4,24 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.UUID; +@Slf4j @Component public class RequestIdFilter extends OncePerRequestFilter { private static final String REQUEST_ID_HEADER = "X-Request-Id"; + private static final String REQUEST_ID_MDC_KEY = "requestId"; @Override protected void doFilterInternal( - HttpServletRequest request, + @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { @@ -27,9 +30,12 @@ protected void doFilterInternal( if (requestId == null || requestId.isBlank()) { requestId = UUID.randomUUID().toString(); + log.debug("Generated new Request ID: {}", requestId); + } else { + log.debug("Received Request ID: {}", requestId); } - MDC.put("requestId", requestId); + MDC.put(REQUEST_ID_MDC_KEY, requestId); response.setHeader(REQUEST_ID_HEADER, requestId); try { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a461708..3704ff7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,21 +3,58 @@ spring: name: event-hub datasource: - driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/postgres username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:123456} + driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: validate + show-sql: true properties: hibernate: - show_sql: true format_sql: true default_schema: event_hub flyway: enabled: true baseline-on-migrate: true - default-schema: event_hub \ No newline at end of file + default-schema: event_hub + + cache: + type: redis + + data: + redis: + host: localhost + port: 6379 + + cloud: + openfeign: + circuitbreaker: + enabled: true + +resilience4j: + circuitbreaker: + instances: + tag-analytics: + slidingWindowSize: 10 + failureRateThreshold: 50 + waitDurationInOpenState: 10s + + retry: + instances: + tag-analytics: + maxAttempts: 3 + waitDuration: 500ms + exponentialBackoffMultiplier: 2 + enableRandomizedWait: true + randomizedWaitFactor: 0.5 + ignoreExceptions: + - ru.practicum.eventhub.config.feign.exception.TagAnalyticsClientException + - ru.practicum.eventhub.config.feign.exception.TagNotFoundException + +services: + tag-analytics: + url: http://localhost:8081 diff --git a/src/main/resources/db/migration/V2__add_cascade_update.sql b/src/main/resources/db/migration/V2__add_cascade_update.sql new file mode 100644 index 0000000..0e220ca --- /dev/null +++ b/src/main/resources/db/migration/V2__add_cascade_update.sql @@ -0,0 +1,47 @@ +-- Добавление ON UPDATE CASCADE для всех внешних ключей с ON DELETE CASCADE +-- Создание таблицы compensation_actions для хранения информации о компенсационных действиях + +-- 1. projects.category_id: изменение с RESTRICT на CASCADE с обновлением +alter table projects drop constraint if exists projects_category_id_fkey; +alter table projects + add constraint projects_category_id_fkey + foreign key (category_id) + references categories(id) + on delete cascade + on update cascade; + +-- 2. user_metadata.user_id: добавление ON UPDATE CASCADE +alter table user_metadata drop constraint if exists user_metadata_user_id_fkey; +alter table user_metadata + add constraint user_metadata_user_id_fkey + foreign key (user_id) + references users(id) + on delete cascade + on update cascade; + +-- 3. event_tags.event_id: добавление ON UPDATE CASCADE +alter table event_tags drop constraint if exists event_tags_event_id_fkey; +alter table event_tags + add constraint event_tags_event_id_fkey + foreign key (event_id) + references events(id) + on delete cascade + on update cascade; + +-- 4. event_tags.tag_id: добавление ON UPDATE CASCADE +alter table event_tags drop constraint if exists event_tags_tag_id_fkey; +alter table event_tags + add constraint event_tags_tag_id_fkey + foreign key (tag_id) + references tags(id) + on delete cascade + on update cascade; + +CREATE TABLE compensation_actions ( + id UUID PRIMARY KEY default gen_random_uuid(), + action_type VARCHAR(50) NOT NULL, + payload TEXT NOT NULL, + retries INT DEFAULT 0, + last_attempt timestamptz, + created_at timestamptz not null default now() +); diff --git a/src/main/resources/openapi/openapi.yml b/src/main/resources/openapi/openapi.yml index 7d15861..238041f 100644 --- a/src/main/resources/openapi/openapi.yml +++ b/src/main/resources/openapi/openapi.yml @@ -1090,7 +1090,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TagDto' + $ref: '#/components/schemas/TagWithStatsDto' '400': description: Некорректные данные запроса content: @@ -1139,7 +1139,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/TagDto' + $ref: '#/components/schemas/TagWithStatsDto' '400': description: Некорректный id content: @@ -1488,15 +1488,6 @@ components: format: uuid description: Идентификатор пользователя example: "a1234567-89ab-cdef-0123-456789abcdef" - username: - type: string - description: Имя пользователя - example: "ivan.petrov" - email: - type: string - format: email - description: Email пользователя - example: "ivan.petrov@example.com" UserMetadataDto: type: object @@ -1575,10 +1566,6 @@ components: format: uuid description: Идентификатор категории example: "b1a7e8c2-3d4f-4e2a-9c1a-2b3c4d5e6f7a" - name: - type: string - description: Название категории - example: "Технологии" ProjectDto: type: object @@ -1617,10 +1604,6 @@ components: format: uuid description: Идентификатор проекта example: "e1f2a3b4-5678-1234-abcd-9876543210ab" - name: - type: string - description: Название проекта - example: "EventHub" EventDto: type: object @@ -1671,10 +1654,6 @@ components: format: uuid description: Идентификатор события example: "b1a7e8c2-3d4f-4e2a-9c1a-2b3c4d5e6f7a" - title: - type: string - description: Название события - example: "Митап по Java" TagDto: type: object @@ -1698,7 +1677,16 @@ components: items: $ref: '#/components/schemas/EventShortDto' - TagShortDto: + TagStatsDto: + type: object + properties: + createdAt: + type: string + format: date-time + description: Дата и время добавления тега к событию + example: "2025-11-05T15:30:00+03:00" + + TagWithStatsDto: type: object properties: id: @@ -1710,13 +1698,33 @@ components: type: string description: Название тега example: "Java" + description: + type: string + description: Описание тега + example: "Тег для событий, связанных с Java." + events: + type: array + description: Список событий с этим тегом + items: + $ref: '#/components/schemas/EventShortDto' + stats: + $ref: '#/components/schemas/TagStatsDto' + + TagShortDto: + type: object + properties: + id: + type: string + format: uuid + description: Идентификатор тега + example: "c1d2e3f4-5678-1234-abcd-9876543210ab" PageMeta: type: object properties: - page: + pageNumber: type: integer - size: + pageSize: type: integer totalElements: type: integer