diff --git a/src/main/java/run/halo/app/event/VisitEvent.java b/src/main/java/run/halo/app/event/VisitEvent.java new file mode 100644 index 000000000..ad6b184e9 --- /dev/null +++ b/src/main/java/run/halo/app/event/VisitEvent.java @@ -0,0 +1,34 @@ +package run.halo.app.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +/** + * Visit event. + * + * @author johnniang + * @date 19-4-22 + */ +public class VisitEvent extends ApplicationEvent { + + private final Integer postId; + + /** + * Create a new ApplicationEvent. + * + * @param source the object on which the event initially occurred (never {@code null}) + * @param postId post id + */ + public VisitEvent(@NonNull Object source, @NonNull Integer postId) { + super(source); + + Assert.notNull(postId, "Post id must not be null"); + this.postId = postId; + } + + @NonNull + public Integer getPostId() { + return postId; + } +} diff --git a/src/main/java/run/halo/app/event/VisitEventListener.java b/src/main/java/run/halo/app/event/VisitEventListener.java new file mode 100644 index 000000000..8942738a5 --- /dev/null +++ b/src/main/java/run/halo/app/event/VisitEventListener.java @@ -0,0 +1,120 @@ +package run.halo.app.event; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import run.halo.app.service.PostService; + +import javax.annotation.PreDestroy; +import java.util.Map; +import java.util.concurrent.*; + +/** + * Visit event listener. + * + * @author johnniang + * @date 19-4-22 + */ +@Slf4j +@Component +public class VisitEventListener { + + private final Map> postVisitQueueMap; + + private final Map postVisitTaskMap; + + private final PostService postService; + + private final ExecutorService executor; + + public VisitEventListener(PostService postService) { + this.postService = postService; + + int initCapacity = 8; + + long count = postService.count(); + + if (count < initCapacity) { + initCapacity = (int) count; + } + + postVisitQueueMap = new ConcurrentHashMap<>(initCapacity); + postVisitTaskMap = new ConcurrentHashMap<>(initCapacity); + + this.executor = Executors.newCachedThreadPool(); + } + + @Async + @EventListener + public void onApplicationEvent(VisitEvent event) throws InterruptedException { + // Get post id + Integer postId = event.getPostId(); + + log.debug("Received a visit event, post id: [{}]", postId); + + // Get post visit queue + BlockingQueue postVisitQueue = postVisitQueueMap.computeIfAbsent(postId, this::createEmptyQueue); + + postVisitTaskMap.computeIfAbsent(postId, this::createPostVisitTask); + + // Put a visit for the post + postVisitQueue.put(postId); + + // TODO Attempt to manage the post visit tasks + } + + private PostVisitTask createPostVisitTask(Integer postId) { + // Create new post visit task + PostVisitTask postVisitTask = new PostVisitTask(postId); + // Start a post visit task + executor.execute(postVisitTask); + + log.debug("Created a new post visit task for post id: [{}]", postId); + return postVisitTask; + } + + @PreDestroy + protected void preDestroy() { + executor.shutdownNow(); + } + + private BlockingQueue createEmptyQueue(Integer postId) { + // Create a new queue + return new LinkedBlockingQueue<>(); + } + + /** + * Post visit task. + */ + private class PostVisitTask implements Runnable { + + private final Integer postId; + + private PostVisitTask(Integer postId) { + this.postId = postId; + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + BlockingQueue postVisitQueue = postVisitQueueMap.get(postId); + Integer postId = postVisitQueue.take(); + + log.debug("Took a new visit for post id: [{}]", postId); + + // Increase the visit + postService.increaseVisit(postId); + + log.debug("Increased visits for post id: [{}]", postId); + } catch (InterruptedException e) { + log.warn("Post visit task: " + Thread.currentThread().getName() + " was interrupted", e); + // Ignore this exception + } + } + + log.debug("Thread: [{}] has been interrupted", Thread.currentThread().getName()); + } + } +} diff --git a/src/main/java/run/halo/app/repository/PostRepository.java b/src/main/java/run/halo/app/repository/PostRepository.java index 08b83cfb5..12b0219a0 100644 --- a/src/main/java/run/halo/app/repository/PostRepository.java +++ b/src/main/java/run/halo/app/repository/PostRepository.java @@ -3,7 +3,9 @@ package run.halo.app.repository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.lang.NonNull; import run.halo.app.model.entity.Post; import run.halo.app.model.enums.PostStatus; @@ -35,4 +37,12 @@ public interface PostRepository extends BasePostRepository, JpaSpecificati @NonNull Optional getByUrlAndStatus(@NonNull String url, @NonNull PostStatus status); + + @Modifying + @Query("update Post p set p.visits = p.visits + :visits where p.id = :postId") + int updateVisit(@Param("visits") long visits, @Param("postId") @NonNull Integer postId); + + @Modifying + @Query("update Post p set p.likes = p.likes + :likes where p.id = :postId") + int updateLikes(@Param("likes") long likes, @Param("postId") @NonNull Integer postId); } diff --git a/src/main/java/run/halo/app/service/PostService.java b/src/main/java/run/halo/app/service/PostService.java index fbdbc0428..4f5fe7c3d 100755 --- a/src/main/java/run/halo/app/service/PostService.java +++ b/src/main/java/run/halo/app/service/PostService.java @@ -181,6 +181,40 @@ public interface PostService extends CrudService { */ long countLike(); + /** + * Increases post visits. + * + * @param visits visits must not be less than 1 + * @param postId post id must not be null + */ + @Transactional + void increaseVisit(long visits, @NonNull Integer postId); + + /** + * Increase post likes. + * + * @param likes likes must not be less than 1 + * @param postId post id must not be null + */ + @Transactional + void increaseLike(long likes, @NonNull Integer postId); + + /** + * Increases post visits (1). + * + * @param postId post id must not be null + */ + @Transactional + void increaseVisit(@NonNull Integer postId); + + /** + * Increase post likes(1). + * + * @param postId post id must not be null + */ + @Transactional + void increaseLike(@NonNull Integer postId); + /** * Lists year archives. * diff --git a/src/main/java/run/halo/app/service/impl/PostServiceImpl.java b/src/main/java/run/halo/app/service/impl/PostServiceImpl.java index 8f758df81..b2c189e90 100644 --- a/src/main/java/run/halo/app/service/impl/PostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/PostServiceImpl.java @@ -15,8 +15,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import run.halo.app.event.LogEvent; +import run.halo.app.event.VisitEvent; import run.halo.app.exception.AlreadyExistsException; import run.halo.app.exception.NotFoundException; +import run.halo.app.exception.ServiceException; import run.halo.app.model.dto.CategoryOutputDTO; import run.halo.app.model.dto.TagOutputDTO; import run.halo.app.model.dto.post.PostMinimalOutputDTO; @@ -308,7 +310,14 @@ public class PostServiceImpl extends AbstractCrudService implemen Optional postOptional = postRepository.getByUrlAndStatus(url, status); - return postOptional.orElseThrow(() -> new NotFoundException("The post with status " + status + " and url " + url + "was not existed").setErrorData(url)); + Post post = postOptional.orElseThrow(() -> new NotFoundException("The post with status " + status + " and url " + url + "was not existed").setErrorData(url)); + + if (PostStatus.PUBLISHED.equals(status)) { + // Log it + eventPublisher.publishEvent(new VisitEvent(this, post.getId())); + } + + return post; } @Override @@ -331,6 +340,40 @@ public class PostServiceImpl extends AbstractCrudService implemen return Optional.ofNullable(postRepository.countLike()).orElse(0L); } + @Override + public void increaseVisit(long visits, Integer postId) { + Assert.isTrue(visits > 0, "Visits to increase must not be less than 1"); + Assert.notNull(postId, "Goods id must not be null"); + + long affectedRows = postRepository.updateVisit(visits, postId); + + if (affectedRows != 1) { + throw new ServiceException("Failed to increase visits " + visits + " for post with id " + postId); + } + } + + @Override + public void increaseLike(long likes, Integer postId) { + Assert.isTrue(likes > 0, "Likes to increase must not be less than 1"); + Assert.notNull(postId, "Goods id must not be null"); + + long affectedRows = postRepository.updateLikes(likes, postId); + + if (affectedRows != 1) { + throw new ServiceException("Failed to increase likes " + likes + " for post with id " + postId); + } + } + + @Override + public void increaseVisit(Integer postId) { + increaseVisit(1L, postId); + } + + @Override + public void increaseLike(Integer postId) { + increaseLike(1L, postId); + } + @Override public List listYearArchives() { // Get all posts diff --git a/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java b/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java index e8047602f..687f8cbef 100644 --- a/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java +++ b/src/main/java/run/halo/app/web/controller/content/ContentArchiveController.java @@ -97,9 +97,9 @@ public class ContentArchiveController { /** * Render post page. * - * @param url post slug url. - * @param cp comment page number - * @param model model + * @param url post slug url. + * @param cp comment page number + * @param model model * @return template path: theme/{theme}/post.ftl */ @GetMapping("{url}") @@ -125,6 +125,8 @@ public class ContentArchiveController { model.addAttribute("tags", tags); model.addAttribute("comments", comments); model.addAttribute("pageRainbow", pageRainbow); + + // Log it return themeService.render("post"); } }