002 递归评论 mongodb websocket消息推送
文章目录
- 商品评论
- CommentController.java
- Comment.java
- CommentServiceImpl.java
- CommentRepository.java
- CommentService.java
- WebSocketConfig.java
- WebSocketProcess.java
- application.yaml
- productReview.html
- index.html
- index.js
- index.css
- 订单评论
- EvaluateMapper.xml
- EvaluateMapper.java
- EvaluateController.java
- Evaluate.java
- EvaluateServiceImpl.java
- IEvaluateService.java
- R.java
- orderReview.css
- orderReview.html
- orderReview.js
- mongodb
- pom.xml
- 递归评论
- 前端
- 后端
- 前后端
商品评论
CommentController.java
package com.fshop.controller;import com.fshop.entity.Comment;
import com.fshop.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;@RestController
@RequestMapping("/productReview/comments")
public class CommentController {@Autowiredprivate CommentService commentService;// 添加根据fruitId获取评论的映射方法@GetMapping("/byFruitId/{fruitId}")public List<Comment> getCommentsByFruitId(@PathVariable Integer fruitId) {return commentService.getCommentsByFruitId(fruitId);}@GetMappingpublic List<Comment> getAllComments() {return commentService.getAllComments();}@PostMappingpublic Comment addComment(@RequestBody Comment comment) {return commentService.addComment(comment);}@PostMapping("/{id}/replies")public Comment addReply(@PathVariable String id, @RequestBody Comment.Reply reply) {return commentService.addReply(id, reply);}@PutMapping("/{id}")public Comment updateComment(@PathVariable String id, @RequestBody Comment comment) {return commentService.updateComment(id, comment);}@DeleteMapping("/{id}")public void deleteComment(@PathVariable String id) {commentService.deleteComment(id);}
}
Comment.java
package com.fshop.entity;import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;import java.util.ArrayList;
import java.util.List;@Document(collection = "comments")
@Data
public class Comment {@Idprivate String id;private int evaluateId;private int fruitId;private int score;private int status;private OriginalPoster originalPoster;private List<Reply> replies = new ArrayList<>();@Datapublic static class OriginalPoster {private int userId;private String content;private String postedAt;}@Datapublic static class Reply {@Idprivate String id;private int userId;private String content;private String postedAt;private String parentId;private List<Reply> replies = new ArrayList<>();}
}
CommentServiceImpl.java
package com.fshop.service.impl;import com.fshop.entity.Comment;
import com.fshop.service.CommentRepository;
import com.fshop.service.CommentService;
import com.fshop.websocket2.WebSocketProcess;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;
import java.util.UUID;@Service
public class CommentServiceImpl implements CommentService {@Autowiredprivate CommentRepository commentRepository;@Autowiredprivate WebSocketProcess websocketProcess;@Overridepublic List<Comment> getAllComments() {return commentRepository.findAll();}@Overridepublic List<Comment> getCommentsByFruitId(Integer fruitId) {return commentRepository.findByFruitId(fruitId);}@Overridepublic Comment addComment(Comment comment) {comment.setId(UUID.randomUUID().toString()); // 生成随机的 `_id`comment.setReplies(new ArrayList<>()); // 确保 `replies` 是一个空列表return commentRepository.save(comment);}@Overridepublic Comment updateComment(String id, Comment comment) {comment.setId(id);return commentRepository.save(comment);}@Overridepublic void deleteComment(String id) {commentRepository.deleteById(id);}@Overridepublic Comment addReply(String id, Comment.Reply reply) {reply.setId(UUID.randomUUID().toString()); // 生成随机的 `_id` 为回复// 查找目标评论或回复Comment targetComment = findCommentById(id);if (targetComment != null) {// 如果找到了目标评论,添加回复addReplyToCommentOrReplies(targetComment, reply);websocketProcess.sendMsg(reply.getUserId(),reply.getContent());return commentRepository.save(targetComment);} else {// 否则,递归查找所有评论的嵌套回复List<Comment> allComments = commentRepository.findAll();for (Comment comment : allComments) {if (addReplyToNestedReplies(comment.getReplies(), id, reply)) {websocketProcess.sendMsg(reply.getUserId(), reply.getContent());return commentRepository.save(comment);}}throw new RuntimeException("Comment not found");}}private Comment findCommentById(String id) {return commentRepository.findById(id).orElse(null);}private boolean addReplyToNestedReplies(List<Comment.Reply> replies, String parentId, Comment.Reply replyToAdd) {for (Comment.Reply reply : replies) {if (reply.getId().equals(parentId)) {reply.getReplies().add(replyToAdd);return true;} else {if (addReplyToNestedReplies(reply.getReplies(), parentId, replyToAdd)) {return true;}}}return false;}private void addReplyToCommentOrReplies(Comment comment, Comment.Reply reply) {if (comment.getId().equals(reply.getParentId())) {comment.getReplies().add(reply);} else {addReplyToNestedReplies(comment.getReplies(), reply.getParentId(), reply);}}
}
CommentRepository.java
package com.fshop.service;import com.fshop.entity.Comment;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;import java.util.List;@Repository
public interface CommentRepository extends MongoRepository<Comment, String> {// 添加根据fruitId查找评论的方法List<Comment> findByFruitId(Integer fruitId);}
CommentService.java
package com.fshop.service;import com.fshop.entity.Comment;import java.util.List;public interface CommentService {List<Comment> getAllComments();Comment addComment(Comment comment);Comment updateComment(String id, Comment comment);void deleteComment(String id);// Comment findCommentById(String id);Comment addReply(String id, Comment.Reply reply);// 添加根据fruitId获取评论列表的方法声明List<Comment> getCommentsByFruitId(Integer fruitId);
}
WebSocketConfig.java
package com.fshop.websocket2;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}
}
WebSocketProcess.java
package com.fshop.websocket2;/*** 该类封装了 客户端与服务器端的Websocket 通讯的* (1) 连接对象的管理 ConcurrentHashMap<Long, WebSocketProcess>* (2) 事件监听 @OnOpen , @OnMessage, @OnClose , @OnError* (3) 服务器向 (所有/单个)客户端 发送消息*/import org.springframework.stereotype.Component;import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;/*** 1. manage client and sever socket object(concurrentHashMap)* 2. event trigger :* receive client connect : onopen* receive message from client : onmessage* client socket close :onclose** 3. server send message to client*/
@Component
@ServerEndpoint(value = "/testWebSocket/{id}")
public class WebSocketProcess {private static ConcurrentHashMap<Integer,WebSocketProcess> map = new ConcurrentHashMap();private Session session;@OnOpenpublic void onOpen(Session session, @PathParam("id") Integer clientId){this.session = session;map.put(clientId,this);System.out.println("server get a socket from client :" + clientId);}// receive message from client : onmessage@OnMessagepublic void onMessage(String message, @PathParam("id") Integer clientId){System.out.println("server get message from client id:" + clientId+", and message is :" + message);}@OnClosepublic void onClose(Session session, @PathParam("id") Integer clientId){map.remove(clientId);}// server send message to clientpublic void sendMsg(Integer clientId,String message) {WebSocketProcess socket = map.get(clientId);if(socket!=null){if(socket.session.isOpen()){try {socket.session.getBasicRemote().sendText(message);System.out.println("server has send message to client :"+clientId +", and message is:"+ message);} catch (IOException e) {e.printStackTrace();}}else{System.out.println("this client "+clientId +" socket has closed");}}else{System.out.println("this client "+clientId +" socket has exit");}}public void sendMsg(String message) throws IOException {Set<Map.Entry<Integer, WebSocketProcess>> entrySet = map.entrySet();for(Map.Entry<Integer, WebSocketProcess> entry: entrySet ){Integer clientId = entry.getKey();WebSocketProcess socket = entry.getValue();if(socket!=null){if(socket.session.isOpen()){socket.session.getBasicRemote().sendText(message);}else{System.out.println("this client "+clientId +" socket has closed");}}else{System.out.println("this client "+clientId +" socket has exit");}}}}
application.yaml
spring:datasource:druid:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/fshop_app?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: devpassword: 123456initial-size: 5 # 初始化连接池大小max-active: 20 # 最大连接数min-idle: 10 # 最小连接数max-wait: 60000 # 超时等待时间min-evictable-idle-time-millis: 600000 # 连接在连接池中的最小生存时间max-evictable-idle-time-millis: 900000 # 连接在连接池中的最大生存时间time-between-eviction-runs-millis: 2000 # 配置间隔多久进行一次检测,检测需要关闭的空闲连接test-while-idle: true # 从连接池中获取连接时,当连接空闲时间大于timeBetweenEvictionRunsMillis时检查连接有效性phy-max-use-count: 1000 # 配置一个连接最大使用次数,避免长时间使用相同连接造成服务器端负载不均衡spring:data:mongodb:uri: mongodb://abc:123456@localhost:27017/commentDB
productReview.html
<!DOCTYPE html>
<html>
<head><title>Fruit Comments</title><script src="../common/jquery-3.3.1.min.js"></script><style>body {font-family: Arial, sans-serif;color: #333;}.comment, .reply {margin-bottom: 20px;padding: 15px;border: 1px solid #ddd;border-radius: 8px;background-color: #fff;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);}.reply {margin-left: 60px; /* 增加缩进以更好地显示嵌套回复 */}.reply-form {margin-top: 10px;margin-left: 40px;}.reply-form textarea {width: calc(100% - 20px); /* 留出一些空间 */padding: 10px;border: 1px solid #ddd;border-radius: 5px;resize: vertical;}.reply-button {cursor: pointer;color: #007BFF; /* 蓝色 */text-decoration: underline;font-weight: bold; /* 加粗字体 */}.reply-button:hover {color: #0056b3; /* 鼠标悬停颜色变深 */}button[type="submit"] {padding: 5px 10px;background-color: #007BFF;color: white;border: none;border-radius: 5px;cursor: pointer;}button[type="submit"]:hover {background-color: #0056b3; /* 鼠标悬停颜色变深 */}</style>
</head>
<body>
<h1>Comments</h1>
<div id="content"></div>
<div id="comments"></div><script>// 从URL参数中获取fruitIdfunction getQueryParameterByName(name, url) {if (!url) url = window.location.href;name = name.replace(/[\[\]]/g, '\\$&');var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),results = regex.exec(url);if (!results) return null;if (!results[2]) return '';return decodeURIComponent(results[2].replace(/\+/g, ' '));}$(document).ready(function() {// // 获取fruitId// var fruitId = getParameterByName('fruitId');// if (!fruitId) {// alert('No fruitId provided!');// return;// }//解析userIdfunction getUserIdFromToken(token) {if (!token) {return null;}const base64Url = token.split('.')[1];const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');try {const decodedPayload = JSON.parse(atob(base64));return decodedPayload.userId; // 假设JWT的payload中包含userId字段} catch (e) {console.error('Error decoding JWT payload', e);return null;}}// 使用示例const token = localStorage.getItem('token'); // 假设你将token保存在localStorage中const userId = getUserIdFromToken(token);console.log(userId); // 输出userIdloadComments();function loadComments() {var fruitId = 43;$.ajax({url: 'comments/byFruitId/'+ fruitId, // 在这里使用fruitIdmethod: 'GET',success: function(data) {let commentsHtml = '';data.forEach(comment => {commentsHtml += renderComment(comment);});$('#comments').html(commentsHtml);}});}function renderComment(comment) {let commentHtml = '<div class="comment">';commentHtml += '<p><strong>' + comment.originalPoster.userId + ':</strong> ' + comment.originalPoster.content + ' (' + new Date(comment.originalPoster.postedAt).toLocaleString() + ')</p>';commentHtml += '<span class="reply-button" data-id="' + comment.id + '">Reply</span>';if (comment.replies && comment.replies.length > 0) {commentHtml += renderReplies(comment.replies);}commentHtml += '<div class="reply-form" id="reply-form-' + comment.id + '" style="display:none;">';commentHtml += '<textarea id="reply-content-' + comment.id + '"></textarea><br>';commentHtml += '<button οnclick="addReply(\'' + comment.id + '\')">Submit</button>';commentHtml += '</div>';commentHtml += '</div>';return commentHtml;}function renderReplies(replies) {let repliesHtml = '<div style="margin-left: 20px;">';replies.forEach(reply => {repliesHtml += '<div class="reply">';repliesHtml += '<p><strong>' + reply.userId + ':</strong> ' + reply.content + ' (' + new Date(reply.postedAt).toLocaleString() + ')</p>';repliesHtml += '<span class="reply-button" data-id="' + reply.id + '">Reply</span>';if (reply.replies && reply.replies.length > 0) {repliesHtml += renderReplies(reply.replies);}repliesHtml += '<div class="reply-form" id="reply-form-' + reply.id + '" style="display:none;">';repliesHtml += '<textarea id="reply-content-' + reply.id + '"></textarea><br>';repliesHtml += '<button οnclick="addReply(\'' + reply.id + '\')">Submit</button>';repliesHtml += '</div>';repliesHtml += '</div>';});repliesHtml += '</div>';return repliesHtml;}$(document).on('click', '.reply-button', function() {let replyFormId = '#reply-form-' + $(this).data('id');$(replyFormId).toggle();});window.addReply = function(parentId) {let replyContentId = '#reply-content-' + parentId;let content = $(replyContentId).val();let reply = {userId: userId,content: content,postedAt: new Date().toISOString(),parentId: parentId};$.ajax({url: 'comments/' + parentId + '/replies',method: 'POST',contentType: 'application/json',data: JSON.stringify(reply),success: function() {loadComments();}});}});
</script></body>
</html>
index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>果粒优选</title><link rel="icon" href="./favicon.png" type="image/x-icon"><link rel="stylesheet" href="./common/element-ui/lib/theme-chalk/index.css"><script src="./common/jquery-3.3.1.min.js"></script><script src="./common/vue.js"></script><script src="./common/element-ui/lib/index.js"></script><link rel="stylesheet" href="./css/default.css" /><link rel="stylesheet" href="./css/index.css" /><style type="text/css">body{z-index: -100;}#app{width: 100%;height: 100vh;}#bg{position: fixed;left: 50%;z-index: -50;height: 100vh;width: 1300px;//transform: translate(-50%, 0); /*水平居中*///background-color: #F2F6FC;}</style>
</head><body ><div id="app"><div id="bg"></div><!-- 顶部工具栏 --><div id="tool-nav"><div class="center clearfix"><ul class="fl"><li class="tool-nav-li"><span>欢迎来到果粒优选!</span></li><li id="login-if" class="tool-nav-li enable-click"><a id="login-if-title" href="./html/user/login.html">登录/注册</a><ul id="login-if-body" class="submenu-detail"><li><a id="userIndex" href="myaccount/user_index.html">个人中心</a></li><li><a href="">退出登录</a></li></ul></li></ul><ul class="fr"><li id="login-if-history" class="tool-nav-li enable-click submenu"><span class="submenu-title"><a href="./html/user/user-order.html">历史订单</a></span></li><li class="tool-nav-li enable-click submenu"><span class="submenu-title">手机版</span><img class="submenu-detail" src="./images/image.png" alt=""></li><li class="tool-nav-li enable-click submenu"><span class="submenu-title">网站导航</span></li><li class="tool-nav-li enable-click submenu"><span class="submenu-title">客户服务</span><ul class="submenu-detail"><li><a href="">服务中心</a></li><li><a href="">联系客服</a></li></ul></li><li id="message-popup-container" class="tool-nav-li"><div id="message-popup" class="hidden" style=""><div class="message-content"><p class="message-text"></p><button id="close-popup">关闭</button></div></div></li></ul></div></div><!-- 顶部搜索栏 --><div id="head-search" class="center clearfix"><h1><img src="./images/logo.png" alt=""></h1><form action=""><input type="text" name="search-keywords" placeholder="请输入搜索关键字..."><button>搜索</button></form><div><a id="cart" class="btn-normal-designer" href="#">购物车</a></div></div><!-- 顶部导航栏 --><div id="head-nav"><ul class="clearfix center"><li><a href="#" @click="clickIndex()">首页</a></li><li><a href="#" @click="clickRanking()">排行榜</a></li><li><a href="#">当季热卖</a></li><li><a href="#">活动</a></li><li><a href="#">领券广场</a></li><li><a href="#">关于我们</a></li></ul></div><iframe id="iframe" :src="slider" style="overflow: hidden;"></iframe></div></body>
<script type="text/javascript" src="./js/index.js"></script><script>var userId;$(document).ready(function() {$.ajax({url: 'http://localhost:8080/fshop/user/loginUserName',type: 'GET', // 或者 'POST', 'PUT', 'DELETE' 等dataType: 'json', // 预期服务器返回的数据类型,如 'json', 'xml', 'html', 'text'// 如果请求需要发送数据,可以使用 data 属性// data: {// key1: 'value1',// key2: 'value2'// },// 如果请求需要认证信息,如设置请求头,可以使用 headers 属性headers:{'token': localStorage.getItem("token")},success: function(response, textStatus, jqXHR) {// 请求成功时调用的函数console.log('请求成功:', response.data.userId);userId = response.data.userId;// 在这里处理返回的数据let ws ;if('WebSocket' in window){console.log("this broswer is support websocket.");console.log("USERID是:"+userId);<!-- build webscoket to server -->ws = new WebSocket("ws://localhost:8080/fshop/testWebSocket/"+userId);ws.onopen = function (){console.log("用户1已经连接");}// receive message from serverws.onmessage = function (event){var message = event.data;// 显示WebSocket接收到的消息到弹框内$("#message-popup .message-text").text("用户id=1发了条消息,消息是: " + message);// 可以设置延迟显示弹框,以提供更好的用户体验setTimeout(function() {$('#message-popup').removeClass('hidden'); // 显示弹框}, 2000); // 延迟2秒显示弹框,你可以根据需要调整这个时间// 自动隐藏弹框(可选)setTimeout(function() {$('#message-popup').addClass('hidden'); // 隐藏弹框}, 5000); // 5秒后自动隐藏弹框}ws.onclose = function (){console.log("client id= 1" +"has closed, disconnect to server")}}else{console.log("this browser is not support websocket.")}},error: function(jqXHR, textStatus, errorThrown) {// 请求失败时调用的函数console.error('请求失败:', textStatus, errorThrown);// 在这里处理错误情况}});// 监听关闭按钮的点击事件$('#close-popup').click(function() {// 清空p标签的内容$('.message-text').text(''); // 使用text('')来清空文本内容// 同时隐藏消息弹框(如果之前没有隐藏的话)$('#message-popup').addClass('hidden');});})</script>
</html>
index.js
new Vue({el: '#app',data() {return {//需要跳转的页面//默认页面slider: './index-slider.html',//首页index: './index-slider.html',//排行榜ranking: './html/fruit/fruit-ranking.html',//当季热卖//活动//领券广场//关于我们}},methods: {//分类鼠标移入函数handleMouseEnter() {this.$refs.ul_show.style.display = 'block'},//分类鼠标移出函数handleMouseLeave() {this.$refs.ul_show.style.display = 'none'},//点击排行榜页面跳转clickRanking() {this.slider = this.ranking;},//点击首页跳转clickIndex() {this.slider = this.index;}}
});let token = localStorage.getItem('token');console.log(token);let loginIf = document.getElementById('login-if');
let loginIfTitle = document.getElementById('login-if-title');
let loginIfBody = document.getElementById('login-if-body');
let loginIfHistory = document.getElementById('login-if-history');if (token != null && token !== '') {// 已经登陆,在工具栏显示用户名$.ajax({url: '/fshop/user/loginUserName',type: 'GET',headers: {'token': token},dataType: 'JSON',success: function (result) {if (result.code === 1) {loginIfTitle.innerText = result.data.userName;loginIfTitle.setAttribute('href', './html/user/user-evaluate.html');loginIf.classList.add('submenu');loginIfHistory.removeAttribute('hidden');}}});
} else {loginIf.classList.remove('submenu');loginIfTitle.innerText = '请登录/注册';loginIfTitle.setAttribute('href', './html/user/login.html');loginIfHistory.setAttribute('hidden', 'hidden');
}//浏览器关闭删除localstroge中的数据window.addEventListener('beforeunload', function (event) {var fruitId = localStorage.getItem('fruitId')var fruitCount = localStorage.getItem('fruitCount')var fruitStandard = localStorage.getItem('fruitStandard')if(fruitId != '' || fruitCount != '' || fruitStandard != ''){localStorage.removeItem('fruitId');localStorage.removeItem('fruitCount')localStorage.removeItem('fruitStandard')}
});
index.css
/* 顶部工具栏 */#tool-nav {/* 工具栏位置固定 */position: fixed;z-index: 1000;width: 100%;height: 50px;background-color: rgb(8, 5, 0);font-size: 14px;color: rgb(194, 191, 191);line-height: 50px;
}.tool-nav-li {float: left;
}.tool-nav-li a,
.tool-nav-li span {display: block;padding: 0 20px;
}.enable-click:hover {background-color: #484848;
}/* 可折叠菜单 */
.submenu-title {cursor: pointer;
}.submenu-detail {display: none;
}img.submenu-detail {width: 85px;box-shadow: 10px 10px 10px;
}ul.submenu-detail {background-color: #f8f7f7;box-shadow: 5px 5px 10px;color: #484848;
}ul.submenu-detail li:hover {background-color: #dbdada;}.submenu:hover .submenu-detail {display: inline-block;position: absolute;top: 100%;z-index: 1000;
}/* 顶部搜索栏 */
#head-search {position: relative;top: 55px;height: 140px;
}#head-search>* {float: left;height: 100%;
}#head-search h1 img {height: 100%;cursor: pointer;
}#head-search form {position: relative;width: 600px;height: 100%;margin-left: 20px;
}#head-search form input[type='text'] {position: absolute;top: 50%;transform: translate(0%, -50%);width: 80%;height: 45px;padding: 0 20px;border: 1px solid rgb(255, 119, 0);border-radius: 45px 0 0 45px;outline: none;
}#head-search form button {position: absolute;top: 50%;right: 0;transform: translate(0, -50%);width: 20%;height: 45px;border-radius: 0 45px 45px 0;background-color: rgb(255, 119, 0);color: white;cursor: pointer;
}.btn-normal-designer {width: 200px;height: 45px;border-radius: 45px;background-color: rgb(255, 119, 0);color: white;text-align: center;line-height: 45px;
}#head-search form button:hover,
.btn-normal-designer:hover {background-color: rgb(217, 102, 2);color: white;
}#cart {position: absolute;top: 50%;transform: translate(0, -50%);margin-left: 20px;
}/* 顶部导航栏 */
#head-nav{position: relative;top: 50px;width: 100%;height: 50px;background-color: rgb(255, 119, 0);color: rgb(231, 231, 231);line-height: 50px;} #head-nav li{float: left;padding: 0 40px;
}#head-nav li:hover{color: white;
}#app{width: 100%;height: 100%;
}
#iframe{position: relative;top: 60px;width: 100%;height: 65vh;
}#login-if-body{width: 102px;text-align: center;
}/*弹框*/#message-popup {position: absolute;right: 20px; /* 根据需要调整位置 */top: 50px; /* 根据需要调整位置 */width: 300px; /* 根据需要调整宽度 */background-color: #fff;border-radius: 5px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);padding: 15px;z-index: 1000; /* 确保弹框在其他元素之上 */display: flex;flex-direction: column;align-items: flex-start;
}#message-popup.hidden {display: none;
}.message-content {display: flex;flex-direction: column;align-items: flex-start;
}.message-text {font-size: 16px;line-height: 1.5;margin-bottom: 10px;
}#close-popup {font-size: 14px;padding: 5px 10px;background-color: #eee;border: none;border-radius: 3px;cursor: pointer;transition: background-color 0.2s ease;
}#close-popup:hover {background-color: #ddd;
}
订单评论
EvaluateMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fshop.mapper.EvaluateMapper"><resultMap id="Evaluate" type="com.fshop.entity.Evaluate"/><resultMap id="EvaluateDto" type="com.fshop.dto.EvaluateDto"/><!-- 查询所有评论 --><select id="getEvaluateInfo" resultMap="EvaluateDto">select user.user_id, user.user_name, user.user_avatar_url, fruit.fruit_id, evaluate.evaluate_infofrom user,fruit,(select myorder_id, user_id, fruit_id from myorder where myorder_status = 3 and status = 0) myorder,(select myorder_id, evaluate_info, evaluate_create_time from evaluate where status = 1) evaluatewhere myorder.myorder_id = evaluate.myorder_idand myorder.fruit_id = #{fruitId}order by evaluate.evaluate_create_time desc limit #{currentPage},#{queryCount}</select>
</mapper>
EvaluateMapper.java
package com.fshop.mapper;import com.fshop.dto.EvaluateDto;
import com.fshop.entity.Evaluate;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;import java.util.List;/*** <p>* 订单评价表 Mapper 接口* </p>** @author dev* @since 2024-04-23*/
public interface EvaluateMapper extends BaseMapper<Evaluate> {List<EvaluateDto> getEvaluateInfo(@Param("fruitId") Integer fruitId , @Param("currentPage") Integer currentPage , @Param("queryCount") Integer queryCount );}
EvaluateController.java
package com.fshop.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fshop.common.R;
import com.fshop.entity.Evaluate;
import com.fshop.service.CommentService;
import com.fshop.service.IEvaluateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;/*** <p>* 订单评价表 前端控制器* </p>** @author dev* @since 2024-04-23*/
@RestController
@RequestMapping("/evaluate")
public class EvaluateController {@Autowiredprivate IEvaluateService evaluateService;// 分页查询@GetMappingpublic R<Page<Evaluate>> getAll(HttpServletRequest request, Integer pageNum) {// 获取tokenString token = request.getHeader("token");// System.out.println(token);// System.out.println(pageNum);return evaluateService.getAll(token, pageNum);}// 按ID查询评论@GetMapping("/{evaluateId}")public R<Evaluate> getEvaluateById(@PathVariable String evaluateId) {return null;}// 删除评论@PostMapping("remove")public R<String> removeEvaluate(HttpServletRequest request, String evaluateId) {// 获取tokenString token = request.getHeader("token");//System.out.println(token);// System.out.println(evaluateId);return evaluateService.removeEvaluate(token, evaluateId);}// 添加评论@PostMapping("save")public R<String> saveEvaluate(Evaluate evaluate,HttpServletRequest request) {String token = request.getHeader("token");System.out.println("controller层"+token);return evaluateService.saveEvaluate(token,evaluate);}//查询所有评论并返回用户ID、用户名称、用户头像以及用户评论@GetMapping("getEvaluateInfo/{fruitId}/{currentPage}/{queryCount}")public R getEvaluateInfo(@PathVariable("fruitId") Integer fruitId,@PathVariable("currentPage") Integer currentPage,@PathVariable("queryCount") Integer queryCount){return evaluateService.getEvaluateInfo(fruitId,currentPage,queryCount);}
}
Evaluate.java
package com.fshop.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;
import java.time.LocalDateTime;/*** <p>* 订单评价表* </p>** @author dev* @since 2024-04-23*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Evaluate implements Serializable {private static final long serialVersionUID = 1L;/*** 评价ID*/@TableId(value = "evaluate_id", type = IdType.AUTO)private String evaluateId;/*** 用户ID,外键(关联用户表)*/private Integer userId;/*** 评价内容*/private byte[] evaluateInfo;/*** 评价分数*/private Integer evaluateScore;/*** 订单ID,外键(关联订单表)*/private Integer myorderId;/*** 评价状态*/private Integer status;/*** 版本*/private Integer version;/*** 创建(评价)时间*/@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime evaluateCreateTime;/*** 最近更新时间*/@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime updateTime;private String other1;private String other2;private Integer fruitId;}
EvaluateServiceImpl.java
package com.fshop.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fshop.common.PageHelper;
import com.fshop.common.R;
import com.fshop.dto.LoginUserDto;
import com.fshop.entity.Comment;
import com.fshop.entity.Evaluate;
import com.fshop.mapper.EvaluateMapper;
import com.fshop.service.CommentRepository;
import com.fshop.service.IEvaluateService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fshop.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;/*** <p>* 订单评价表 服务实现类* </p>** @author dev* @since 2024-04-23*/
@Service
public class EvaluateServiceImpl extends ServiceImpl<EvaluateMapper, Evaluate> implements IEvaluateService {@Autowiredprivate EvaluateMapper evaluateMapper;@Autowiredprivate CommentRepository commentRepository;@Overridepublic R<Page<Evaluate>> getAll(String token, Integer pageNum) {// 解析tokenLoginUserDto loginUser = JwtUtil.parseToken(token);// 查询所有,user_id等于loginUser.getUserId,且status等于1的评论QueryWrapper<Evaluate> wrapper = new QueryWrapper<>();wrapper.eq("user_id", loginUser.getUserId()).eq("status", 1);// 分页,每页显示10条评论Page<Evaluate> page = new Page<>(pageNum, PageHelper.EVALUATE_PAGE_SIZE);page = baseMapper.selectPage(page, wrapper);if (page != null) {return R.ok("查询成功", page);}return R.error("查询失败");}@Overridepublic R<String> removeEvaluate(String token, String evaluateId) {// 先查询LoginUserDto loginUser = JwtUtil.parseToken(token);QueryWrapper<Evaluate> wrapper = new QueryWrapper<>();wrapper.eq("user_id", loginUser.getUserId()).eq("evaluate_id", evaluateId).eq("status", 1);Evaluate evaluate = baseMapper.selectOne(wrapper);// System.out.println(evaluate);if (evaluate != null) {evaluate.setStatus(0);int update = baseMapper.update(evaluate, wrapper);if (update > 0) {return R.ok("删除成功");}}return R.error("查询失败");}@Overridepublic R<Evaluate> getById(String token, String evaluateId) {// 解析tokenLoginUserDto loginUser = JwtUtil.parseToken(token);QueryWrapper<Evaluate> wrapper = new QueryWrapper<>();wrapper.eq("user_id", loginUser.getUserId()).eq("evaluate_id", evaluateId).eq("status", 1);Evaluate evaluate = baseMapper.selectOne(wrapper);if (evaluate != null) {return R.ok("查询成功", evaluate);}return R.error("查询失败");}//查询所有评论并返回用户ID、用户名称、用户头像以及用户评论@Overridepublic R getEvaluateInfo(Integer fruitId,Integer currentPage, Integer queryCount) {Integer preCurrentPage = (currentPage - 1) * queryCount;return R.ok(evaluateMapper.getEvaluateInfo(fruitId,preCurrentPage , queryCount));}@Overridepublic R<String> saveEvaluate(String token, Evaluate evaluate){LoginUserDto loginUser = JwtUtil.parseToken(token);Integer tokenUserId = loginUser.getUserId();Integer userId = evaluate.getUserId();Comment comment = new Comment();comment.setId(UUID.randomUUID().toString()); // 生成随机的 `_id`evaluate.setEvaluateId(comment.getId());comment.setFruitId(43);//假数据comment.setScore(evaluate.getEvaluateScore());comment.setStatus(1);// 创建一个OriginalPoster对象并设置其字段值Comment.OriginalPoster originalPoster = new Comment.OriginalPoster();originalPoster.setUserId(evaluate.getUserId()); // 假设用户ID是123originalPoster.setContent( new String(evaluate.getEvaluateInfo()));LocalDateTime now = LocalDateTime.now();// 对于LocalDateTime,应该直接使用ISO_LOCAL_DATE_TIMEDateTimeFormatter isoLocalDateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;String iso8601String = now.format(isoLocalDateTimeFormatter);originalPoster.setPostedAt(iso8601String); // 使用ISO 8601格式的日期时间字符串comment.setOriginalPoster(originalPoster);comment.setReplies(new ArrayList<>()); // 确保 `replies` 是一个空列表commentRepository.save(comment);// var evaluationData = {// //userId: userId,// myorderId: myorderId,// evaluateInfo: evaluateInfo,// evaluateScore: evaluateScore// };if (tokenUserId == null || userId == null) {// 至少有一个ID是null,因此它们不相等return R.error("添加失败");} else if (tokenUserId != null && tokenUserId.equals(userId)) {// 两个ID相等evaluate.setStatus(1);evaluate.setEvaluateCreateTime(LocalDateTime.now());evaluate.setUpdateTime(LocalDateTime.now());evaluate.setFruitId(43);int insert = evaluateMapper.insert(evaluate);if(insert > 0){return R.ok("添加成功");}else{return R.error("添加失败");}} else {// 两个ID都不为null,但不相等return R.error("添加失败");}}
}
IEvaluateService.java
package com.fshop.service;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fshop.common.R;
import com.fshop.entity.Evaluate;
import com.baomidou.mybatisplus.extension.service.IService;import java.util.List;/*** <p>* 订单评价表 服务类* </p>** @author dev* @since 2024-04-23**/
public interface IEvaluateService extends IService<Evaluate> {R<Page<Evaluate>> getAll(String token, Integer pageNum);R<String> removeEvaluate(String token, String evaluateId);R<Evaluate> getById(String token, String evaluateId);//商品详情页获取所有评论R getEvaluateInfo(Integer fruitId, Integer currentPage, Integer queryCount);//增加评论R<String> saveEvaluate(String token, Evaluate evaluate);
}
R.java
package com.fshop.common;import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;import java.io.Serializable;// @JsonInclude 保证序列化json的时候, 如果是null的对象, key也会消失
@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R<T> implements Serializable {private static final long serialVersionUID = 7735505903525411467L;// 成功值,默认为1private static final int SUCCESS_CODE = 1;// 失败值,默认为0private static final int ERROR_CODE = 0;// 状态码private final int code;// 消息private String msg;// 返回数据private T data;private R(int code) {this.code = code;}private R(int code, T data) {this.code = code;this.data = data;}private R(int code, String msg) {this.code = code;this.msg = msg;}private R(int code, String msg, T data) {this.code = code;this.msg = msg;this.data = data;}public static <T> R<T> ok() {return new R<T>(SUCCESS_CODE, "success");}public static <T> R<T> ok(String msg) {return new R<T>(SUCCESS_CODE, msg);}public static <T> R<T> ok(T data) {return new R<T>(SUCCESS_CODE, data);}public static <T> R<T> ok(String msg, T data) {return new R<T>(SUCCESS_CODE, msg, data);}public static <T> R<T> error() {return new R<T>(ERROR_CODE, "error");}public static <T> R<T> error(String msg) {return new R<T>(ERROR_CODE, msg);}public static <T> R<T> error(int code, String msg) {return new R<T>(code, msg);}public static <T> R<T> error(ResponseCode res) {return new R<T>(res.getCode(), res.getMessage());}@Overridepublic String toString() {return "R{" +"code=" + code +", msg='" + msg + '\'' +", data=" + data +'}';}
}
orderReview.css
body {font-family: Arial, sans-serif;background-color: #f4f4f9;margin: 0;padding: 0;
}.evaluation-container {max-width: 600px;margin: 50px auto;padding: 20px;background-color: #fff;border-radius: 8px;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}h1 {text-align: center;color: #333;
}.form-group {margin-bottom: 20px;
}.form-group label {display: block;font-weight: bold;margin-bottom: 5px;color: #555;
}.form-group input, .form-group textarea, .form-group select {width: 100%;padding: 10px;border: 1px solid #ccc;border-radius: 4px;font-size: 16px;box-sizing: border-box;
}button {width: 100%;padding: 15px;background-color: #007bff;border: none;border-radius: 4px;color: #fff;font-size: 18px;cursor: pointer;
}button:hover {background-color: #0056b3;
}
orderReview.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>评价页面</title><link rel="stylesheet" href="orderReview.css">
</head>
<body><div class="evaluation-container"><h1>评价页面</h1><!-- 隐藏的用户ID --><input type="hidden" id="userId" name="userId" value="用户ID"><!-- 隐藏的订单ID --><input type="hidden" id="myorderId" name="myorderId" value="订单ID"><div class="form-group"><label for="evaluateInfo">评价内容:</label><textarea id="evaluateInfo" name="evaluateInfo" rows="4"></textarea></div><div class="form-group"><label for="evaluateScore">评价分数:</label><select id="evaluateScore" name="evaluateScore"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option></select></div><button id="submitBtn">提交评价</button></div><script src="../common/jquery-3.3.1.min.js"></script><script src="orderReview.js"></script>
</body>
</html>
orderReview.js
$(document).ready(function() {// 从 URL 中获取参数并赋值给输入框function getQueryParams() {var params = new URLSearchParams(window.location.search);var userId = params.get('userId');var myorderId = params.get('myorderId');if (userId) {$('#userId').val(userId);}if (myorderId) {$('#myorderId').val(myorderId);}}getQueryParams();$('#submitBtn').click(function() {var userId = $('#userId').val();var myorderId = $('#myorderId').val();var evaluateInfo = $('#evaluateInfo').val();var evaluateScore = $('#evaluateScore').val();// 这里可以添加对输入的验证代码if(userId && myorderId && evaluateInfo && evaluateScore) {var evaluationData = {userId: userId,myorderId: myorderId,evaluateInfo: evaluateInfo,evaluateScore: evaluateScore};// 发送评价数据到服务器$.ajax({url: 'http://localhost:8080/fshop/evaluate/save',type: 'POST',data: evaluationData,headers:{'token': localStorage.getItem("token")},success: function(response) {if(response.code == 1){alert('评价提交成功!');}else{alert("评价提交失败! ");}},error: function(error) {alert('提交失败,请重试。');}});} else {alert('请填写所有字段。');}});
});
mongodb
db.createUser({ user: "abc", pwd: "123456", roles: [{ role: "dbOwner", db: "commentDB" }] });
db.comments.insert({"_id": "1","evaluateId": 8888,"fruitId": 44,"score": 3,"status": 1,"originalPoster": {"userId": 4,"content": "I'm not satisfied with this product.","postedAt": ISODate("2023-04-24T10:00:00Z")},"replies": [{"_id": "2","userId": 5,"content": "Sorry to hear that, can you please elaborate?","postedAt": ISODate("2023-04-24T11:00:00Z"),"parentId": "8888","replies": []}]
});
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.6</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.fshop</groupId><artifactId>fshop-app</artifactId><version>0.0.1-SNAPSHOT</version><name>fshop-app</name><description>fshop-app</description><packaging>war</packaging><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- Spring Data MongoDB --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId></dependency><!--短信--><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.13</version></dependency><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpcore</artifactId><version>4.4.15</version></dependency><!-- SPRINGBOOT --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId><scope>provided</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- JDBC --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.33</version></dependency><!-- MYBATIS PLUS --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version></dependency><!-- DRUID --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.9</version></dependency><!--REDIS--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- JWT --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- ALIPAY --><dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>3.1.0</version></dependency><!-- LOMBOK --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- rabbitmq --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId><version>2.9.10</version></dependency><!-- servlet --><!--<dependency><groupId>javax.servlet.jsp.jstl</groupId><artifactId>jstl</artifactId><version>1.2</version></dependency>--><!-- 牛🐎云相关依赖 --><dependency><groupId>com.qiniu</groupId><artifactId>qiniu-java-sdk</artifactId><version>7.13.0</version></dependency><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>3.14.2</version><scope>compile</scope></dependency><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.5</version><scope>compile</scope></dependency><dependency><groupId>com.qiniu</groupId><artifactId>happy-dns-java</artifactId><version>0.1.6</version><scope>test</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><!-- WebSocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.7.6</version></plugin></plugins></build></project>
递归评论
前端
递归渲染逻辑图
renderComment(comment)||-- commentHtml (包含评论内容和回复按钮)||-- if (comment.replies 存在)||-- renderReplies(comment.replies)||-- repliesHtml (包含每个回复)||-- for each reply in comment.replies||-- replyHtml (包含回复内容和回复按钮)||-- if (reply.replies 存在)||-- renderReplies(reply.replies)
comment1├── reply1.1| ├── reply1.1.1| └── reply1.1.2├── reply1.2| └── reply1.2.1└── reply1.3
comment2└── reply2.1├── reply2.1.1└── reply2.1.2
comment3├── reply3.1└── reply3.2└── reply3.2.1
渲染主评论:调用renderComment(comment),生成主评论的HTML。
如果该评论有回复,调用renderReplies(comment.replies)进行渲染。
渲染回复:renderReplies(replies)会遍历每个回复,生成每个回复的HTML。
对每个回复,如果存在嵌套回复,再次调用renderReplies(reply.replies),以此类推,直到没有更多的回复。
function renderComment(comment) {let commentHtml = '<div class="comment">';commentHtml += '<p><strong>' + comment.originalPoster.userId + ':</strong> ' + comment.originalPoster.content + ' (' + new Date(comment.originalPoster.postedAt).toLocaleString() + ')</p>';commentHtml += '<span class="reply-button" data-id="' + comment.id + '">Reply</span>';if (comment.replies && comment.replies.length > 0) {commentHtml += renderReplies(comment.replies);}commentHtml += '<div class="reply-form" id="reply-form-' + comment.id + '" style="display:none;">';commentHtml += '<textarea id="reply-content-' + comment.id + '"></textarea><br>';commentHtml += '<button onclick="addReply(\'' + comment.id + '\')">Submit</button>';commentHtml += '</div>';commentHtml += '</div>';return commentHtml;
}function renderReplies(replies) {let repliesHtml = '<div style="margin-left: 20px;">';replies.forEach(reply => {repliesHtml += '<div class="reply">';repliesHtml += '<p><strong>' + reply.userId + ':</strong> ' + reply.content + ' (' + new Date(reply.postedAt).toLocaleString() + ')</p>';repliesHtml += '<span class="reply-button" data-id="' + reply.id + '">Reply</span>';if (reply.replies && reply.replies.length > 0) {repliesHtml += renderReplies(reply.replies); // 递归调用renderReplies}repliesHtml += '<div class="reply-form" id="reply-form-' + reply.id + '" style="display:none;">';repliesHtml += '<textarea id="reply-content-' + reply.id + '"></textarea><br>';repliesHtml += '<button onclick="addReply(\'' + reply.id + '\')">Submit</button>';repliesHtml += '</div>';repliesHtml += '</div>';});repliesHtml += '</div>';return repliesHtml;
}
后端
递归逻辑主要体现在 addReplyToNestedReplies 方法中:方法签名:private boolean addReplyToNestedReplies(List<Comment.Reply> replies, String parentId, Comment.Reply replyToAdd)
目标:向嵌套回复中添加一个新回复 replyToAdd,其父ID为 parentId。
遍历当前回复列表 replies。
如果找到回复的ID等于 parentId,则将新回复添加到这个回复的 replies 列表中并返回 true。
如果没有找到,递归调用 addReplyToNestedReplies 方法,检查当前回复的嵌套回复列表。
如果在任何嵌套层次找到匹配的父ID并成功添加回复,则返回 true。
如果遍历完所有回复及其嵌套回复后仍未找到匹配的父ID,则返回 false
addReplyToNestedReplies(replies, parentId, replyToAdd)||-- for (Comment.Reply reply : replies)||-- if (reply.getId().equals(parentId))|-- reply.getReplies().add(replyToAdd)|-- return true|-- else|-- if (addReplyToNestedReplies(reply.getReplies(), parentId, replyToAdd))|-- return true||-- return false
前后端
后端逻辑
后端代码的核心功能是管理评论和回复,包括添加、更新、删除评论,以及在评论或回复中添加嵌套回复。递归的主要目的是在嵌套层次中查找特定的评论或回复,并在其下添加新回复。后端递归逻辑详解
1.添加回复的方法 addReply:addReply 方法负责向特定评论或回复中添加新的回复。
该方法首先通过 findCommentById 查找目标评论。
如果找到了目标评论,调用 addReplyToCommentOrReplies 方法将回复添加到该评论或其嵌套回复中。
如果没有找到目标评论,则递归查找所有评论及其嵌套回复,通过 addReplyToNestedReplies 方法查找并添加回复。
2.递归查找并添加回复的方法 addReplyToNestedReplies:该方法递归遍历回复列表,查找目标回复。
如果找到目标回复,添加新回复并返回 true。
如果没有找到,递归调用自身查找嵌套回复,直到找到目标回复并添加新回复,或者遍历完所有回复返回 false。
3.向评论或其嵌套回复中添加回复的方法 addReplyToCommentOrReplies:该方法检查目标评论是否为回复的父级,如果是,则直接添加回复。
否则,调用 addReplyToNestedReplies 方法递归查找并添加回复。
前端逻辑
前端代码负责显示评论和回复,并提供用户交互界面以添加新回复。递归逻辑主要体现在显示嵌套回复时。前端递归逻辑详解
1.加载并显示评论的方法 loadComments:通过 AJAX 请求从后端获取指定水果 ID 的评论。
获取到评论后,遍历每个评论,调用 renderComment 方法生成 HTML。
2.渲染单个评论的方法 renderComment:生成单个评论的 HTML 结构,包括显示评论内容和回复按钮。
如果评论有嵌套回复,调用 renderReplies 方法递归生成嵌套回复的 HTML。
3.渲染嵌套回复的方法 renderReplies:递归遍历回复列表,生成每个回复的 HTML 结构。
每个回复也可能包含嵌套回复,因此再次调用 renderReplies 方法生成这些嵌套回复的 HTML。
4.显示和隐藏回复表单的事件处理:使用 jQuery 的 on('click', '.reply-button', function() { ... }) 处理回复按钮的点击事件,显示或隐藏对应的回复表单。
5.添加回复的方法 addReply:从表单获取回复内容,生成回复对象。
通过 AJAX 请求将回复发送到后端,并在成功后重新加载评论。
前后端结合的递归逻辑流程
1.用户交互:用户在前端页面点击“回复”按钮,显示回复表单。
用户在回复表单中输入内容并点击“提交”按钮。
2.前端处理:前端通过 addReply 方法将新回复发送到后端。
3.后端处理:后端接收到回复请求,调用 addReply 方法处理。
如果目标评论或回复存在,调用 addReplyToCommentOrReplies 方法。
addReplyToCommentOrReplies 方法通过递归查找嵌套回复,并添加新回复。
4.前端显示更新:后端返回成功响应,前端调用 loadComments 方法重新加载并显示最新的评论和回复。
loadComments 方法调用 renderComment 和 renderReplies 方法生成嵌套的评论和回复结构,通过递归实现嵌套显示。递归流程图
复制代码
前端:
- 用户点击“回复”按钮-> 显示回复表单
- 用户输入回复内容并点击“提交”按钮-> 调用 addReply 方法-> 发送 AJAX 请求到后端后端:
- 接收到回复请求-> 调用 addReply 方法-> 生成新回复 ID-> 查找目标评论或回复-> 找到目标评论-> 调用 addReplyToCommentOrReplies 方法-> 直接添加回复-> 或调用 addReplyToNestedReplies 方法-> 递归查找并添加回复-> 未找到目标评论-> 遍历所有评论-> 调用 addReplyToNestedReplies 方法-> 递归查找并添加回复前端:
- 后端返回成功响应-> 调用 loadComments 方法重新加载评论-> 调用 renderComment 方法生成评论 HTML-> 调用 renderReplies 方法生成嵌套回复 HTML-> 递归调用 renderReplies 方法生成所有嵌套回复的 HTML
通过这种方式,前后端结合实现了评论和回复的递归管理和显示,确保嵌套回复可以正确地被添加和显示。
相关文章:
002 递归评论 mongodb websocket消息推送
文章目录 商品评论CommentController.javaComment.javaCommentServiceImpl.javaCommentRepository.javaCommentService.javaWebSocketConfig.javaWebSocketProcess.javaapplication.yamlproductReview.htmlindex.htmlindex.jsindex.css 订单评论EvaluateMapper.xmlEvaluateMapp…...

高开高走的续作,可不止《庆余年2》
说起最近霸屏的影视剧,莫过于《庆余年2》。火爆全网的讨论度总归是没有辜负观众们五年的等待,在五月的影视市场独占鳌头已成定局。张若昀、陈道明、李沁等一众演员稳定发挥,剧情节奏随着故事发展渐入佳境,评分一路高涨。 对影视作…...

uniapp android使用uni.chooseLocation,app云打包后,定位地址列表一直在加载中
复现BUG 1、自己生成一个证书 参考生成证书流程 2、使用刚生成证书的SHA1 ,重新创建一个高德key 高德开放平台地址 3、打包(打包的包名要与高德申请key所填的包名一致)...

详解http协议
什么是HTTP协议 定义 Http协议即超文本传送协议 (HTTP-Hypertext transfer protocol) 。 它定义了浏览器(即万维网客户进程)怎样向万维网服务器请求万维网文档,以及服务器怎样把文档传送给浏览器。从层次的角度看,HTTP是面向&am…...

台湾省军事演习路径规划:A*算法在复杂地形中的应用
❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容,和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣! 推荐:数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航: LeetCode解锁100…...
OpenHarmony鸿蒙软总线使用mbedtls数据加密详解
OpenHarmony鸿蒙软总线子系统中使用了多种的加密技术,本篇介绍调用mbedtls的数据加密。 调用mbedtls加密的源码位于: foundation/communication/dsoftbus/adapter/common/mbedtls/softbus_adapter_crypto.c 这个源码单元,调用mbedTLS库实现了各种加密功能,包括AES-GCM加密…...

【JavaEE】Servlet
文章目录 一、Servlet 是什么二、如何创建Servlet程序1、创建项目2、引入依赖3、创建目录4、编写代码5、打包程序6、部署程序7、验证程序 一、Servlet 是什么 二、如何创建Servlet程序 1、创建项目 2、引入依赖 Maven 项目创建完后,会自动生成一个 pom.xml 的文…...

SpringBoot——整合Redis
目录 Redis 创建Commodity表 启动MySQL和Redis 新建一个SpringBoot项目 pom.xml application.properties Commodity实体类 ComMapper接口 ComService业务层接口 ComServiceImpl业务接口的实现类 ComController控制器 RedisConfig配置类 SpringbootRdisApplication启…...

2024全新Langchain大模型AI应用与多智能体实战开发
2024全新Langchain大模型AI应用与多智能体实战开发 LangChain 就是一个 LLM 编程框架,你想开发一个基于 LLM 应用,需要什么组件它都有,直接使用就行;甚至针对常规的应用流程,它利用链(LangChain中Chain的由来)这个概念…...

【JavaEE 初阶(十)】JVM
❣博主主页: 33的博客❣ ▶️文章专栏分类:JavaEE◀️ 🚚我的代码仓库: 33的代码仓库🚚 🫵🫵🫵关注我带你了解更多进阶知识 目录 1.前言2.JVM内存区域划分3.类加载3.1双亲委派模型 4.垃圾回收(GC࿰…...

【Flutter】AspectRatio组件Card组件按钮组件Wrap组件
🔥 本文由 程序喵正在路上 原创,CSDN首发! 💖 系列专栏:Flutter学习 🌠 首发时间:2024年5月25日 🦋 欢迎关注🖱点赞👍收藏🌟留言🐾 目…...

【IDEA软件应用篇】IDEA基础开发设置和开发快捷键
IDEA是一种集成开发环境,可以运行java代码。 本篇文章你将收获到下面的知识: (1)IDEA如何设置字体大小快捷键 (2)如何解决每次进IDEA时,进去的页面都是上次使用完时的那个页面 (3&am…...

机器学习--数学部分笔记
前言 因为周三要考试,所以数学部分写一下笔记 正文 随机事件和随机实验 条件概率 • 在已知事件 𝐵 发生的条件下,事件𝐴发生的概率称为事件 𝐴 的条件概率,记为𝑃(𝐴|𝐵) 全概率…...
基于springboot的在线宠物用品交易网站源码数据库
基于springboot的在线宠物用品交易网站源码数据库 随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了在线宠物用品交易网站的开发全过程。通过分析在线宠物用品交易网站管理的不足,创建了一个计算机管理在…...

【Pytorch】13.搭建完整的CIFAR10模型
项目源码 已上传至githubCIFAR10Model,如果有帮助可以点个star 简介 在前文【Pytorch】10.CIFAR10模型搭建我们学习了用Module来模拟搭建CIFAR10的训练流程 本节将会加入损失函数,梯度下降,TensorBoard来完整搭建一个训练的模型 基本步骤 搭建…...

护目镜佩戴自动识别预警摄像机
护目镜佩戴自动识别预警摄像机是一种智能监测设备,专门用于佩戴护目镜的工人进行作业时,能够自动识别有潜在风险的场景,并及时发出预警信号。该摄像机配备人脸识别和智能预警系统,可以检测危险情况并为工人提供实时安全保护&#…...
keep-alive的使用
Vue中的<keep-alive>组件是前端开发中的一个宝藏功能,它如同时光胶囊般保留组件的状态,让组件在切换时仿佛按下暂停键,再次回来时还能继续播放,极大地优化了用户体验和性能。🚀✨ 作用 状态保留:当包…...

【Linux】中的常见的重要指令(中)
目录 一、man指令 二、cp指令 三、cat指令 四、mv指令 五、more指令 六、less指令 七、head指令 八、tail指令 一、man指令 Linux的命令有很多参数,我们不可能全记住,我们可以通过查看联机手册获取帮助。访问Linux手册页的命令是 man 语法: m…...

营收净利双降、股东减持,大降价也救不了良品铺子
号称“高端零食第一股”的良品铺子(603719.SH),正遭遇部分股东的“用脚投票”。 5月17日晚间,良品铺子连发两份减持公告,其控股股东宁波汉意创业投资合伙企业、持股5%以上股东达永有限公司,两者均计划减持。 其中,宁…...
【设计模式】设计模式的分类
通常设计模式的分类有创建型、行为型和结构型。 创建型 常用的有:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式。 不常用的有:原型模式。 创建型模式涉及到将对象实例化,这类模式都提供一个方法,将…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...

【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...

大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...

【2025年】解决Burpsuite抓不到https包的问题
环境:windows11 burpsuite:2025.5 在抓取https网站时,burpsuite抓取不到https数据包,只显示: 解决该问题只需如下三个步骤: 1、浏览器中访问 http://burp 2、下载 CA certificate 证书 3、在设置--隐私与安全--…...

[10-3]软件I2C读写MPU6050 江协科技学习笔记(16个知识点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16...

Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...