diff --git a/src/main/java/com/krylov/refound/ReFoundApplication.java b/src/main/java/com/krylov/refound/ReFoundApplication.java index fc3d524..39fad69 100644 --- a/src/main/java/com/krylov/refound/ReFoundApplication.java +++ b/src/main/java/com/krylov/refound/ReFoundApplication.java @@ -2,8 +2,10 @@ package com.krylov.refound; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(scanBasePackages = "com.krylov.refound") +@EnableScheduling public class ReFoundApplication { public static void main(String[] args) { diff --git a/src/main/java/com/krylov/refound/controller/PostController.java b/src/main/java/com/krylov/refound/controller/PostController.java index d3413e7..a208c7c 100644 --- a/src/main/java/com/krylov/refound/controller/PostController.java +++ b/src/main/java/com/krylov/refound/controller/PostController.java @@ -2,6 +2,7 @@ package com.krylov.refound.controller; import com.krylov.refound.dto.PostRequest; import com.krylov.refound.dto.PostResponse; +import com.krylov.refound.dto.StatusUpdateRequest; import com.krylov.refound.enums.PostType; import com.krylov.refound.service.PostService; import java.util.List; @@ -12,9 +13,11 @@ import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; @@ -75,4 +78,12 @@ public class PostController { public void delete(@PathVariable Long id) { service.delete(id); } + + @PatchMapping("/{id}/status") + public PostResponse updateStatus( + @PathVariable Long id, + @RequestBody StatusUpdateRequest request + ) { + return service.updateStatus(id, request.getStatus()); + } } diff --git a/src/main/java/com/krylov/refound/dto/StatusUpdateRequest.java b/src/main/java/com/krylov/refound/dto/StatusUpdateRequest.java new file mode 100644 index 0000000..6cbe7a1 --- /dev/null +++ b/src/main/java/com/krylov/refound/dto/StatusUpdateRequest.java @@ -0,0 +1,8 @@ +package com.krylov.refound.dto; + +import lombok.Data; + +@Data +public class StatusUpdateRequest { + private String status; +} diff --git a/src/main/java/com/krylov/refound/repository/FavoriteRepository.java b/src/main/java/com/krylov/refound/repository/FavoriteRepository.java index 741a70d..df23b18 100644 --- a/src/main/java/com/krylov/refound/repository/FavoriteRepository.java +++ b/src/main/java/com/krylov/refound/repository/FavoriteRepository.java @@ -12,6 +12,8 @@ public interface FavoriteRepository extends JpaRepository { boolean existsByUserAndPost(User user, Post post); List findByUser(User user); void deleteByUserAndPost(User user, Post post); + void deleteByPost(Post post); + void deleteByPostIn(List posts); @Query(""" SELECT f.post.id FROM Favorite f diff --git a/src/main/java/com/krylov/refound/repository/PostRepository.java b/src/main/java/com/krylov/refound/repository/PostRepository.java index 12eee3d..6db7364 100644 --- a/src/main/java/com/krylov/refound/repository/PostRepository.java +++ b/src/main/java/com/krylov/refound/repository/PostRepository.java @@ -1,6 +1,8 @@ package com.krylov.refound.repository; import com.krylov.refound.entity.Post; +import com.krylov.refound.enums.PostStatus; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -9,4 +11,8 @@ public interface PostRepository extends JpaRepository, JpaSpecificat List findByUserId(Long userId); + List findByStatus(PostStatus status); + + List findByCreatedAtBefore(LocalDateTime date); + } \ No newline at end of file diff --git a/src/main/java/com/krylov/refound/security/SecurityConfig.java b/src/main/java/com/krylov/refound/security/SecurityConfig.java index a2801cb..59f1063 100644 --- a/src/main/java/com/krylov/refound/security/SecurityConfig.java +++ b/src/main/java/com/krylov/refound/security/SecurityConfig.java @@ -27,6 +27,7 @@ public class SecurityConfig { .authorizeHttpRequests(auth -> auth // Public endpoints .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers("/api/v1/test/**").permitAll() .requestMatchers(org.springframework.http.HttpMethod.GET, "/api/v1/posts/**").permitAll() .requestMatchers("/api/v1/reviews/**").permitAll() .requestMatchers("/files/**", "/uploads/**").permitAll() diff --git a/src/main/java/com/krylov/refound/service/PostService.java b/src/main/java/com/krylov/refound/service/PostService.java index 61bbd73..3e6d982 100644 --- a/src/main/java/com/krylov/refound/service/PostService.java +++ b/src/main/java/com/krylov/refound/service/PostService.java @@ -214,4 +214,63 @@ public class PostService { repository.delete(post); } + + public PostResponse updateStatus(Long id, String statusString) { + // Find the post + Post post = repository.findById(id) + .orElseThrow(() -> new ApiException( + ErrorCode.NOT_FOUND, + "Post not found", + HttpStatus.NOT_FOUND + )); + + User currentUser = userService.getCurrentUser(); + if (!post.getUser().getId().equals(currentUser.getId())) { + throw new ApiException( + ErrorCode.FORBIDDEN, + "Only the post owner can update status", + HttpStatus.FORBIDDEN + ); + } + + // Parse status string to enum + PostStatus newStatus; + try { + newStatus = PostStatus.valueOf(statusString.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new ApiException( + ErrorCode.VALIDATION_ERROR, + "Invalid status: " + statusString + ". Must be ACTIVE or CLOSED", + HttpStatus.BAD_REQUEST + ); + } + + // Update status + post.setStatus(newStatus); + Post updated = repository.save(post); + + // Convert to response with like count + PostResponse response = mapper.toResponse(updated); + + // Count likes for this post + List postIds = List.of(id); + Map likeCounts = favoriteRepository.countLikesByPostIds(postIds).stream() + .collect(Collectors.toMap( + row -> (Long) row[0], + row -> (Long) row[1] + )); + + response.setLikeCount(likeCounts.getOrDefault(id, 0L)); + + // Check if current user has favorited this post + try { + List favoriteIds = favoriteRepository.findFavoritePostIds(currentUser.getId(), postIds); + response.setFavorite(favoriteIds.contains(id)); + } catch (Exception e) { + response.setFavorite(false); + } + + return response; + } + } diff --git a/src/main/java/com/krylov/refound/service/SchedulerService.java b/src/main/java/com/krylov/refound/service/SchedulerService.java new file mode 100644 index 0000000..df716b7 --- /dev/null +++ b/src/main/java/com/krylov/refound/service/SchedulerService.java @@ -0,0 +1,59 @@ +package com.krylov.refound.service; + +import com.krylov.refound.entity.Post; +import com.krylov.refound.enums.PostStatus; +import com.krylov.refound.repository.FavoriteRepository; +import com.krylov.refound.repository.PostRepository; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SchedulerService { + + private final PostRepository postRepository; + private final FavoriteRepository favoriteRepository; + + /** + * Runs every day at 00:01 to delete: + * 1. Posts with CLOSED status + * 2. Posts older than 6 months + */ + @Scheduled(cron = "0 1 0 * * ?") + @Transactional + public void deleteExpiredPosts() { + log.info("Starting scheduled cleanup of expired posts..."); + + LocalDateTime sixMonthsAgo = LocalDateTime.now().minusMonths(6); + + // Find and delete posts with CLOSED status + List closedPosts = postRepository.findByStatus(PostStatus.CLOSED); + if (!closedPosts.isEmpty()) { + // Delete favorites first to avoid foreign key constraint violation + favoriteRepository.deleteByPostIn(closedPosts); + log.info("Deleted {} favorites for CLOSED posts", closedPosts.size()); + + postRepository.deleteAll(closedPosts); + log.info("Deleted {} posts with CLOSED status", closedPosts.size()); + } + + // Find and delete posts older than 6 months + List oldPosts = postRepository.findByCreatedAtBefore(sixMonthsAgo); + if (!oldPosts.isEmpty()) { + // Delete favorites first to avoid foreign key constraint violation + favoriteRepository.deleteByPostIn(oldPosts); + log.info("Deleted {} favorites for old posts", oldPosts.size()); + + postRepository.deleteAll(oldPosts); + log.info("Deleted {} posts older than 6 months", oldPosts.size()); + } + + log.info("Scheduled cleanup completed successfully"); + } +}