002 递归评论 mongodb websocket消息推送

商品评论

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 {

    @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


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


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


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 {
    @Bean
    public 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;

    @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

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



<!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


<!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


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_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


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 {
    @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


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 {

    @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


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;

    // 成功值,默认为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



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


  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

简 洁 冬冬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值