发帖、对帖子点赞、评论、加精时会产生分数,根据分数对帖子进行排名
(设置定时任务,每隔一段时间自动计算分数)
config
@Configuration
public class QuartzConfig {
/**
* FactoryBean和BeanFactory
*
* BeanFactory:IOC容器顶层接口
*
*
* FactoryBean:可简化bean的实例化过程
* 1.通过FactoryBean封装了某些bean的实例化过程
* 2.将FactoryBean装配到spring中
* 3.将FactoryBean注入给其他的bean
* 4.该bean得到的是FactoryBean所管理的对象实例
*
*/
//配置jobDetail
//刷新帖子分数配置
@Bean
public JobDetailFactoryBean postScoreRefreshDetail(){
JobDetailFactoryBean factoryBean=new JobDetailFactoryBean(); //实例化对象
//设置属性
factoryBean.setJobClass( PostScoreRefreshJob.class);
factoryBean.setName("postScoreRefreshJob");
factoryBean.setGroup("communityJobGroup");
factoryBean.setDurability(true); //该任务是否长久保存
factoryBean.setRequestsRecovery(true); //该任务是否可恢复的
return factoryBean;
}
//配置trigger(依赖于jobDetail,需将jobDetail注入)【SimpleTriggerFactoryBean(简单trigger),CronTriggerFactoryBean(复杂的trigger)】
@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshDetail){
SimpleTriggerFactoryBean factoryBean=new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(postScoreRefreshDetail);
factoryBean.setName("postScoreRefreshTrigger");
factoryBean.setGroup("communityTriggerGroup");
factoryBean.setRepeatInterval(1000*60*5); //时间间隔
factoryBean.setJobDataMap(new JobDataMap()); //trigger底层需要存储job的一些状态(初始化了一个默认的存储方式
return factoryBean;
}
}
quartz
public class PostScoreRefreshJob implements Job , CommunityConstant {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private LikeService likeService;
@Autowired
private DiscussPostService discussPostService;
@Autowired
private ElasticsearchService elasticsearchService;
private static final Logger logger= LoggerFactory.getLogger(PostScoreRefreshJob.class);
//牛客纪元
private static final Date epoch;
static {
try {
epoch=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-08-01 00:00:00");
} catch (ParseException e) {
throw new RuntimeException("初始化牛客纪元失败!",e);
}
}
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
String redisKey = RedisKeyUtil.getPostScoreKey();
///BoundSetOperations就是一个绑定key的对象,我们可以通过这个对象来进行与key相关的操作。
BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);
if(operations.size()==0){
logger.info("[任务取消] 没有要刷新的帖子!");
return;
}
logger.info("[任务开始] 正在刷新帖子分数:"+operations.size());
while(operations.size()>0){
this.refresh((Integer) operations.pop()); //根据帖子id查找帖子计算分数
}
logger.info("[任务结束] 帖子分数刷新完毕!");
}
private void refresh(int postId){
DiscussPost post = discussPostService.findDiscussPostById(postId);
if(post==null){
logger.error("该帖子不存在!");
return;
}
if(post.getStatus()==2){
logger.error("该帖子已被删除!");
return;
}
//是否精华
boolean wonderful = post.getStatus() == 1;
//评论数量
int commentCount = post.getCommentCount();
//点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);
//log(精华分+评论数*10+ 点赞数*2) +(发布时间 - 牛客纪元)
//计算权重
long w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;
//分数
double score = Math.log10(Math.max(1, w)) + (post.getCreateTime().getTime() - epoch.getTime()) / (3600 * 24 * 1000);
//更新帖子分数
discussPostService.updateScore(postId,score);
//同步elasticsearch中搜索数据
post.setScore(score);
elasticsearchService.saveDiscussPost(post); //这个实体是早先查到的,中途更改了它的分数,所以需要将分数同步一下
}
}
controller
发布帖子
@RequestMapping(value = "/add",method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title,String content){
//判断是否登录
User user = hostHolder.getUser();
if(user==null){
//还未登陆无权限访问
return CommunityUtil.getJsonString(403,"还未登陆!");
}
DiscussPost post=new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
//发布帖子后,同步到elasticsearch中
//利用事件进行发送
Event event=new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId());
eventProducer.fireEvent(event);
//计算帖子分数(将要计算分数的帖子加入set中
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,post.getId());
return CommunityUtil.getJsonString(0,"发布成功!");
}
给帖子加精
//加精
@RequestMapping(value = "/wonderful",method = RequestMethod.POST)
@ResponseBody //结果返回为json字符串
public String setWonderful(int id){
DiscussPost post = discussPostService.findDiscussPostById(id);
// 1为加精,0为正常, 1^1=0, 0^1=1
int status = post.getStatus()^1;
//更改帖子状态
discussPostService.updateStatus(id,status);
//将返回的结果
Map<String,Object> map=new HashMap<>();
map.put("status",status);
//更改完帖子后,要同步更新elasticsearch中的结果
//触发发帖事件
Event event=new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(hostHolder.getUser().getId())
.setEntityId(id)
.setEntityType(ENTITY_TYPE_POST);
eventProducer.fireEvent(event);
//计算帖子分数(将要计算分数的帖子加入set中
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,id);
return CommunityUtil.getJsonString(0,null,map);
}
点赞
@RequestMapping(value = "/like",method = RequestMethod.POST)
@ResponseBody
public String like(int entityType,int entityId,int entityUserId,int postId){
User user = hostHolder.getUser();
//点赞
likeService.like(user.getId(),entityType,entityId, entityUserId);
//获取点赞数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
//获取点赞状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
//返回的结果
Map<String,Object> map=new HashMap<>();
map.put("likeCount",likeCount);
map.put("likeStatus",likeStatus);
//触发点赞事件
if(likeStatus==1){
Event event=new Event()
.setTopic(TOPIC_LIKE)
.setUserId(hostHolder.getUser().getId())
.setEntityId(entityId)
.setEntityType(entityType)
.setEntityUserId(entityUserId)
.setData("postId",postId);
eventProducer.fireEvent(event);
}
//计算对帖子点赞的情况
if(entityType==ENTITY_TYPE_POST){
//计算帖子分数(将要计算分数的帖子加入set中
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,postId);
}
return CommunityUtil.getJsonString(0,null,map);
}
重构首页代码
//定义处理请求的方法
//若返回的是一个HTML,则不用加@Response注解
@RequestMapping(path = "/index",method = RequestMethod.GET)
public String getIndexPage(Model model, Page page, @RequestParam(name = "orderMode",defaultValue = "0")int orderMode){
//在SpringMVC中,方法参数都是由DispatcherServlet初始化的,
//还会额外把Page对象装进Model中
/*
在方法调用前,SpringMVC会自动实例化Model和Page,并将Page注入Model中
所以在thymeleaf模板中就可以直接访问Page对象中的数据
*/
page.setRows(discussPostService.findDiscussPostRows(0));//设置总行数
page.setPath("/index?orderMode="+orderMode);//设置返回路径
//首先获取帖子信息
List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit(),orderMode);
List<Map<String,Object>> discussPosts=new ArrayList<>();
for(DiscussPost post:list){
Map<String,Object> map=new HashMap<>();
map.put("post",post);
User user = userService.findUserById(post.getUserId());//再通过获取的帖子信息的UserID找到User完整信息
map.put("user",user);
//获取点赞数量
long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());
map.put("likeCount",likeCount);
discussPosts.add(map);
}
model.addAttribute("discussPosts",discussPosts);
model.addAttribute("orderMode",orderMode);
//通过
return "/index";
}
service
//更改帖子分数
public int updateScore(int id,double score){
return discussPostMapper.updateScore(id,score);
}
dao
//更改帖子分数
int updateScore(int id,double score);
mapper.xml
<!-- int updateScore(int id,int score); -->
<update id="updateScore">
update discuss_post
set score=#{score}
where id=#{id}
</update>
重构查询帖子代码
<select id="selectDiscussPost" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where status!=2
<if test="userId!=0">
and user_id=#{userId}
</if>
<if test="orderMode==0">
order by type desc,create_time desc
</if>
<if test="orderMode==1">
order by type desc,score desc,create_time desc
</if>
limit #{offset},#{limit}
</select>
html
<!-- 筛选条件 -->
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<a th:class="|nav-link ${orderMode==0?'active':''}|" th:href="@{/index(orderMode=0)}">最新</a>
</li>
<li class="nav-item">
<a th:class="|nav-link ${orderMode==1?'active':''}|" th:href="@{/index(orderMode=1)}">最热</a>
</li>
</ul>