diff --git a/my-project-backend/src/main/java/com/example/controller/ForumController.java b/my-project-backend/src/main/java/com/example/controller/ForumController.java index a53530b..b3e1874 100644 --- a/my-project-backend/src/main/java/com/example/controller/ForumController.java +++ b/my-project-backend/src/main/java/com/example/controller/ForumController.java @@ -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 topic(@RequestParam @Min(0) int tid){ return RestBean.success(topicService.getTopic(tid)); } + + @GetMapping("/interact") + public RestBean 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(); + } } diff --git a/my-project-backend/src/main/java/com/example/entity/dto/Interact.java b/my-project-backend/src/main/java/com/example/entity/dto/Interact.java new file mode 100644 index 0000000..f55d5b9 --- /dev/null +++ b/my-project-backend/src/main/java/com/example/entity/dto/Interact.java @@ -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; + } +} diff --git a/my-project-backend/src/main/java/com/example/mapper/TopicMapper.java b/my-project-backend/src/main/java/com/example/mapper/TopicMapper.java index 66c860c..a5f9659 100644 --- a/my-project-backend/src/main/java/com/example/mapper/TopicMapper.java +++ b/my-project-backend/src/main/java/com/example/mapper/TopicMapper.java @@ -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 { + @Insert(""" + + """) + int addInteract(List interacts, String type); + + @Delete(""" + + """) + int deleteInteract(List interacts, String type); } diff --git a/my-project-backend/src/main/java/com/example/service/TopicService.java b/my-project-backend/src/main/java/com/example/service/TopicService.java index c7f804d..5f97b01 100644 --- a/my-project-backend/src/main/java/com/example/service/TopicService.java +++ b/my-project-backend/src/main/java/com/example/service/TopicService.java @@ -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 { List listTopicByPage(int pageNumber, int type); List topTopics(); TopicDetailVO getTopic(int tid); + void interact(Interact interact, boolean state); } diff --git a/my-project-backend/src/main/java/com/example/service/impl/TopicServiceImpl.java b/my-project-backend/src/main/java/com/example/service/impl/TopicServiceImpl.java index f19f60b..a6a1edb 100644 --- a/my-project-backend/src/main/java/com/example/service/impl/TopicServiceImpl.java +++ b/my-project-backend/src/main/java/com/example/service/impl/TopicServiceImpl.java @@ -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 implements TopicService { @@ -42,6 +46,9 @@ public class TopicServiceImpl extends ServiceImpl implements @Resource CacheUtils cacheUtils; + @Resource + StringRedisTemplate template; + @Override public List listType() { return typeMapper.selectList(null); @@ -108,6 +115,54 @@ public class TopicServiceImpl extends ServiceImpl 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 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 check = new LinkedList<>(); + List 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 fillUserDetailsByPrivacy(T target, int uid) { AccountDetails details = accountDetailsMapper.selectById(uid); Account account = accountMapper.selectById(uid); diff --git a/my-project-frontend/src/components/InteractButton.vue b/my-project-frontend/src/components/InteractButton.vue new file mode 100644 index 0000000..aad0d6e --- /dev/null +++ b/my-project-frontend/src/components/InteractButton.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/my-project-frontend/src/views/forum/TopicDetail.vue b/my-project-frontend/src/views/forum/TopicDetail.vue index aba0d64..f7986b7 100644 --- a/my-project-frontend/src/views/forum/TopicDetail.vue +++ b/my-project-frontend/src/views/forum/TopicDetail.vue @@ -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}!`) + }) +}