mirror of https://github.com/halo-dev/halo
Complete post visits statistic feature
parent
d1da6880f6
commit
77196780ee
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Integer, BlockingQueue<Integer>> postVisitQueueMap;
|
||||||
|
|
||||||
|
private final Map<Integer, PostVisitTask> 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<Integer> 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<Integer> 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<Integer> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,9 @@ package run.halo.app.repository;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
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.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import run.halo.app.model.entity.Post;
|
import run.halo.app.model.entity.Post;
|
||||||
import run.halo.app.model.enums.PostStatus;
|
import run.halo.app.model.enums.PostStatus;
|
||||||
|
@ -35,4 +37,12 @@ public interface PostRepository extends BasePostRepository<Post>, JpaSpecificati
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
Optional<Post> getByUrlAndStatus(@NonNull String url, @NonNull PostStatus status);
|
Optional<Post> 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,40 @@ public interface PostService extends CrudService<Post, Integer> {
|
||||||
*/
|
*/
|
||||||
long countLike();
|
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.
|
* Lists year archives.
|
||||||
*
|
*
|
||||||
|
|
|
@ -15,8 +15,10 @@ import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import run.halo.app.event.LogEvent;
|
import run.halo.app.event.LogEvent;
|
||||||
|
import run.halo.app.event.VisitEvent;
|
||||||
import run.halo.app.exception.AlreadyExistsException;
|
import run.halo.app.exception.AlreadyExistsException;
|
||||||
import run.halo.app.exception.NotFoundException;
|
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.CategoryOutputDTO;
|
||||||
import run.halo.app.model.dto.TagOutputDTO;
|
import run.halo.app.model.dto.TagOutputDTO;
|
||||||
import run.halo.app.model.dto.post.PostMinimalOutputDTO;
|
import run.halo.app.model.dto.post.PostMinimalOutputDTO;
|
||||||
|
@ -308,7 +310,14 @@ public class PostServiceImpl extends AbstractCrudService<Post, Integer> implemen
|
||||||
|
|
||||||
Optional<Post> postOptional = postRepository.getByUrlAndStatus(url, status);
|
Optional<Post> 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
|
@Override
|
||||||
|
@ -331,6 +340,40 @@ public class PostServiceImpl extends AbstractCrudService<Post, Integer> implemen
|
||||||
return Optional.ofNullable(postRepository.countLike()).orElse(0L);
|
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
|
@Override
|
||||||
public List<ArchiveYearVO> listYearArchives() {
|
public List<ArchiveYearVO> listYearArchives() {
|
||||||
// Get all posts
|
// Get all posts
|
||||||
|
|
|
@ -97,9 +97,9 @@ public class ContentArchiveController {
|
||||||
/**
|
/**
|
||||||
* Render post page.
|
* Render post page.
|
||||||
*
|
*
|
||||||
* @param url post slug url.
|
* @param url post slug url.
|
||||||
* @param cp comment page number
|
* @param cp comment page number
|
||||||
* @param model model
|
* @param model model
|
||||||
* @return template path: theme/{theme}/post.ftl
|
* @return template path: theme/{theme}/post.ftl
|
||||||
*/
|
*/
|
||||||
@GetMapping("{url}")
|
@GetMapping("{url}")
|
||||||
|
@ -125,6 +125,8 @@ public class ContentArchiveController {
|
||||||
model.addAttribute("tags", tags);
|
model.addAttribute("tags", tags);
|
||||||
model.addAttribute("comments", comments);
|
model.addAttribute("comments", comments);
|
||||||
model.addAttribute("pageRainbow", pageRainbow);
|
model.addAttribute("pageRainbow", pageRainbow);
|
||||||
|
|
||||||
|
// Log it
|
||||||
return themeService.render("post");
|
return themeService.render("post");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue