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