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
通过这种方式,前后端结合实现了评论和回复的递归管理和显示,确保嵌套回复可以正确地被添加和显示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/15032.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?

前言 大家好&#xff0c;我是老马。很高兴遇到你。 作为一个 java 开发者&#xff0c;工作中一直在使用 nginx。却发现一直停留在使用层面&#xff0c;无法深入理解。 有一天我在想&#xff0c;为什么不能有一个 java 版本的 nginx 呢&#xff1f; 一者是理解 nginx 的设计…

HTTP 协议中 GET 和 POST 有什么区别?分别适用于什么场景?

HTTP 协议中 GET 和 POST 是两种常用的请求方法&#xff0c;它们的区别如下: 1. 参数传递方式不同 GET 请求参数是在 URL 中以键值对的形式传递的&#xff0c;例如:http://www.example.com/&#xff1f;key1value1&k ey2value2。 而 POST 请求参数是在请求体中以键值对的…

SQOOP详细讲解

SQOOP安装及使用 SQOOP安装及使用SQOOP安装1、上传并解压2、修改文件夹名字3、修改配置文件4、修改环境变量5、添加MySQL连接驱动6、测试准备MySQL数据登录MySQL数据库创建student数据库切换数据库并导入数据另外一种导入数据的方式使用Navicat运行SQL文件导出MySQL数据库impo…

数据访问与Spring Data JPA

数据访问与Spring Data JPA 在现代Java应用程序中&#xff0c;持久化数据是核心功能之一。Spring Data JPA&#xff08;Java Persistence API&#xff09;为开发者提供了一种简单且高效的方式来访问和操作数据库。在本博文中&#xff0c;我将向您展示如何使用Spring Data JPA来…

数据结构------二叉树经典习题2

博主主页: 码农派大星. 关注博主带你了解更多数据结构知识 1.非递归的前序遍历 1.用栈来实现 2,前序遍历是根左右, 先是根节点入栈,,然后不为空时向左遍历,当为空时就返回向右遍历,右为空时直接出栈,依次循环即可. public void preOrderNot(TreeNode root){Stack<TreeNo…

科技赋能,打破视障人士的沟通壁垒

在探索如何增强盲人群体的社会参与度与幸福感的旅程中&#xff0c;盲人社交能力提升策略成为了不容忽视的一环。随着科技的不断进步&#xff0c;像“蝙蝠避障”这样的辅助软件&#xff0c;不仅在日常出行中为盲人提供了实时避障和拍照识别的便利&#xff0c;也在无形中为他们拓…

华为数通 HCIP-Datacom(H12-821)题库

最新 HCIP-Datacom&#xff08;H12-821&#xff09;完整题库请扫描上方二维码访问&#xff0c;持续更新中。 BGP路由的Update消息中可不包含以下哪些属性&#xff1f; A、Local Preference B、AS Path C、MED D、Origin 答案&#xff1a;AC 解析&#xff1a;as-path和ori…

深度学习训练框架——监督学习为例

训练框架 文章目录 训练框架1. 模型网络结构2. 数据读取与数据加载2.1Dataloater参数2.2 collate_fn 3. 优化器与学习率调整3.1 优化器3.2 学习率调度 4迭代训练4.1 train_epoch4.2 train iteration 5.1 保存模型权重 本文内容以pytorch为例 1. 模型网络结构 自定义网络模型继…

测试开发面试题

简述自动化测试的三大等待 强制等待。直接使用time.sleep()方法让程序暂停指定的时间。优点是实现简单&#xff0c;缺点是不够灵活&#xff0c;可能会导致不必要的等待时间浪费。隐式等待。设置一个固定的等待时间&#xff0c;在这个时间内不断尝试去查找元素&#xff0c;如果…

Java17 --- SpringCloud之Sentinel

目录 一、Sentinel下载并运行 二、创建8401微服务整合Sentinel 三、流控规则 3.1、直接模式 3.2、关联模式 3.3、链路模式 3.3.1、修改8401代码 3.3.2、创建流控模式 3.4、Warm UP&#xff08;预热&#xff09; ​编辑 3.5、排队等待 四、熔断规则 4.1、慢调用比…

【C++】09.vector

一、vector介绍和使用 1.1 vector的介绍 vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是可以动态改…

操作系统实验四 (综合实验)设计简单的Shell程序

前言 因为是一年前的实验&#xff0c;很多细节还有知识点我都已经遗忘了&#xff0c;但我还是尽可能地把各个细节讲清楚&#xff0c;请见谅。 1.实验目的 综合利用进程控制的相关知识&#xff0c;结合对shell功能的和进程间通信手段的认知&#xff0c;编写简易shell程序&…

Excel透视表:快速计算数据分析指标的利器

文章目录 概述1.数据透视表基本操作1.1准备数据&#xff1a;1.2创建透视表&#xff1a;1.3设置透视表字段&#xff1a;1.4多级分类汇总和交叉汇总的差别1.5计算汇总数据&#xff1a;1.6透视表美化&#xff1a;1.7筛选和排序&#xff1a;1.8更新透视表&#xff1a; 2.数据透视-数…

【B站 heima】小兔鲜Vue3 项目学习笔记Day02

文章目录 Pinia1.使用2. pinia-计数器案例3. getters实现4. 异步action5. storeToRefsx 数据解构保持响应式6. pinia 调试 项目起步1.项目初始化和git管理2. 使用ElementPlus3. ElementPlus 主题色定制4. axios 基础配置5. 路由设计6. 静态资源初始化和 Error lens安装7.scss自…

Github 2024-05-24 开源项目日报 Top10

根据Github Trendings的统计,今日(2024-05-24统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目3非开发语言项目2TypeScript项目2JavaScript项目1Kotlin项目1C#项目1C++项目1Shell项目1Microsoft PowerToys: 最大化Windows系统生产…

软件设计师备考笔记(十):网络与信息安全基础知识

文章目录 一、网络概述二、网络互连硬件&#xff08;一&#xff09;网络的设备&#xff08;二&#xff09;网络的传输介质&#xff08;三&#xff09;组建网络 三、网络协议与标准&#xff08;一&#xff09;网络的标准与协议&#xff08;二&#xff09;TCP/IP协议簇 四、Inter…

某神,云手机启动?

某神自从上线之后&#xff0c;热度不减&#xff0c;以其丰富的内容和独特的魅力吸引着众多玩家&#xff1b; 但是随着剧情无法跳过&#xff0c;长草期过长等原因&#xff0c;近年脱坑的玩家多之又多&#xff0c;之前米家推出了一款云某神的app&#xff0c;目标是为了减少用户手…

RedisTemplateAPI:String

文章目录 ⛄1 String 介绍⛄2 命令⛄3 对应 RedisTemplate API❄️❄️ 3.1 添加缓存❄️❄️ 3.2 设置过期时间(单独设置)❄️❄️ 3.3 获取缓存值❄️❄️ 3.4 删除key❄️❄️ 3.5 顺序递增❄️❄️ 3.6 顺序递减 ⛄4 以下是一些常用的API⛄5 应用场景 ⛄1 String 介绍 Str…

ue引擎游戏开发笔记(47)——设置状态机解决跳跃问题

1.问题分析&#xff1a; 目前当角色起跳时&#xff0c;只是简单的上下移动&#xff0c;空中仍然保持行走动作&#xff0c;并没有设置跳跃动作&#xff0c;因此&#xff0c;给角色设置新的跳跃动作&#xff0c;并优化新的动作动画。 2.操作实现&#xff1a; 1.实现跳跃不复杂&…

LabVIEW常用的电机控制算法有哪些?

LabVIEW常用的电机控制算法主要包括以下几种&#xff1a; 1. PID控制&#xff08;比例-积分-微分控制&#xff09; 描述&#xff1a;PID控制是一种经典的控制算法&#xff0c;通过调节比例、积分和微分三个参数来控制电机速度和位置。应用&#xff1a;广泛应用于直流电机、步…