From 39a8e981e20524a36a8a9f13ec7ef0237f25d48c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9A=D0=B0?= =?UTF-8?q?=D0=BD?= Date: Thu, 16 Oct 2025 16:38:54 +0300 Subject: [PATCH] Implement category management: add create, update, delete, and retrieve functionalities --- .../controller/AdminCategoryController.java | 36 ++++++ .../controller/PublicCategoryController.java | 33 ++++++ .../category/mapper/CategoryMapper.java | 8 +- .../ru/practicum/category/model/Category.java | 3 +- .../repository/CategoryRepository.java | 21 +++- .../category/service/CategoryService.java | 15 +++ .../category/service/CategoryServiceImpl.java | 105 ++++++++++++++++++ .../practicum/event/mapper/EventMapper.java | 3 +- .../event/repository/EventRepository.java | 2 + .../event/service/EventServiceImpl.java | 15 ++- 10 files changed, 233 insertions(+), 8 deletions(-) diff --git a/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java index 7d521b8..5397596 100644 --- a/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java +++ b/main-service/src/main/java/ru/practicum/category/controller/AdminCategoryController.java @@ -1,4 +1,40 @@ package ru.practicum.category.controller; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.service.CategoryService; + +@Slf4j +@RestController +@RequestMapping("/admin/categories") +@RequiredArgsConstructor public class AdminCategoryController { + private final CategoryService categoryService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public CategoryDto createCategory(@RequestBody @Valid NewCategoryDto newCategoryDto) { + log.debug("Controller: createCategory with data {}", newCategoryDto); + return categoryService.createCategory(newCategoryDto); + } + + @DeleteMapping("/{categoryId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteCategory(@PathVariable Long categoryId) { + log.debug("Controller: deleteCategory with id={}", categoryId); + categoryService.deleteCategory(categoryId); + } + + @PatchMapping("/{categoryId}") + public CategoryDto updateCategory( + @PathVariable Long categoryId, + @RequestBody @Valid NewCategoryDto newCategoryDto) { + log.debug("Controller: updateCategory with id={} with data={}", categoryId, newCategoryDto); + return categoryService.updateCategory(categoryId, newCategoryDto); + } } diff --git a/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java index 9c051bd..2c3b5a3 100644 --- a/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java +++ b/main-service/src/main/java/ru/practicum/category/controller/PublicCategoryController.java @@ -1,4 +1,37 @@ package ru.practicum.category.controller; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.service.CategoryService; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/categories") +@RequiredArgsConstructor public class PublicCategoryController { + private final CategoryService categoryService; + + @GetMapping + public List getCategories( + @RequestParam(defaultValue = "0") @PositiveOrZero Integer from, + @RequestParam(defaultValue = "10") @Positive Integer size) { + int page = from / size; + Pageable pageable = PageRequest.of(page, size); + log.debug("Controller: getCategories with from={}, size={}", from, size); + return categoryService.getCategories(pageable); + } + + @GetMapping("/{categoryId}") + public CategoryDto getCategoryById(@PathVariable Long categoryId) { + log.debug("Controller: getCategoryById with id={}", categoryId); + return categoryService.getCategoryById(categoryId); + } } diff --git a/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java b/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java index 417b392..c8682f9 100644 --- a/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java +++ b/main-service/src/main/java/ru/practicum/category/mapper/CategoryMapper.java @@ -2,6 +2,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; import ru.practicum.category.dto.CategoryDto; import ru.practicum.category.dto.NewCategoryDto; import ru.practicum.category.model.Category; @@ -10,7 +11,12 @@ public interface CategoryMapper { @Mapping(target = "id", ignore = true) - Category toEntity(NewCategoryDto dto); + Category toEntity(NewCategoryDto newCategoryDto); CategoryDto toDto(Category category); + + @Mapping(target = "id", ignore = true) + void updateCategoryFromDto(NewCategoryDto dto, @MappingTarget Category category); + + Category toEntityFromDto(CategoryDto dto); } diff --git a/main-service/src/main/java/ru/practicum/category/model/Category.java b/main-service/src/main/java/ru/practicum/category/model/Category.java index 144c242..4f93be5 100644 --- a/main-service/src/main/java/ru/practicum/category/model/Category.java +++ b/main-service/src/main/java/ru/practicum/category/model/Category.java @@ -1,7 +1,6 @@ package ru.practicum.category.model; import jakarta.persistence.*; -import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -9,7 +8,7 @@ @Entity @Table(name = "categories") @Getter -@Setter(AccessLevel.PROTECTED) +@Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class Category { @Id diff --git a/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java index 6f4235f..a584821 100644 --- a/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java +++ b/main-service/src/main/java/ru/practicum/category/repository/CategoryRepository.java @@ -1,4 +1,23 @@ package ru.practicum.category.repository; -public class CategoryRepository { +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import ru.practicum.category.model.Category; + +import java.util.Optional; + +public interface CategoryRepository extends JpaRepository { + + @Query(""" + SELECT c FROM Category c + WHERE c.name = :name""" + ) + Optional findByName(@Param("name") String name); + + @Query(""" + SELECT c FROM Category c + WHERE c.name = :name AND c.id != :id""" + ) + Optional findByNameAndIdNot(@Param("name") String name, @Param("id") Long id); } diff --git a/main-service/src/main/java/ru/practicum/category/service/CategoryService.java b/main-service/src/main/java/ru/practicum/category/service/CategoryService.java index e2dedef..1c93b7d 100644 --- a/main-service/src/main/java/ru/practicum/category/service/CategoryService.java +++ b/main-service/src/main/java/ru/practicum/category/service/CategoryService.java @@ -1,4 +1,19 @@ package ru.practicum.category.service; +import org.springframework.data.domain.Pageable; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; + +import java.util.List; + public interface CategoryService { + CategoryDto createCategory(NewCategoryDto newCategoryDto); + + void deleteCategory(Long categoryId); + + CategoryDto updateCategory(Long categoryId, NewCategoryDto newCategoryDto); + + List getCategories(Pageable pageable); + + CategoryDto getCategoryById(Long categoryId); } diff --git a/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java index c8db8b8..22428c7 100644 --- a/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java +++ b/main-service/src/main/java/ru/practicum/category/service/CategoryServiceImpl.java @@ -1,4 +1,109 @@ package ru.practicum.category.service; +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.category.dto.CategoryDto; +import ru.practicum.category.dto.NewCategoryDto; +import ru.practicum.category.mapper.CategoryMapper; +import ru.practicum.category.model.Category; +import ru.practicum.category.repository.CategoryRepository; +import ru.practicum.event.repository.EventRepository; +import ru.practicum.exception.ConflictException; +import ru.practicum.exception.NotFoundException; + +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor public class CategoryServiceImpl implements CategoryService { + private final CategoryRepository categoryRepository; + private final CategoryMapper categoryMapper; + private final EventRepository eventRepository; + + @Override + @Transactional + public CategoryDto createCategory(NewCategoryDto newCategoryDto) { + checkCategoryNameUnique(newCategoryDto.name(), null); + + Category category = categoryMapper.toEntity(newCategoryDto); + + Category savedCategory = categoryRepository.save(category); + + log.info("Создана новая категория: {}", savedCategory); + return categoryMapper.toDto(savedCategory); + } + + @Override + @Transactional + public void deleteCategory(Long categoryId) { + Category category = getCategoryByIdOrThrow(categoryId); + + if (eventRepository.existsByCategoryId(categoryId)) { + throw new ConflictException("Невозможно удалить категорию с существующими событиями"); + } + + categoryRepository.delete(category); + log.info("Удалена категория с ID: {}", categoryId); + } + + @Override + @Transactional + public CategoryDto updateCategory(Long categoryId, NewCategoryDto newCategoryDto) { + Category category = getCategoryByIdOrThrow(categoryId); + + checkCategoryNameUnique(newCategoryDto.name(), categoryId); + + categoryMapper.updateCategoryFromDto(newCategoryDto, category); + Category updatedCategory = categoryRepository.save(category); + + log.info("Обновлена категория: {}", updatedCategory); + return categoryMapper.toDto(updatedCategory); + } + + @Override + public List getCategories(Pageable pageable) { + Page categoriesPage = categoryRepository.findAll(pageable); + + if (categoriesPage.isEmpty()) { + return List.of(); + } + + List result = categoriesPage.stream() + .map(categoryMapper::toDto) + .toList(); + + log.info("Найдено {} категорий", result.size()); + return result; + } + + @Override + public CategoryDto getCategoryById(Long categoryId) { + Category category = getCategoryByIdOrThrow(categoryId); + CategoryDto result = categoryMapper.toDto(category); + + log.info("Найдена категория: {}", result); + return result; + } + + private void checkCategoryNameUnique(String name, Long excludedId) { + Optional existingCategory = (excludedId == null) + ? categoryRepository.findByName(name) + : categoryRepository.findByNameAndIdNot(name, excludedId); + + existingCategory.ifPresent(category -> { + throw new ConflictException("Категория с названием '" + name + "' уже существует"); + }); + } + + private Category getCategoryByIdOrThrow(Long categoryId) { + return categoryRepository.findById(categoryId) + .orElseThrow(() -> new NotFoundException("Категория с id " + categoryId + " не найдена")); + } } diff --git a/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java b/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java index 76e9b50..5b1dfb6 100644 --- a/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java +++ b/main-service/src/main/java/ru/practicum/event/mapper/EventMapper.java @@ -3,6 +3,7 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; +import ru.practicum.category.model.Category; import ru.practicum.event.dto.*; import ru.practicum.event.model.Event; import ru.practicum.event.model.EventState; @@ -14,7 +15,7 @@ public interface EventMapper { @Mapping(target = "id", ignore = true) @Mapping(target = "publishedOn", ignore = true) @Mapping(target = "category", ignore = true) //TODO убрать после реализации Category - Event fromNewEvent(NewEventDto dto, User initiator, EventState state); + Event fromNewEvent(NewEventDto dto, User initiator, Category category, EventState state); EventShortDto toEventShortDto(Event event, Long confirmedRequests, Long views); diff --git a/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java index 55f79fe..6b6f3ae 100644 --- a/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java +++ b/main-service/src/main/java/ru/practicum/event/repository/EventRepository.java @@ -34,4 +34,6 @@ public interface EventRepository extends JpaRepository, JpaSpecific WHERE e.id = :eventId""" ) Optional findByIdNew(@Param("eventId") Long eventId); + + boolean existsByCategoryId(@Param("categoryId") Long categoryId); } diff --git a/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java index 557b0bf..75e618f 100644 --- a/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java +++ b/main-service/src/main/java/ru/practicum/event/service/EventServiceImpl.java @@ -10,6 +10,10 @@ import ru.practicum.CreateEndpointHitDto; import ru.practicum.StatsClient; import ru.practicum.ViewStatsDto; +import ru.practicum.category.dto.CategoryDto; +import ru.practicum.category.mapper.CategoryMapper; +import ru.practicum.category.model.Category; +import ru.practicum.category.service.CategoryService; import ru.practicum.event.dto.*; import ru.practicum.event.mapper.EventMapper; import ru.practicum.event.model.*; @@ -38,7 +42,8 @@ public class EventServiceImpl implements EventService { private final UserService userService; private final RequestRepository requestRepository; private final StatsClient statsClient; -// private final CategoryService categoryService; + private final CategoryService categoryService; + private final CategoryMapper categoryMapper; @Override @Transactional @@ -46,8 +51,12 @@ public EventFullDto createEvent(Long userId, NewEventDto newEventDto) { validateDateEvent(newEventDto.eventDate(), 2); User user = userService.getUserById(userId); -// Category category = categoryService.getCategoryById(newEventDto.category()); TODO - Event event = eventMapper.fromNewEvent(newEventDto, user, EventState.PENDING); + + CategoryDto categoryDto = categoryService.getCategoryById(newEventDto.category()); + Category category = categoryMapper.toEntityFromDto(categoryDto); + + Event event = eventMapper.fromNewEvent(newEventDto, user, category, EventState.PENDING); + event.setCategory(category); log.info("Создано новое событие: {}", event); return eventMapper.toEventFullDto(eventRepository.save(event), 0L, 0L); }