完成点赞和收藏功能

This commit is contained in:
柏码の讲师 2023-10-22 23:20:46 +08:00
parent fce060dc82
commit e808240a32
7 changed files with 195 additions and 6 deletions

View File

@ -1,6 +1,7 @@
package com.example.controller;
import com.example.entity.RestBean;
import com.example.entity.dto.Interact;
import com.example.entity.vo.request.TopicCreateVO;
import com.example.entity.vo.response.*;
import com.example.service.TopicService;
@ -10,9 +11,11 @@ import com.example.utils.ControllerUtils;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
//论坛相关接口都在这里
@ -63,8 +66,17 @@ public class ForumController {
return RestBean.success(topicService.topTopics());
}
@GetMapping("topic")
@GetMapping("/topic")
public RestBean<TopicDetailVO> topic(@RequestParam @Min(0) int tid){
return RestBean.success(topicService.getTopic(tid));
}
@GetMapping("/interact")
public RestBean<TopicDetailVO> topic(@RequestParam @Min(0) int tid,
@RequestParam @Pattern(regexp = "(like|collect)") String type,
@RequestParam boolean state,
@RequestAttribute(Const.ATTR_USER_ID) int id){
topicService.interact(new Interact(tid, id, new Date(), type), state);
return RestBean.success();
}
}

View File

@ -0,0 +1,26 @@
package com.example.entity.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.Date;
@Data
@AllArgsConstructor
public class Interact {
Integer tid;
Integer uid;
Date time;
String type;
public static Interact parseInteract(String key, String type) {
String[] keys = key.split(":");
return new Interact(Integer.parseInt(keys[0]),
Integer.parseInt(keys[0]),
new Date(), type);
}
public String toKey() {
return this.tid + ":" + this.uid;
}
}

View File

@ -1,9 +1,33 @@
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.dto.Interact;
import com.example.entity.dto.Topic;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface TopicMapper extends BaseMapper<Topic> {
@Insert("""
<script>
insert ignore into db_topic_interact_${type} values
<foreach collection ="interacts" item="item" separator =",">
(#{item.tid}, #{item.uid}, #{item.time})
</foreach>
</script>
""")
int addInteract(List<Interact> interacts, String type);
@Delete("""
<script>
delete from db_topic_interact_${type} where
<foreach collection="interacts" item="item" separator=" or ">
(tid = #{item.tid} and uid = #{item.uid})
</foreach>
</script>
""")
int deleteInteract(List<Interact> interacts, String type);
}

View File

@ -1,6 +1,7 @@
package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.dto.Interact;
import com.example.entity.dto.Topic;
import com.example.entity.dto.TopicType;
import com.example.entity.vo.request.TopicCreateVO;
@ -16,4 +17,5 @@ public interface TopicService extends IService<Topic> {
List<TopicPreviewVO> listTopicByPage(int pageNumber, int type);
List<TopicTopVO> topTopics();
TopicDetailVO getTopic(int tid);
void interact(Interact interact, boolean state);
}

View File

@ -17,9 +17,13 @@ import com.example.utils.Const;
import com.example.utils.FlowUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Service
public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements TopicService {
@ -42,6 +46,9 @@ public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements
@Resource
CacheUtils cacheUtils;
@Resource
StringRedisTemplate template;
@Override
public List<TopicType> listType() {
return typeMapper.selectList(null);
@ -108,6 +115,54 @@ public class TopicServiceImpl extends ServiceImpl<TopicMapper, Topic> implements
return vo;
}
@Override
public void interact(Interact interact, boolean state) {
String type = interact.getType();
synchronized (type.intern()) {
template.opsForHash().put(type, interact.toKey(), Boolean.toString(state));
saveInteractData(type);
}
}
/**
* 由于论坛交互数据如点赞收藏等更新可能会非常频繁
* 更新信息实时到MySQL不太现实所以需要用Redis做缓冲并在合适的时机一次性入库一段时间内的全部数据
* 当数据更新到来时会创建一个新的定时任务此任务会在一段时间之后执行
* 将全部Redis暂时缓存的信息一次性加入到数据库从而缓解MySQL压力如果
* 在定时任务已经设定期间又有新的更新到来仅更新Redis不创建新的延时任务
*/
private final Map<String, Boolean> state = new HashMap<>();
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
private void saveInteractData(String type) {
System.out.println(state);
if(!state.getOrDefault(type, false)) {
state.put(type, true);
service.schedule(() -> {
this.saveInteract(type);
state.put(type, false);
}, 3, TimeUnit.SECONDS);
}
}
private void saveInteract(String type) {
synchronized (type.intern()) {
System.out.println(type);
List<Interact> check = new LinkedList<>();
List<Interact> uncheck = new LinkedList<>();
template.opsForHash().entries(type).forEach((k, v) -> {
if(Boolean.parseBoolean(v.toString()))
check.add(Interact.parseInteract(k.toString(), type));
else
uncheck.add(Interact.parseInteract(k.toString(), type));
});
if(!check.isEmpty())
baseMapper.addInteract(check, type);
if(!uncheck.isEmpty())
baseMapper.deleteInteract(uncheck, type);
template.delete("collects");
}
}
private <T> T fillUserDetailsByPrivacy(T target, int uid) {
AccountDetails details = accountDetailsMapper.selectById(uid);
Account account = accountMapper.selectById(uid);

View File

@ -0,0 +1,45 @@
<script setup>
defineProps({
name: String,
checkName: String,
color: String,
check: Boolean
})
const emit = defineEmits(['check'])
</script>
<template>
<div class="interact-button">
<span class="icon" :style="{color: check ? color : 'unset'}" @click="emit('check')">
<slot/>
</span>
<span class="name"
:style="{'color': color}">
{{check ? checkName : name}}
</span>
</div>
</template>
<style scoped>
.interact-button {
display: inline-block;
height: 20px;
.name {
font-size: 13px;
margin-left: 5px;
opacity: 0.7;
}
.icon{
vertical-align: middle;
transition: .3s;
&:hover {
cursor: pointer;
font-size: 18px;
}
}
}
</style>

View File

@ -3,11 +3,13 @@ import {useRoute} from "vue-router";
import {get} from "@/net";
import {computed, reactive} from "vue";
import axios from "axios";
import {ArrowLeft, Female, Male} from "@element-plus/icons-vue";
import {ArrowLeft, CircleCheck, Female, Male, Star} from "@element-plus/icons-vue";
import { QuillDeltaToHtmlConverter } from 'quill-delta-to-html';
import {useStore} from "@/store";
import Card from "@/components/Card.vue";
import router from "@/router";
import InteractButton from "@/components/InteractButton.vue";
import {ElMessage} from "element-plus";
const route = useRoute()
const store = useStore()
@ -16,6 +18,8 @@ const tid = route.params.tid
const topic = reactive({
data: null,
like: false,
collect: false,
comments: []
})
@ -26,6 +30,16 @@ const content = computed(() => {
const converter = new QuillDeltaToHtmlConverter(ops, { inlineStyles: true });
return converter.convert();
})
function interact(type, message) {
get(`/api/forum/interact?tid=${tid}&type=${type}&state=${!topic[type]}`, () => {
topic[type] = !topic[type]
if(topic[type])
ElMessage.success(`${message}成功!`)
else
ElMessage.success(`已取消${message}!`)
})
}
</script>
<template>
@ -34,7 +48,7 @@ const content = computed(() => {
<card style="display: flex;width: 100%">
<el-button type="info" :icon="ArrowLeft" size="small"
round plain @click="router.push('/index')">返回列表</el-button>
<div style="text-align: center;flex: 1;height: 25px;transform: translateY(-8px)">
<div style="text-align: center;flex: 1;height: 25px">
<div class="topic-type"
:style="{color: store.findTypeById(topic.data.type)?.color+'DD',
'border-color': store.findTypeById(topic.data.type)?.color+'EE',
@ -42,9 +56,6 @@ const content = computed(() => {
{{ store.findTypeById(topic.data.type)?.name }}
</div>
<span style="margin-left: 5px;font-weight: bold">{{topic.data.title}}</span>
<div style="font-size: 12px;color: grey">
{{new Date(topic.data.time).toLocaleString()}}
</div>
</div>
</card>
</div>
@ -75,6 +86,20 @@ const content = computed(() => {
</div>
<div class="topic-main-right">
<div class="topic-content" v-html="content"></div>
<div style="font-size: 13px;color: grey;text-align: center;margin-top: 40px">
<div>发帖时间: {{new Date(topic.data.time).toLocaleString()}}</div>
</div>
<div style="text-align: right;margin-top: 30px">
<interact-button name="点个赞吧" check-name="已点赞" color="pink"
:check="topic.like" @check="interact('like', '点赞')">
<el-icon><CircleCheck /></el-icon>
</interact-button>
<interact-button name="收藏帖子" check-name="已收藏" color="orange"
:check="topic.collect" @check="interact('collect', '收藏')"
style="margin-left: 20px">
<el-icon><Star/></el-icon>
</interact-button>
</div>
</div>
</div>
</div>