完成点赞和收藏功能
This commit is contained in:
parent
fce060dc82
commit
e808240a32
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
45
my-project-frontend/src/components/InteractButton.vue
Normal file
45
my-project-frontend/src/components/InteractButton.vue
Normal 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>
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user