文章目录
- 商品评论
- CommentController.java
- Comment.java
- CommentServiceImpl.java
- CommentRepository.java
- CommentService.java
- WebSocketConfig.java
- WebSocketProcess.java
- application.yaml
- productReview.html
- index.html
- index.js
- index.css
- 订单评论
- EvaluateMapper.xml
- EvaluateMapper.java
- EvaluateController.java
- Evaluate.java
- EvaluateServiceImpl.java
- IEvaluateService.java
- R.java
- orderReview.css
- orderReview.html
- orderReview.js
- mongodb
- pom.xml
- 递归评论
- 前端
- 后端
- 前后端
商品评论
CommentController.java
package com.fshop.controller; import com.fshop.entity.Comment; import com.fshop.service.CommentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/productReview/comments") public class CommentController { @Autowired private CommentService commentService; // 添加根据fruitId获取评论的映射方法 @GetMapping("/byFruitId/{fruitId}") public List
getCommentsByFruitId(@PathVariable Integer fruitId) { return commentService.getCommentsByFruitId(fruitId); } @GetMapping public List 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
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 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
getAllComments() { return commentRepository.findAll(); } @Override public List 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 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 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
{ // 添加根据fruitId查找评论的方法 List findByFruitId(Integer fruitId); } CommentService.java
package com.fshop.service; import com.fshop.entity.Comment; import java.util.List; public interface CommentService { List
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 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
* (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 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 > entrySet = map.entrySet(); for(Map.Entry 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
Fruit Comments Comments
index.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', 'https://blog.csdn.net/m0_46695127/article/details/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
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; /** *
* 订单评价表 Mapper 接口 *
* * @author dev * @since 2024-04-23 */ public interface EvaluateMapper extends BaseMapper{ List 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; /** *
* 订单评价表 前端控制器 *
* * @author dev * @since 2024-04-23 */ @RestController @RequestMapping("/evaluate") public class EvaluateController { @Autowired private IEvaluateService evaluateService; // 分页查询 @GetMapping public R> 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 getEvaluateById(@PathVariable String evaluateId) { return null; } // 删除评论 @PostMapping("remove") public R 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 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; /** *
* 订单评价表 *
* * @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; /** *
* 订单评价表 服务实现类 *
* * @author dev * @since 2024-04-23 */ @Service public class EvaluateServiceImpl extends ServiceImplimplements IEvaluateService { @Autowired private EvaluateMapper evaluateMapper; @Autowired private CommentRepository commentRepository; @Override public R > getAll(String token, Integer pageNum) { // 解析token LoginUserDto loginUser = JwtUtil.parseToken(token); // 查询所有,user_id等于loginUser.getUserId,且status等于1的评论 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("user_id", loginUser.getUserId()).eq("status", 1); // 分页,每页显示10条评论 Page 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 removeEvaluate(String token, String evaluateId) { // 先查询 LoginUserDto loginUser = JwtUtil.parseToken(token); QueryWrapper 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 getById(String token, String evaluateId) { // 解析token LoginUserDto loginUser = JwtUtil.parseToken(token); QueryWrapper 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 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; /** *
* 订单评价表 服务类 *
* * @author dev * @since 2024-04-23 * */ public interface IEvaluateService extends IService{ R > getAll(String token, Integer pageNum); R removeEvaluate(String token, String evaluateId); R getById(String token, String evaluateId); //商品详情页获取所有评论 R getEvaluateInfo(Integer fruitId, Integer currentPage, Integer queryCount); //增加评论 R 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
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 R ok() { return new R (SUCCESS_CODE, "success"); } public static R ok(String msg) { return new R (SUCCESS_CODE, msg); } public static R ok(T data) { return new R (SUCCESS_CODE, data); } public static R ok(String msg, T data) { return new R (SUCCESS_CODE, msg, data); } public static R error() { return new R (ERROR_CODE, "error"); } public static R error(String msg) { return new R (ERROR_CODE, msg); } public static R error(int code, String msg) { return new R (code, msg); } public static R error(ResponseCode res) { return new R (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
评价页面 评价页面
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
4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.6 com.fshop fshop-app 0.0.1-SNAPSHOT fshop-app fshop-app war 1.8 org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-starter-data-mongodb org.apache.httpcomponents httpclient 4.5.13 org.apache.httpcomponents httpcore 4.4.15 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat provided org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java 8.0.33 com.baomidou mybatis-plus-boot-starter 3.5.1 com.alibaba druid-spring-boot-starter 1.2.9 org.springframework.boot spring-boot-starter-data-redis io.jsonwebtoken jjwt 0.9.1 com.alipay.sdk alipay-sdk-java 3.1.0 org.projectlombok lombok org.springframework.boot spring-boot-starter-amqp com.fasterxml.jackson.dataformat jackson-dataformat-xml 2.9.10 com.qiniu qiniu-java-sdk 7.13.0 com.squareup.okhttp3 okhttp 3.14.2 compile com.google.code.gson gson 2.8.5 compile com.qiniu happy-dns-java 0.1.6 test junit junit 4.12 test org.springframework.boot spring-boot-starter-websocket org.springframework.boot spring-boot-maven-plugin 2.7.6 递归评论
前端
递归渲染逻辑图
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 = ''; commentHtml += '
' + comment.originalPoster.userId + ': ' + comment.originalPoster.content + ' (' + new Date(comment.originalPoster.postedAt).toLocaleString() + ')
'; commentHtml += 'Reply'; if (comment.replies && comment.replies.length > 0) { commentHtml += renderReplies(comment.replies); } commentHtml += ''; commentHtml += '
'; commentHtml += ''; commentHtml += ''; commentHtml += ''; return commentHtml; } function renderReplies(replies) { let repliesHtml = ''; replies.forEach(reply => { repliesHtml += ''; repliesHtml += '' + reply.userId + ': ' + reply.content + ' (' + new Date(reply.postedAt).toLocaleString() + ')
'; repliesHtml += 'Reply'; if (reply.replies && reply.replies.length > 0) { repliesHtml += renderReplies(reply.replies); // 递归调用renderReplies } repliesHtml += ''; repliesHtml += '
'; repliesHtml += ''; repliesHtml += ''; repliesHtml += ''; }); repliesHtml += ''; return repliesHtml; }后端
递归逻辑主要体现在 addReplyToNestedReplies 方法中: 方法签名:private boolean addReplyToNestedReplies(List
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 通过这种方式,前后端结合实现了评论和回复的递归管理和显示,确保嵌套回复可以正确地被添加和显示。
还没有评论,来说两句吧...