评论模块 - 后端数据库设计及功能实现

评论模块在很多系统中都有,CodeRiver河码 作为类似程序员客栈的沟通协作平台自然也不会少。

前端界面是参考了简书的评论模块,专门写了一篇文章介绍实现步骤:
vue + element-ui + scss 仿简书评论模块
感兴趣的可以看看。

项目地址:https://github.com/cachecats/coderiver

代码在 根目录/java/comments-service

文章将分三部分介绍:

  1. 前端界面分析
  2. 数据库设计
  3. 功能实现

一、前端界面分析

先看看前端界面长什么样,知道了前端需要什么数据,就知道数据库该怎么设计了。

image

首先评论的主体可以是人、项目、资源,所以要有一个 type 字段标明这条评论的类型。

以项目为例,一个项目下面可能会有多条评论。每条评论其实分为两种,一种是直接对项目的评论,称之为父评论吧;另一种是对已有评论的评论,称为子评论。

梳理一下关系,每个项目可能有多个父评论,每个父评论可能有多个子评论。项目与父评论,父评论与子评论,都是一对多的关系。

由此可知数据库应该分为两个表,一个存储父评论,一个存储子评论。

再看都需要什么字段,先分析主评论。必须要有的是项目id,得知道是对谁评论的,叫 ownerId 吧。还有评论者的头像、昵称、id,还有评论时间、内容、点赞个数等。

子评论跟父评论的字段差不多,只是不要点赞数量。

二、数据库设计

分析了界面,知道需要什么字段,就开始设计数据库吧。

评论主表(父评论表)

CREATE TABLE `comments_info` (
  `id` varchar(32) NOT NULL COMMENT '评论主键id',
  `type` tinyint(1) NOT NULL COMMENT '评论类型:对人评论,对项目评论,对资源评论',
  `owner_id` varchar(32) NOT NULL COMMENT '被评论者id,可以是人、项目、资源',
  `from_id` varchar(32) NOT NULL COMMENT '评论者id',
  `from_name` varchar(32) NOT NULL COMMENT '评论者名字',
  `from_avatar` varchar(512) DEFAULT '' COMMENT '评论者头像',
  `like_num` int(11) DEFAULT '0' COMMENT '点赞的数量',
  `content` varchar(512) DEFAULT NULL COMMENT '评论内容',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  KEY `owner_id` (`owner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论主表';

评论回复表(子评论表)

CREATE TABLE `comments_reply` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `comment_id` varchar(32) NOT NULL COMMENT '评论主表id',
  `from_id` varchar(32) NOT NULL COMMENT '评论者id',
  `from_name` varchar(32) NOT NULL COMMENT '评论者名字',
  `from_avatar` varchar(512) DEFAULT '' COMMENT '评论者头像',
  `to_id` varchar(32) NOT NULL COMMENT '被评论者id',
  `to_name` varchar(32) NOT NULL COMMENT '被评论者名字',
  `to_avatar` varchar(512) DEFAULT '' COMMENT '被评论者头像',
  `content` varchar(512) DEFAULT NULL COMMENT '评论内容',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`),
  KEY `comment_id` (`comment_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='评论回复表';

三、功能实现

项目采用 SpringCloud 微服务架构,评论模块跟其他模块的关联性不强,可以抽出为一个单独的服务 comments-service

数据实体对象

数据实体对象 CommentsInfo

package com.solo.coderiver.comments.dataobject;

import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.Date;

/**
 * 评论表主表
 */
@Entity
@Data
@DynamicUpdate
public class CommentsInfo {

    //评论主键id
    @Id
    private String id;

    //评论类型。1用户评论,2项目评论,3资源评论
    private Integer type;

    //被评论者的id
    private String ownerId;

    //评论者id
    private String fromId;

    //评论者名字
    private String fromName;

    //评论者头像
    private String fromAvatar;

    //获得点赞的数量
    private Integer likeNum;

    //评论内容
    private String content;

    //创建时间
    private Date createTime;

    //更新时间
    private Date updateTime;

}

数据实体对象 CommentsReply

package com.solo.coderiver.comments.dataobject;

import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;

/**
 * 评论回复表
 */
@Entity
@Data
@DynamicUpdate
public class CommentsReply {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    //评论主表id
    private String commentId;

    //评论者id
    private String fromId;

    //评论者名字
    private String fromName;

    //评论者头像
    private String fromAvatar;

    //被评论者id
    private String toId;

    //被评论者名字
    private String toName;

    //被评论者头像
    private String toAvatar;

    //评论内容
    private String content;

    //创建时间
    private Date createTime;

    //更新时间
    private Date updateTime;

}

数据库操作仓库 repository

操作数据库暂时用的是 Jpa ,后期可能会增加一份 mybatis 的实现。

CommentsInfoRepository

package com.solo.coderiver.comments.repository;

import com.solo.coderiver.comments.dataobject.CommentsInfo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CommentsInfoRepository extends JpaRepository<CommentsInfo, String> {

    List<CommentsInfo> findByOwnerId(String ownerId);
}

CommentsReplyRepository

package com.solo.coderiver.comments.repository;

import com.solo.coderiver.comments.dataobject.CommentsReply;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CommentsReplyRepository extends JpaRepository<CommentsReply, Integer> {

    List<CommentsReply> findByCommentId(String commentId);
}

Service 接口封装

为了代码更健壮,要把数据库的操作封装一下

CommentsInfoService

package com.solo.coderiver.comments.service;

import com.solo.coderiver.comments.dataobject.CommentsInfo;
import java.util.List;

public interface CommentsInfoService {

    /**
     * 保存评论
     * @param info
     * @return
     */
    CommentsInfo save(CommentsInfo info);

    /**
     * 根据被评论者的id查询评论列表
     * @param ownerId
     * @return
     */
    List<CommentsInfo> findByOwnerId(String ownerId);
}

CommentsReplyService

package com.solo.coderiver.comments.service;

import com.solo.coderiver.comments.dataobject.CommentsReply;
import java.util.List;

public interface CommentsReplyService {

    /**
     * 保存评论回复
     * @param reply
     * @return
     */
    CommentsReply save(CommentsReply reply);

    /**
     * 根据评论id查询回复
     * @param commentId
     * @return
     */
    List<CommentsReply> findByCommentId(String commentId);
}

接口的实现类

CommentsInfoServiceImpl

package com.solo.coderiver.comments.service.impl;

import com.solo.coderiver.comments.dataobject.CommentsInfo;
import com.solo.coderiver.comments.repository.CommentsInfoRepository;
import com.solo.coderiver.comments.service.CommentsInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CommentsInfoServiceImpl implements CommentsInfoService {

    @Autowired
    CommentsInfoRepository repository;

    @Override
    public CommentsInfo save(CommentsInfo info) {
        return repository.save(info);
    }

    @Override
    public List<CommentsInfo> findByOwnerId(String ownerId) {
        return repository.findByOwnerId(ownerId);
    }
}

CommentsReplyServiceImpl

package com.solo.coderiver.comments.service.impl;

import com.solo.coderiver.comments.dataobject.CommentsReply;
import com.solo.coderiver.comments.repository.CommentsReplyRepository;
import com.solo.coderiver.comments.service.CommentsReplyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CommentsReplyServiceImpl implements CommentsReplyService {

    @Autowired
    CommentsReplyRepository repository;

    @Override
    public CommentsReply save(CommentsReply reply) {
        return repository.save(reply);
    }

    @Override
    public List<CommentsReply> findByCommentId(String commentId) {
        return repository.findByCommentId(commentId);
    }
}

控制层 Controller

Controller 层分发请求,并返回前端需要的数据

package com.solo.coderiver.comments.controller;

@RestController
@Api(description = "评论相关接口")
public class CommentsController {

    @Autowired
    CommentsInfoService infoService;

    @Autowired
    CommentsReplyService replyService;

    @PostMapping("/save")
    @ApiOperation("保存评论")
    @Transactional
    public ResultVO saveComments(@Valid CommentsInfoForm form, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            throw new CommentsException(ResultEnums.PARAMS_ERROR.getCode(), bindingResult.getFieldError().getDefaultMessage());
        }
        //将 CommentsInfoForm 里的数据拷贝到 CommentsInfo
        CommentsInfo info = new CommentsInfo();
        BeanUtils.copyProperties(form, info);
        // 生成并设置评论的主键id
        info.setId(KeyUtils.genUniqueKey());
        CommentsInfo result = infoService.save(info);
        if (result == null) {
            throw new CommentsException(ResultEnums.SAVE_COMMENTS_FAIL);
        }
        return ResultVOUtils.success();
    }

    @GetMapping("/get/{ownerId}")
    @ApiOperation("根据 ownerId 查询评论")
    @ApiImplicitParam(name = "ownerId", value = "被评论者id")
    public ResultVO getCommentsByOwnerId(@PathVariable("ownerId") String ownerId) {
        List<CommentsInfo> infoList = infoService.findByOwnerId(ownerId);
        //将 CommentsInfo 转换为 CommentsInfoDTO
        List<CommentsInfoDTO> infoDTOS = infoList.stream().map(info -> {
            CommentsInfoDTO dto = new CommentsInfoDTO();
            BeanUtils.copyProperties(info, dto);
            return dto;
        }).collect(Collectors.toList());
        return ResultVOUtils.success(infoDTOS);
    }

    @PostMapping("/save-reply")
    @ApiOperation("保存评论回复")
    @Transactional
    public ResultVO saveReply(@Valid CommentsReplyForm form, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            throw new CommentsException(ResultEnums.PARAMS_ERROR.getCode(), bindingResult.getFieldError().getDefaultMessage());
        }
        CommentsReply reply = new CommentsReply();
        BeanUtils.copyProperties(form, reply);
        CommentsReply result = replyService.save(reply);
        if (result == null) {
            throw new CommentsException(ResultEnums.SAVE_COMMENTS_FAIL);
        }
        return ResultVOUtils.success();
    }

    @GetMapping("/get-reply/{commentId}")
    @ApiOperation("通过commentId获取评论回复")
    public ResultVO getReplyByCommentId(@PathVariable("commentId") String commentId) {
        List<CommentsReply> replyList = replyService.findByCommentId(commentId);
        //将 CommentsReply 转换为 CommentsReplyDTO
        List<CommentsReplyDTO> replyDTOS = replyList.stream().map(reply -> {
            CommentsReplyDTO dto = new CommentsReplyDTO();
            BeanUtils.copyProperties(reply, dto);
            return dto;
        }).collect(Collectors.toList());

        return ResultVOUtils.success(replyDTOS);
    }
}

代码中工具类和枚举类请到 github 上查看源码。

以上就是对评论模块的设计与功能实现,欢迎各位大佬提出代码优化建议,共同成长~


代码出自开源项目 CodeRiver,致力于打造全平台型全栈精品开源项目。

coderiver 中文名 河码,是一个为程序员和设计师提供项目协作的平台。无论你是前端、后端、移动端开发人员,或是设计师、产品经理,都可以在平台上发布项目,与志同道合的小伙伴一起协作完成项目。

coderiver河码 类似程序员客栈,但主要目的是方便各细分领域人才之间技术交流,共同成长,多人协作完成项目。暂不涉及金钱交易。

计划做成包含 pc端(Vue、React)、移动H5(Vue、React)、ReactNative混合开发、Android原生、微信小程序、java后端的全平台型全栈项目,欢迎关注。

项目地址:https://github.com/cachecats/coderiver


您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星✨ ~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,607评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,239评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,960评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,750评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,764评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,604评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,347评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,253评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,702评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,893评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,015评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,734评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,352评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,934评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,052评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,216评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,969评论 2 355

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,152评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,728评论 2 59
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,938评论 2 89
  • 今天刚注册【简书】,天气晴朗,心情有点好!
    慕无忧阅读 113评论 0 0
  • 从来不知道如何讲故事,想说却又难说。还记得上次看《奇葩说》柏邦妮那句,最悲惨的是暗恋。或许故事就从这里开始。 ...
    云上风的想念阅读 453评论 0 1