002 递归评论 mongodb websocket消息推送

文章目录

商品评论

CommentController.java

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 {

    @Autowired
    private CommentService commentService;


    // 添加根据fruitId获取评论的映射方法
    @GetMapping("/byFruitId/{fruitId}")
    public List<Comment> getCommentsByFruitId(@PathVariable Integer fruitId) {
        return commentService.getCommentsByFruitId(fruitId);
    }

    @GetMapping
    public List<Comment> getAllComments() {
        return commentService.getAllComments();
    }

    @PostMapping
    public 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

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 {

    @Id
    private String id;
    private int evaluateId;
    private int fruitId;
    private int score;
    private int status;
    private OriginalPoster originalPoster;
    private List<Reply> replies = new ArrayList<>();

    @Data
    public static class OriginalPoster {
        private int userId;
        private String content;
        private String postedAt;
    }

    @Data
    public static class Reply {
        @Id
        private String id;
        private int userId;
        private String content;
        private String postedAt;
        private String parentId;
        private List<Reply> replies = new ArrayList<>();
    }
}

CommentServiceImpl.java

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 {

    @Autowired
    private CommentRepository commentRepository;

    @Autowired
    private WebSocketProcess websocketProcess;

    @Override
    public List<Comment> getAllComments() {
        return commentRepository.findAll();
    }


    @Override
    public List<Comment> getCommentsByFruitId(Integer fruitId) {
        return commentRepository.findByFruitId(fruitId);
    }

    @Override
    public Comment addComment(Comment comment) {
        comment.setId(UUID.randomUUID().toString()); // 生成随机的 `_id`
        comment.setReplies(new ArrayList<>()); // 确保 `replies` 是一个空列表
        return commentRepository.save(comment);
    }

    @Override
    public Comment updateComment(String id, Comment comment) {
        comment.setId(id);
        return commentRepository.save(comment);
    }

    @Override
    public void deleteComment(String id) {
        commentRepository.deleteById(id);
    }

    @Override
    public 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

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

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

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 {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

WebSocketProcess.java

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;

    @OnOpen
    public 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
    @OnMessage
    public void onMessage(String message, @PathParam("id") Integer clientId){
        System.out.println("server get message from client id:" + clientId+", and message is :" + message);
    }

    @OnClose
    public void onClose(Session session, @PathParam("id") Integer clientId){
        map.remove(clientId);
    }


    //    server  send message to client
    public  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

yaml 复制代码
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/fshop_app?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
      username: dev
      password: 123456
      initial-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

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参数中获取fruitId
  function 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;
    // }




    //解析userId
    function 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); // 输出userId





    loadComments();

    function loadComments() {
      var fruitId = 43;
      $.ajax({
        url: 'comments/byFruitId/'+ fruitId, // 在这里使用fruitId
        method: '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

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 server
                    ws.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

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

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 复制代码
<?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_info
        from 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) evaluate
        where myorder.myorder_id = evaluate.myorder_id
          and myorder.fruit_id = #{fruitId}
        order by evaluate.evaluate_create_time desc limit #{currentPage},#{queryCount}
    </select>
</mapper>

EvaluateMapper.java

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

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 {
    @Autowired
    private IEvaluateService evaluateService;



    // 分页查询
    @GetMapping
    public R<Page<Evaluate>> getAll(HttpServletRequest request, Integer pageNum) {
        // 获取token
        String 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) {
        // 获取token
        String 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

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

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 {

    @Autowired
    private EvaluateMapper evaluateMapper;

    @Autowired
    private CommentRepository commentRepository;

    @Override
    public R<Page<Evaluate>> getAll(String token, Integer pageNum) {
        // 解析token
        LoginUserDto 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("查询失败");
    }

    @Override
    public 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("查询失败");
    }

    @Override
    public R<Evaluate> getById(String token, String evaluateId) {
        // 解析token
        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);

        if (evaluate != null) {
            return R.ok("查询成功", evaluate);
        }

        return R.error("查询失败");
    }

    //查询所有评论并返回用户ID、用户名称、用户头像以及用户评论
    @Override
    public R getEvaluateInfo(Integer fruitId,Integer currentPage, Integer queryCount) {
        Integer preCurrentPage = (currentPage - 1) * queryCount;
        return R.ok(evaluateMapper.getEvaluateInfo(fruitId,preCurrentPage , queryCount));
    }




    @Override
    public 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是123
        originalPoster.setContent( new String(evaluate.getEvaluateInfo()));
        LocalDateTime now = LocalDateTime.now();
        // 对于LocalDateTime,应该直接使用ISO_LOCAL_DATE_TIME
        DateTimeFormatter 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

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

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;

    // 成功值,默认为1
    private static final int SUCCESS_CODE = 1;
    // 失败值,默认为0
    private 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());
    }

    @Override
    public String toString() {
        return "R{" +
                "code=" + code +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }
}

orderReview.css

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

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

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

shell 复制代码
db.createUser({ user: "abc", pwd: "123456", roles: [{ role: "dbOwner", db: "commentDB" }] });
shell 复制代码
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 复制代码
<?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
通过这种方式,前后端结合实现了评论和回复的递归管理和显示,确保嵌套回复可以正确地被添加和显示。
相关推荐
看山还是山,看水还是。9 分钟前
MySQL 管理
数据库·笔记·mysql·adb
fishmemory7sec15 分钟前
Koa2项目实战2(路由管理、项目结构优化)
数据库·mongodb·koa
momo小菜pa25 分钟前
【MySQL 09】表的内外连接
数据库·mysql
Jasonakeke34 分钟前
【重学 MySQL】四十九、阿里 MySQL 命名规范及 MySQL8 DDL 的原子化
数据库·mysql
小宇成长录1 小时前
Mysql:数据库和表增删查改基本语句
数据库·mysql·数据库备份
团儿.2 小时前
解锁MySQL高可用新境界:深入探索MHA架构的无限魅力与实战部署
数据库·mysql·架构·mysql之mha架构
权^3 小时前
MySQL--聚合查询、联合查询、子查询、合并查询(上万字超详解!!!)
大数据·数据库·学习·mysql
缘友一世5 小时前
macos安装mongodb
数据库·mongodb·macos
万事大吉CC6 小时前
mysql单表查询·3
数据库·mysql
苹果醋312 小时前
大模型实战--FastChat一行代码实现部署和各个组件详解
java·运维·spring boot·mysql·nginx