Merge pull request 'feat: add user statistics functionality with DTOs and service methods' (#46) from feat/user-stats into main

Reviewed-on: #46
This commit is contained in:
rov 2026-04-05 11:49:09 -03:00
commit 890421ec9c
7 changed files with 127 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

View File

@ -0,0 +1,29 @@
package com.magamochi.userinteraction.controller;
import com.magamochi.common.model.dto.DefaultResponseDTO;
import com.magamochi.userinteraction.model.dto.UserStatisticsDTO;
import com.magamochi.userinteraction.service.UserStatisticsService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user/statistics")
@CrossOrigin(origins = "*")
@RequiredArgsConstructor
public class UserStatisticsController {
private final UserStatisticsService userStatisticsService;
@Operation(
summary = "Get user statistics",
description = "Get statistics for the logged in user.",
tags = {"User Statistics"},
operationId = "getUserStatistics")
@GetMapping
public DefaultResponseDTO<UserStatisticsDTO> getUserStatistics() {
return DefaultResponseDTO.ok(userStatisticsService.getUserStatistics());
}
}

View File

@ -0,0 +1,8 @@
package com.magamochi.userinteraction.model.dto;
import java.time.Instant;
import lombok.Builder;
@Builder
public record UserRecentActivityDTO(
String mangaTitle, String contentTitle, Instant readAt, Long mangaId) {}

View File

@ -0,0 +1,13 @@
package com.magamochi.userinteraction.model.dto;
import java.util.List;
import lombok.Builder;
@Builder
public record UserStatisticsDTO(
long favoriteMangaCount,
long mangaReadingCount,
long chaptersReadCount,
UserRecentActivityDTO lastReadContent,
boolean hasReadingActivity,
List<UserRecentActivityDTO> recentActivities) {}

View File

@ -13,4 +13,6 @@ public interface UserFavoriteMangaRepository extends JpaRepository<UserFavoriteM
Optional<UserFavoriteManga> findByUserAndManga(User user, Manga manga);
Set<UserFavoriteManga> findByUser(User user);
long countByUser(User user);
}

View File

@ -3,11 +3,25 @@ package com.magamochi.userinteraction.model.repository;
import com.magamochi.content.model.entity.MangaContent;
import com.magamochi.user.model.entity.User;
import com.magamochi.userinteraction.model.entity.UserMangaContentRead;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface UserMangaContentReadRepository extends JpaRepository<UserMangaContentRead, Long> {
boolean existsByUserAndMangaContent(User user, MangaContent mangaContent);
Optional<UserMangaContentRead> findByUserAndMangaContent(User user, MangaContent mangaContent);
long countByUser(User user);
@Query(
"SELECT COUNT(DISTINCT mcp.manga) FROM UserMangaContentRead umcr "
+ "JOIN umcr.mangaContent mc "
+ "JOIN mc.mangaContentProvider mcp "
+ "WHERE umcr.user = :user")
long countDistinctMangaByUser(@Param("user") User user);
List<UserMangaContentRead> findTop10ByUserOrderByCreatedAtDesc(User user);
}

View File

@ -0,0 +1,58 @@
package com.magamochi.userinteraction.service;
import com.magamochi.user.service.UserService;
import com.magamochi.userinteraction.model.dto.UserRecentActivityDTO;
import com.magamochi.userinteraction.model.dto.UserStatisticsDTO;
import com.magamochi.userinteraction.model.entity.UserMangaContentRead;
import com.magamochi.userinteraction.model.repository.UserFavoriteMangaRepository;
import com.magamochi.userinteraction.model.repository.UserMangaContentReadRepository;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserStatisticsService {
private final UserService userService;
private final UserFavoriteMangaRepository userFavoriteMangaRepository;
private final UserMangaContentReadRepository userMangaContentReadRepository;
public UserStatisticsDTO getUserStatistics() {
var user = userService.getLoggedUserThrowIfNotFound();
var favoriteMangaCount = userFavoriteMangaRepository.countByUser(user);
var mangaReadingCount = userMangaContentReadRepository.countDistinctMangaByUser(user);
var chaptersReadCount = userMangaContentReadRepository.countByUser(user);
var recentReads = userMangaContentReadRepository.findTop10ByUserOrderByCreatedAtDesc(user);
UserRecentActivityDTO lastReadContent = null;
if (!recentReads.isEmpty()) {
lastReadContent = mapToRecentActivityDTO(recentReads.getFirst());
}
var recentActivities =
recentReads.stream().map(this::mapToRecentActivityDTO).collect(Collectors.toList());
return UserStatisticsDTO.builder()
.favoriteMangaCount(favoriteMangaCount)
.mangaReadingCount(mangaReadingCount)
.chaptersReadCount(chaptersReadCount)
.lastReadContent(lastReadContent)
.hasReadingActivity(!recentReads.isEmpty())
.recentActivities(recentActivities)
.build();
}
private UserRecentActivityDTO mapToRecentActivityDTO(UserMangaContentRead read) {
var mangaContent = read.getMangaContent();
var manga = mangaContent.getMangaContentProvider().getManga();
return UserRecentActivityDTO.builder()
.mangaTitle(manga.getTitle())
.contentTitle(mangaContent.getTitle())
.readAt(read.getCreatedAt())
.mangaId(manga.getId())
.build();
}
}