云上社群系统部分接口设计详解与测试

news2024/9/28 19:21:38

目录

一、项目简介

1. 使用统一返回格式+全局错误信息定义处理前后端交互时的返回结果

2.使用@ControllerAdvice+@ExceptionHandler实现全局异常处理

 3.使用拦截器实现用户登录校验

4. 使用MybatisGeneratorConfig生成常的增删改查方法

5. 集成Swagger实现自动生成API测试接口

6. 使用jQuery完成AJAX请求,并处理HTML页面标签

7. 对数据库中常用的查询字段段建立索引,并使用查询计划分析索引是否生效

8.对用户密码进行MD5加密

二、技术选型及交互图

 三、数据库设计

四、接口设计及思考

接口设计共分具体8个步骤:

 编写类与映射⽂件

生成类与映射⽂件(了解)

创建generatorConfig.xml

运⾏插件⽣成⽂件

4.1 回复帖子

4.1.1.1 实现逻辑

4.1.1.2创建Service接⼝

4.1.1.3 实现Service接⼝

4.1.1.4 实现Controller

4.1.1.5 测试接口

4.1.1.6 实现前端页面

4.2 点赞帖子

4.2.1.1 参数要求

4.2.1.2 创建Service接⼝

4.2.1.3 实现Service接⼝

4.3 删除帖子

4.3.1.1 参数要求

 4.3.1.2 创建Service接口

4.3.1.3 实现Service接⼝

4.3.1.4 实现Controller

五、项目测试

测试用例  ⁡⁤‍⁤‍‍‍​⁡⁣‌​‌⁢​⁢‬⁤‬‌‬‌⁡⁤‌‌‍​‬​‌⁡​⁡⁤⁤⁣⁡‬‌‌​​⁡‍云上社群系统测试用例 - 飞书云文档 (feishu.cn)

编写功能测试用例,对每个模块进行功能测试

用swagger框架进行API管理和接口规整,并对每一个接口都进行了测试

          单元测试



一、项目简介

项目URL:云上社群 - 用户登录

项目源码: https://gitee.com/li-dot/forum.git
本系统实现了基于 Spring 的前后端分离版本的社群系统, 后端主要采用了SSM架构,前端采用ajax和后端进行交互,采用MySQL 数据库,实现了用户登录注册、个人信息和帖子的查看、发布帖子、帖子回复、站内信等功能。
业务实现过程中主要的包和目录及主要功能:
model 包:实体对象
dao 包:数据库访问
services 包:业务处理相关的接⼝与实现,调用dao完成数据的持久化,所有业务都在Services中实现,如果需要执行多条数据库更新操作,那么就需要用事务管理。
controller 包:提供URL映射,⽤来接收参数并做校验,封装Service层需要用的对象,最终为客户端响应结果。
src/main/resources/mapper ⽬录:Mybaits映射⽂件,配置数据库实体与类之间的映射关系
src/main/resources/static ⽬录:前端资源
应用技术有:SpringBoot、SpringMVC、Mybaits、MySQL、CSS等。
功能亮点:
本项目围绕帖子与用户两个核心角色进行业务处理,在构建项目时,基本功能有对帖子的操作(增删改查),点赞,对用户的登录注册等,除此之外介绍以下用到的技术点。


1. 使用统一返回格式+全局错误信息定义处理前后端交互时的返回结果

对执⾏业务处理逻辑过程中可能出现的成功与失败状态做针对性描述,⽤枚举定义状态码ResultCode com.example.forum.common 包下创建枚举类型命名为ResultCode
package com.example.forum.common;

/**
 * @author Dian
 * Description
 * Date:2023/8/7:19:06
 */
public enum ResultCode {
    //全局定义
    SUCCESS                 (0, "操作成功"),
    FAILED                  (1000, "操作失败"),
    FAILED_UNAUTHORIZED     (1001, "未授权"),
    FAILED_PARAMS_VALIDATE  (1002, "参数校验失败"),
    FAILED_FORBIDDEN        (1003, "禁止访问"),
    FAILED_CREATE           (1004, "新增失败"),
    FAILED_NOT_EXISTS       (1005, "资源不存在"),
    //用户的定义
    FAILED_USER_EXISTS      (1101, "用户已存在"),
    FAILED_USER_NOT_EXISTS  (1102, "用户不存在"),
    FAILED_LOGIN            (1103, "用户名或密码错误"),
    FAILED_USER_BANNED      (1104, "您已被禁言, 请联系管理员, 并重新登录."),
    FAILED_TWO_PWD_NOT_SAME (1105, "两次输入的密码不一致"),
    FAILED_PASSWOED_CHECK (1106, "密码校验失败"),
    //版块的定义
    FAILED_BOARD_EXISTS      (1201, "版块已存在"),
    FAILED_BOARD_NOT_EXISTS  (1202, "版块不存在"),

    // 帖子相关的定义
    FAILED_ARTICLE_NOT_EXISTS  (1302, "帖子不存在"),

    FAILED_ARTICLE_STATE (1301,"帖子有效性异常"),

    //服务器定义
    ERROR_SERVICES          (2000, "服务器内部错误"),
    ERROR_IS_NULL           (2001, "IS NULL.");
    long code;
    String message;

    public long getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    ResultCode(long code, String message) {
        this.code = code;
        this.message = message;
    }

    @Override
    public String toString() {
        return "code = " + code + ",message = " + message +".";
    }
}

系统实现前后端分离,统⼀返回JSON格式的字符串,需要定义⼀个类,其中包含状态码,描述信息,返回的结果数据,在com.example.forum.common包下创建AppResult类

@ApiModel("统一返回对象")
public class AppResult<T> {
    @ApiModelProperty("状态码")
    @JsonInclude(JsonInclude.Include.ALWAYS)
    private long code;
    @JsonInclude(JsonInclude.Include.ALWAYS)
    @ApiModelProperty("描述信息")
    private String message;
    @JsonInclude(JsonInclude.Include.ALWAYS)
    @ApiModelProperty("具体对象")
    private T data;
    public AppResult(long code, String message) {
        this.code = code;
        this.message = message;
    }
    public AppResult(long code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }


    /**
     * 成功
     */
    public static AppResult success(){
        return new AppResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.message);

    }
    public static AppResult success(String message){
        return new AppResult(ResultCode.SUCCESS.getCode(), message);

    }
    public static <T> AppResult<T> success (String message,T data){
   return new AppResult<>(ResultCode.SUCCESS.getCode(),message,data);
    }

    public static <T> AppResult<T> success (T data){
        return new AppResult<>(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.message,data);
    }

    /**
     * 失败
     */

    public static AppResult failed(){
        return new AppResult(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage());

    }

    public static AppResult failed(String message){
        return new AppResult(ResultCode.FAILED.getCode(), message);

    }

    public static AppResult failed(ResultCode resultCode){
        return new AppResult(resultCode.getCode(), resultCode.getMessage());

    }

    public long getCode() {
        return code;
    }

    public void setCode(long code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}


2.使用@ControllerAdvice+@ExceptionHandler实现全局异常处理

自定义异常, 创建⼀个异常类,加⼊状态码与状态描述属性 .forum.exception包下创建ApplicationException
/**
 * @author Dian
 * Description
 * Date:2023/8/8:14:33
 */
public class ApplicationException extends RuntimeException{

    private static final long serialVersionUID = -3533806916645793660L;
    protected AppResult errorResult;

    // 指定状态码,异常描述
    public ApplicationException(AppResult errorResult) {
        super(errorResult.getMessage());
        this.errorResult = errorResult;
    }

    // ⾃定义异常描述
    public ApplicationException(String message) {
        super(message);
    }
    // 指定异常
    public ApplicationException(Throwable cause) {
        super(cause);
    }
    // ⾃定义异常描述,异常信息
    public ApplicationException(String message, Throwable cause) {
        super(message, cause);
    }
    public AppResult getErrorResult() {
        return errorResult;
    }
}

@ControllerAdvice 表⽰控制器通知类   forum.exception包下创建GlobalExceptionHandler
/**
 * @author Dian
 * Description
 * Date:2023/8/8:14:40
 */
    /**
     * 统一异常处理
     */
    //添加注解
    @Slf4j
    @ControllerAdvice
    public class GlobalExceptionHandler {
/**
  * 处理⾃定义的已知异常
  * @param e ApplicationException
  * @return AppResult
  */
        // 以JSON形式返回BODY中的数据
        @ResponseBody
        @ExceptionHandler(ApplicationException.class)
        public AppResult handleApplication(ApplicationException e){
           //打印异常
            e.printStackTrace();
            //记录日志
            log.error(e.getMessage());
            //获取异常信息,判断自定义的异常是否为空
            if(e.getErrorResult()!= null){
                //返回异常类中记录的状态
                return e.getErrorResult();
            }
            //根据异常信息封装Appresult
            return AppResult.failed(e.getMessage());
        }

/**
  * 处理全未捕获的其他异常
  * @param e Exception
  * @return AppResult
  */
         @ResponseBody
         @ExceptionHandler(Exception.class)
         public AppResult handleException(Exception e){
             //打印异常
             e.printStackTrace();
             //记录日志
             log.error(e.getMessage());
             //非空校验
             if(e.getMessage() == null){
                 return AppResult.failed(ResultCode.ERROR_SERVICES);
             }
             //根据异常信息封装Appresult
             return AppResult.failed(e.getMessage());
         }








 
3.使用拦截器实现用户登录校验

在interceptor包下创建LoginInterceptor
/**
 * @author Dian
 * Description
 * Date:2023/8/11:14:38
 */
@Component// 加入到Spring中
public class LoginInterceptor implements HandlerInterceptor {
    // 从配置文件中读取配置内容
    @Value("${bit-forum.login.url}")
    private String defautURL;

    /**
     * 预处理(请求的前置处理)回调方法<br/>
     *
     * @return true 继续请求流程 </br> false 中止请求流程
     * @throws Exception
     */

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler)
                              throws Exception{
        HttpSession session = request.getSession(false);
        if(session != null && session.getAttribute(AppConfig.SESSION_USER_KEY) != null){
return true;
        }
        // 校验不通过时要处理的逻辑
        // 1. 返回一个错误的HTTP状态码
//        response.setStatus(403);
        // 2. 跳转到某一个页面
        // 对URL前缀做校验(确保目标URL从根目录开发)
        if(!defautURL.startsWith("/")){
            defautURL = "/" +defautURL;
        }
        response.sendRedirect(defautURL);
        return false;
    }
}

修改application.yml配置⽂件,添加跳转⻚⾯
forum:
  login:
  url: sign-in.html # 未登录状况下强制跳转⻚⾯
在interceptor包下创建AppInterceptorConfigurer
/**
 * @author Dian
 * Description
 * Date:2023/8/11:14:55
 */
@Configuration// 把当前配置类加入到Spring中
public class AppInterceptorConfigure implements WebMvcConfigurer {

    @Resource
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加登录拦截器
        registry.addInterceptor(loginInterceptor) // 添加⽤⼾登录拦截器
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns("/sign-in.html") // 排除登录HTML
                .excludePathPatterns("/sign-up.html") // 排除注册HTML
                .excludePathPatterns("/user/login") // 排除登录api接⼝
                .excludePathPatterns("/user/register") // 排除注册api接⼝
                .excludePathPatterns("/user/logout") // 排除退出api接⼝
                .excludePathPatterns("/swagger*/**") // 排除登录swagger下所有
                .excludePathPatterns("/v3*/**") // 排除登录v3下所有,与swag
                .excludePathPatterns("/dist/**") // 排除所有静态⽂件
                . excludePathPatterns("/image/**")
                .excludePathPatterns("/**.ico")
                .excludePathPatterns("/js/**");
    }
}


4. 使用MybatisGeneratorConfig生成常的增删改查方法

在做项目的时候,写类与映射⽂件时学习到的,了解到这个Maven插件,就是一个代码生成器,可以非常方便的生成mapper文件。

• 在 src/main/resources下创建mybatis⽬录,在mybatis⽬录下创建generatorConfig.xml⽂
件,运⾏插件⽣成⽂件
• 在 src/main/resources下创建mapper⽬录
• 点下下图重新加载Maven项⽬,在Plugins节点下出现mybatis-generator,双击运⾏,在对应的
⽬录下会⽣成相应的类与映射⽂件。
下文有详细介绍。


5. 集成Swagger实现自动生成API测试接口

Swagger 简介:Swagger是⼀套API定义的规范,按照这套规范的要求去定义接⼝及接⼝相关信息, 再通过可以解析这套规范⼯具,就可以⽣成各种格式的接⼝⽂档,以及在线接⼝调试⻚⾯,通过⾃动⽂档的⽅式,解决了接⼝⽂档更新不及时的问题。
实现API自动生成
使⽤Springfox Swagger⽣成API,并导⼊Postman,完成API单元测试
使用方法:
pom.xml中引⽤依赖
统⼀管理版本,在properties标签中加⼊版本号
编写配置类
在com.bitejiuyeke.forum.config包下新建SwaggerConfig.java
application.yml 中添加配置
在 spring 节点下添加mvc配置项
记录一些API常⽤注解
@Api: 作⽤在Controller上,对控制器类的说明
tags="说明该类的作⽤,可以在前台界⾯上看到的注解"
@ApiModel: 作⽤在响应的类上,对返回响应数据的说明
@ApiModelProerty:作⽤在类的属性上,对属性的说明
@ApiOperation: 作⽤在具体⽅法上,对API接⼝的说明,描述方法
@ApiParam: 作⽤在⽅法中的每⼀个参数上,对参数的属性进⾏说明
@RequestParam:请求时指定参数名
@NonNull:请求时参数不能为空
修改测试接⼝
修改TestController中的⽅法,添加关于API的注释
访问API列表
启动程序,浏览器中输⼊地址: http://127.0.0.1:58080/swagger-ui/index.html ,可以正常并
显⽰接⼝信息,说明配置成功,此时接⼝信息已经显⽰出来了,可以分别针每个接⼝进⾏测试,具
体操作按⻚⾯指引即可。


6. 使用jQuery完成AJAX请求,并处理HTML页面标签

7. 对数据库中常用的查询字段段建立索引,并使用查询计划分析索引是否生效

8.对用户密码进行MD5加密

密码在数据库中不会去存明文,明文密码 + 扰动字符串(salt盐 随机字符) =  密文密码

MD5(MD5(明文密码) + salt) = 密码对应的密文

 创建MD5加密⼯具类

项⽬中使⽤commons-codec,它是Apache提供的⽤于摘要运算、编码解码的⼯具包。常⻅的编码解码⼯具Base64、MD5、Hex、SHA1、DES等。
pom.xml中导⼊依赖,SpringBoot已经对这个包做了版本管理,所以这⾥不⽤指定版本号
在forum.utils包下创建MD5Util类,代码如下:
/**
 * @author Dian
 * Description
 * Date:2023/8/8:20:26
 */
public class MD5Utils {
    /**
     * 普通MD5加密
     * @param str 原始字符串
     * @return 一次MD5加密后的密文
     */
    public static String md5 (String str) {
        return DigestUtils.md5Hex(str);
    }

    /**
     * 原始字符串与Key组合进行一次MD5加密
     * @param str 原始字符串
     * @param key
     * @return 组合字符串一次MD5加密后的密文
     */
    public static String md5 (String str, String key) {
        return DigestUtils.md5Hex(str + key);
    }

    /**
     * 原始字符串加密后与扰动字符串组合再进行一次MD5加密
     * @param str 原始字符串
     * @param salt 扰动字符串
     * @return 加密后的密文
     */
    public static String md5Salt (String str, String salt) {
        return DigestUtils.md5Hex(DigestUtils.md5Hex(str) + salt);
    }

    /**
     * 校验原文与盐加密后是否与传入的密文相同
     * @param original 原字符串
     * @param salt 扰动字符串
     * @param ciphertext 密文
     * @return true 相同, false 不同
     */
    public static boolean verifyOriginalAndCiphertext (String original, String salt, String ciphertext) {
        String md5text = md5Salt(original, salt);
        if (md5text.equalsIgnoreCase(ciphertext)) {
            return true;
        }
        return false;
    }
}

 创建⽣成UUID⼯具类

⾃定义的⼯具类,
在forum.utils包下创建UUIDUtil类
/**
 * @author Dian
 * Description
 * Date:2023/8/8:20:22
 */
public class UUIDUtils {
    /**
     * 生成一个标准的36字符的UUID
     * @return
     */
    public static String UUID_36 () {
        return UUID.randomUUID().toString();
    }

    /**
     * 生成一个32字符的UUID
     * @return
     */
    public static String UUID_32 () {
        return UUID.randomUUID().toString().replace("-", "");
    }

}

Service层

 @Override
    public void createNormalUser(User user) {
        // 非空校验
        if (user == null || StringUtils.isEmpty(user.getUsername())

                || StringUtils.isEmpty(user.getNickname()) || StringUtils.isEmpty(user.getPassword())
                || StringUtils.isEmpty(user.getSalt())) {
            // 打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 校验用户名是否存在
        User existsUser = selectByName(user.getUsername());
        if (existsUser != null) {
            // 打印日志
            log.warn(ResultCode.FAILED_USER_EXISTS.toString() + " username = " + user.getUsername());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));
        }
        // 为属性设置默认值
        // 性别
        if (user.getGender() != null) {
            if (user.getGender() < 0 || user.getGender() > 2) {
                user.setGender((byte) 2);
            }
        } else {
            user.setGender((byte) 2);
        }
        // 发帖数
        user.setArticleCount(0);
        // 是否管理员
        user.setIsAdmin((byte) 0);
        // 状态
        user.setState((byte) 0);
        // 是否删除
        user.setDeleteState((byte) 0);
        // 时间
        Date date = new Date();
        user.setCreateTime(date); // 创建时间
        user.setUpdateTime(date); // 更新时间

        // 写入数据库
        int row = userMapper.insertSelective(user);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.FAILED_CREATE.toString() + " 注册用户失败. username = " + user.getUsername());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
    }

Controller层

 @ApiOperation("用户注册")
    @PostMapping("/register")
    public AppResult register (@ApiParam("用户名") @RequestParam("username") @NonNull String username,
                               @ApiParam("昵称") @RequestParam("nickname")  @NonNull String nickname,
                               @ApiParam("密码") @RequestParam("password")  @NonNull String password,
                               @ApiParam("确认密码") @RequestParam("passwordRepeat")  @NonNull String passwordRepeat) {
        // 校验密码是否一致
        if (!password.equals(passwordRepeat)) {
            // 返回错误信息
            return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
        }
        // 构造对象
        User user = new User();
        user.setUsername(username); // 用户名
        user.setNickname(nickname); // 昵称
        // 处理密码
        // 1. 生成盐
        String salt = UUIDUtils.UUID_32();
        // 2. 生成密码的密文
        String encryptPassword = MD5Utils.md5Salt(password, salt);
        // 3. 设置密码和盐
        user.setPassword(encryptPassword);
        user.setSalt(salt);

        // 调用Service
        userService.createNormalUser(user);
        // 返回结果
        return AppResult.success("注册成功");
    }

二、技术选型及交互图

 

 三、数据库设计

数据库名: forum_db

公共字段:⽆特殊要求的情况下,每张表必须有⻓整型的⾃增主键,删除状态、创建时间、更新时 间,如下所⽰:

共建五张表  ,具体操作如下:

 SQL脚本


-- ----------------------------
-- 创建数据库,并指定字符集
-- ----------------------------
drop database if exists forum_db;
create database forum_db character set utf8mb4 collate utf8mb4_general_ci;
-- 选择数据库
use forum_db;

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 创建帖子表 t_article
-- ----------------------------
DROP TABLE IF EXISTS `t_article`;
CREATE TABLE `t_article`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '帖子编号,主键,自增',
  `boardId` bigint(20) NOT NULL COMMENT '关联板块编号,非空',
  `userId` bigint(20) NOT NULL COMMENT '发帖人,非空,关联用户编号',
  `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题,非空,最大长度100个字符',
  `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '帖子正文,非空',
  `visitCount` int(11) NOT NULL DEFAULT 0 COMMENT '访问量,默认0',
  `replyCount` int(11) NOT NULL DEFAULT 0 COMMENT '回复数据,默认0',
  `likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
  `state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0正常 1 禁用,默认0',
  `deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0 否 1 是,默认0',
  `createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
  `updateTime` datetime NOT NULL COMMENT '修改时间,精确到秒,非空',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- 创建帖子回复表 t_article_reply
-- ----------------------------
DROP TABLE IF EXISTS `t_article_reply`;
CREATE TABLE `t_article_reply`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号,主键,自增',
  `articleId` bigint(20) NOT NULL COMMENT '关联帖子编号,非空',
  `postUserId` bigint(20) NOT NULL COMMENT '楼主用户,关联用户编号,非空',
  `replyId` bigint(20) NULL DEFAULT NULL COMMENT '关联回复编号,支持楼中楼',
  `replyUserId` bigint(20) NULL DEFAULT NULL COMMENT '楼主下的回复用户编号,支持楼中楼',
  `content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '回贴内容,长度500个字符,非空',
  `likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
  `state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1禁用,默认0',
  `deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
  `createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
  `updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子回复表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- 创建版块表 t_board
-- ----------------------------
DROP TABLE IF EXISTS `t_board`;
CREATE TABLE `t_board`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '版块编号,主键,自增',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '版块名,非空',
  `articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '帖子数量,默认0',
  `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序优先级,升序,默认0,',
  `state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态,0 正常,1禁用,默认0',
  `deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
  `createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
  `updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '版块表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- 创建站内信表 for t_message
-- ----------------------------
DROP TABLE IF EXISTS `t_message`;
CREATE TABLE `t_message`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '站内信编号,主键,自增',
  `postUserId` bigint(20) NOT NULL COMMENT '发送者,并联用户编号',
  `receiveUserId` bigint(20) NOT NULL COMMENT '接收者,并联用户编号',
  `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容,非空,长度255个字符',
  `state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0未读 1已读,默认0',
  `deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
  `createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
  `updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '站内信表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- 创建用户表 for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户编号,主键,自增',
  `username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名,非空,唯一',
  `password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '加密后的密码',
  `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称,非空',
  `phoneNum` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址',
  `gender` tinyint(4) NOT NULL DEFAULT 2 COMMENT '0女 1男 2保密,非空,默认2',
  `salt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '为密码加盐,非空',
  `avatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户头像URL,默认系统图片',
  `articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '发帖数量,非空,默认0',
  `isAdmin` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否管理员,0否 1是,默认0',
  `remark` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注,自我介绍',
  `state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1 禁言,默认0',
  `deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
  `createTime` datetime NOT NULL COMMENT '创建时间,精确到秒',
  `updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `user_username_uindex`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

-- 写入版块信息数据
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (1, 'Java', 0, 1, 0, 0, '2023-01-14 19:02:18', '2023-01-14 19:02:18');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (2, 'C++', 0, 2, 0, 0, '2023-01-14 19:02:41', '2023-01-14 19:02:41');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (3, '前端技术', 0, 3, 0, 0, '2023-01-14 19:02:52', '2023-01-14 19:02:52');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (4, 'MySQL', 0, 4, 0, 0, '2023-01-14 19:03:02', '2023-01-14 19:03:02');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (5, '面试宝典', 0, 5, 0, 0, '2023-01-14 19:03:24', '2023-01-14 19:03:24');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (6, '经验分享', 0, 6, 0, 0, '2023-01-14 19:03:48', '2023-01-14 19:03:48');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (7, '招聘信息', 0, 7, 0, 0, '2023-01-25 21:25:33', '2023-01-25 21:25:33');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (8, '福利待遇', 0, 8, 0, 0, '2023-01-25 21:25:58', '2023-01-25 21:25:58');
 INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (9, '灌水区', 0, 9, 0, 0, '2023-01-25 21:26:12', '2023-01-25 21:26:12');

-- 写入帖子表数据
insert into t_article values (null, 1, 1, '测试数据-标题1', '测试数据-内容1', 0,0,0,0,0, '2023-08-11 16:52:00','2023-08-11 16:52:00');
insert into t_article values (null, 1, 1, '测试数据-标题2', '测试数据-内容2', 0,0,0,0,0, '2023-08-11 16:53:00','2023-08-11 16:53:00');
insert into t_article values (null, 1, 1, '测试数据-标题3', '测试数据-内容3', 0,0,0,0,0, '2023-08-11 16:54:00','2023-08-11 16:54:00');
insert into t_article values (null, 2, 2, '测试数据-标题4', '测试数据-内容4', 0,0,0,0,0, '2023-08-11 16:55:00','2023-08-11 16:55:00');
insert into t_article values (null, 2, 2, '测试数据-标题5', '测试数据-内容5', 0,0,0,0,0, '2023-08-11 16:56:00','2023-08-11 16:56:00');

-- 写入回复表数据
insert into t_article_reply values (NULL, 1, 1, NULL, NULL, '回复内容111', 0, 0, 0, '2023-08-14 16:52:00', '2023-08-14 16:52:00');
insert into t_article_reply values (NULL, 1, 1, NULL, NULL, '回复内容222', 0, 0, 0, '2023-08-14 16:53:00', '2023-08-14 16:53:00');
insert into t_article_reply values (NULL, 1, 1, NULL, NULL, '回复内容333', 0, 0, 0, '2023-08-14 16:54:00', '2023-08-14 16:54:00');
insert into t_article_reply values (NULL, 1, 2, NULL, NULL, '回复内容444', 0, 0, 0, '2023-08-14 16:55:00', '2023-08-14 16:55:00');
insert into t_article_reply values (NULL, 1, 2, NULL, NULL, '回复内容555', 0, 0, 0, '2023-08-14 16:56:00', '2023-08-14 16:56:00');

四、接口设计及思考

本项目共设计23个接口,介绍其设计思路及其中三个的具体实现。

接口设计共分具体8个步骤:

1.在Mapper.xml中编写语句

2.在Mapper.java中定义方法

3.定义Service接口

4.实现Service接口

5.单元测试

6.Controller实现方法并对外提供API接口

7.测试API接口(swaggerUI)

8.实现前端逻辑,完成前后端交互

 编写类与映射⽂件
1 根据数据库编写实体类
2 编写映射⽂件xxxMapper.xml
3 编写Dao类,xxxMapper.java

成类与映射⽂件(了解)

 pom.xml 中引⽤依赖
统⼀管理版本,在properties标签中加⼊版本号
<mybatis-generator-plugin-version>1.4.1</mybatis-generator-plugin-version>
在 build --> plugins 标签中加⼊如下配置
<!-- mybatis ⽣成器插件 -->
<plugin>
 <groupId>org.mybatis.generator</groupId>
 <artifactId>mybatis-generator-maven-plugin</artifactId>
 <version>${mybatis-generator-plugin-version}</version>
 <executions>
 <execution>
 <id>Generate MyBatis Artifacts</id>

<phase>deploy</phase>
 <goals>
 <goal>generate</goal>
 </goals>
 </execution>
 </executions>
 <!-- 相关配置 -->
 <configuration>
 <!-- 打开⽇志 -->
 <verbose>true</verbose>
 <!-- 允许覆盖 -->
 <overwrite>true</overwrite>
 <!-- 配置⽂件路径 -->
 <configurationFile>
 src/main/resources/mybatis/generatorConfig.xml
 </configurationFile>
 </configuration>
</plugin

创建generatorConfig.xml

在 src/main/resources下创建mybatis⽬录,在mybatis⽬录下创建generatorConfig.xml⽂
件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!-- 驱动包路径,location中路径替换成自己本地路径 -->
    <classPathEntry location="D:\apache-tomcat-8.5.49\webapps\blog\WEB-INF\lib\mysql-connector-java-5.1.49.jar"/>

    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- 禁用自动生成的注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true"/>
            <property name="suppressDate" value="true"/>
        </commentGenerator>

        <!-- 连接配置 -->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://127.0.0.1:3306/forum_db?characterEncoding=utf8&amp;useSSL=false"
                        userId="root"
                        password="123456">
        </jdbcConnection>

        <javaTypeResolver>
            <!-- 小数统一转为BigDecimal -->
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!-- 实体类生成位置 -->
        <javaModelGenerator targetPackage="com.example.forum.model" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!-- mapper.xml生成位置 -->
        <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!-- DAO类生成位置 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.forum.dao" targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!-- 配置生成表与实例, 只需要修改表名tableName, 与对应类名domainObjectName 即可-->
        <table tableName="t_article" domainObjectName="Article" enableSelectByExample="false"
               enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
               enableUpdateByExample="false">
            <!-- 类的属性用数据库中的真实字段名做为属性名, 不指定这个属性会自动转换 _ 为驼峰命名规则-->
            <property name="useActualColumnNames" value="true"/>
        </table>
        <table tableName="t_article_reply" domainObjectName="ArticleReply" enableSelectByExample="false"
               enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
               enableUpdateByExample="false">
            <property name="useActualColumnNames" value="true"/>
        </table>
        <table tableName="t_board" domainObjectName="Board" enableSelectByExample="false" enableDeleteByExample="false"
               enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false">
            <property name="useActualColumnNames" value="true"/>
        </table>
        <table tableName="t_message" domainObjectName="Message" enableSelectByExample="false"
               enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
               enableUpdateByExample="false">
            <property name="useActualColumnNames" value="true"/>
        </table>
        <table tableName="t_user" domainObjectName="User" enableSelectByExample="false" enableDeleteByExample="false"
               enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false">
            <property name="useActualColumnNames" value="true"/>
        </table>

    </context>
</generatorConfiguratio

运⾏插件⽣成⽂件

在 src/main/resources下创建mapper⽬录

点下下图重新加载Maven项⽬,在Plugins节点下出现mybatis-generator,双击运⾏,在对应的
⽬录下⽣成相应的类与映射⽂件,如下图所⽰:

 

回复帖子部分

4.1 回复帖子

4.1.1 提交回复内容

在帖⼦详情⻚⾯⽤⼾可以发表回复

4.1.1.1 实现逻辑

1. 帖⼦在正常状态下允许⽤⼾回复   (校验1)
2. 填写回复内容,点击提交按钮后发送请求到服务器
3. 服务器校验回复内容、帖⼦与⽤⼾状态(校验二),通过后写⼊数据库 (回复表中新增一条数据)
4. 帖⼦回复数量加1 (帖子表的更新)
5. 返回结果
思考:发表回复时,需要对两张表进行更新操作,那么就需要用事务管理起来
使用事务的原则:当一个执行流程中,要进行多个更新操作(增删改),不论涉及几张表都需要用事务管理起来。

4.1.1.2创建Service接⼝

在IArticleReplyService定义⽅法

    /**
     * 新增一个回复记录
     * @param articleReply
     */
    @Transactional
    void create (ArticleReply articleReply);
}

4.1.1.3 实现Service接⼝

在ArticleReplyServiceImpl中实现⽅法

4.1.1.4 实现Controller

新建ArticleReplyController并提供对外的API接⼝

4.1.1.5 测试接口

测试⽤例:
1. ⽤⼾在没有登录的情况下回复帖⼦,不成功
2. 回复已删除或已禁⽤的帖⼦,不成功 3. 回复不存在的帖⼦,不成功
4. 禁⾔⽤⼾回复帖⼦,不成功
5. ⽤⼾和帖⼦状态正常的情况下,回复帖⼦,成功
6. 回复成功,帖⼦回复数量加1
7. 在测试⼯具中编辑并在数据库中查看验证是否成功

4.1.1.6 实现前端页面

// ====================== 回复帖⼦ ======================
 $('#details_btn_article_reply').click(function () {
 let articleIdEl = $('#details_article_id');
 let replyContentEl = $('#details_article_reply_content');
  // ⾮空校验
 if (!replyContentEl.val()) {
 // 提⽰
 $.toast({
 heading: '提⽰',
 text: '请输⼊回复内容',
 icon: 'warning'
 });
 return;
 }

 // 构造帖⼦对象
 let articleReplyObj = {
 articleId: articleIdEl.val(),
 content: replyContentEl.val()
 }
 // 发送请求
 $.ajax({
 type: 'post',
 url: '/article/reply',
 // 发送的数据
 data: articleReplyObj,
 success: function (respData) {
 // ⽤状态码判断是否成功
 if (respData.code == 0) {
 // 清空输⼊区
 editor.setValue('');
 // 更新回贴数
 currentArticle.replyCount = currentArticle.replyCount + 1;
 $('#details_article_replyCount').html(currentArticle.replyCount);
 // 构建回贴⻚⾯
 loadArticleDetailsReply();
 $.toast({
 heading: '提⽰',
 text: respData.message,
 icon: 'success'
 });
 } else {
 // 提⽰
 $.toast({
 heading: '提⽰',
 text: respData.message,
 icon: 'info'
 });
 }
 },
 error: function () {
 $.toast({
 heading: '错误',
 text: '出错了,请联系管理员',
 icon: 'error'
 });
 }
 });
});

4.2 点赞帖子

⽤⼾在帖⼦详情⻚进⾏点赞操作

4.2.1.1 参数要求

4.2.1.2 创建Service接⼝

在IArticleService定义⽅法

    /**
     * 点赞
     * @param id 帖子Id
     */
    void thumbsUpById(Long id);
}

4.2.1.3 实现Service接⼝

在ArticleServiceImpl中实现⽅法
    @Override
    public void thumbsUpById(Long id) {
        //非空校验
        if (id == null || id <= 0) {
            // 打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //查询帖子信息
        Article article = selectById(id);
        if (article == null || article.getState() == 1 || article.getDeleteState() == 1) {
            //打印日志
            log.info(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
        }
        //构造更新对象
        Article update = new Article();
        update.setId(article.getId());
        update.setLikeCount(article.getLikeCount() + 1);

        //更新数据库
        int row = articleMapper.updateByPrimaryKeySelective(update);
        if (row != 1) {
            log.info(ResultCode.FAILED_CREATE.toString() + "userId = " + article.getUserId());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
    }
}
4.2.1.4   实现Controller
在ArticleController中提供对外的API接⼝

    @ApiOperation("点赞")
 @PostMapping("/thumbsUp")
    public AppResult thumbsUp(HttpServletRequest request,
                              @ApiParam(value = "帖子Id") @RequestParam(value = "id") @NonNull Long id){
        // 获取⽤⼾信息
        HttpSession session = request.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        // 判断是否被禁⾔
        if (user.getState() != 0) {
            // ⽇志
            log.warn(ResultCode.FAILED_USER_BANNED.toString() + ", userId = " +
                    user.getId());
            // 返回错误信息
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        // 更新点赞数
        articleService.thumbsUpById(id);

        // 返回结果
        return AppResult.success();
    }

4.3 删除帖子

4.3.1.1 参数要求

 4.3.1.2 创建Service接口

在IBoardService定义⽅法
    /**
     * 版块中的帖子数量 -1
     * @param id 版块Id
     */
    void subOneArticleCountById(Long id);
}
在IUserService定义⽅法
 /**
  * 用户发帖数 -1
  * @param id
  */
 void subOneArticleCountById(Long id);
}
在IArticleService定义⽅法,并加⼊事务管理

    /**
     * 根据Id删除帖子
     * @param id 帖子Id
     */
@Transactional
    void deleteById(Long id);
}

4.3.1.3 实现Service接⼝

在BoardServiceImpl中实现⽅法
    @Override
    public void subOneArticleCountById(Long id) {
        //非空校验
        if (id == null || id < 0) {
            //打印日志
            log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));

        }
        //查询板块信息
        Board board = selectById(id);
        // 校验版块是否存在
        if (board == null) {
            // 打印日志
            log.warn(ResultCode.FAILED_BOARD_NOT_EXISTS.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_NOT_EXISTS));
        }
        // 构造要更新的对象
        Board updateBoard = new Board();
        updateBoard.setId(board.getId()); // 版块ID
        updateBoard.setArticleCount(board.getArticleCount() - 1); // 帖子数量
        if (updateBoard.getArticleCount() < 0) {
            //如果小于0那么设置为0
            updateBoard.setArticleCount(0);
        }
        // 调用DAO
        int row = boardMapper.updateByPrimaryKeySelective(updateBoard);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.FAILED.toString() + ",受影响的行数不等于1.");
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED));
        }
    }
}
在UserServiceImpl中实现⽅法
    @Override
    public void subOneArticleCountById(Long id) {
        //非空校验
        if (id == null || id < 0) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //根据用户名查询用户信息
        User user = userMapper.selectById(id);
        //校验用户是否存在
        if(user == null){
            // 打印日志
            log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString() + ", user id = " + id);
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }
        //构造要更新的对象
        User updateUser = new User();
        updateUser.setId(user.getId());//用户Id
        updateUser.setArticleCount(user.getArticleCount() - 1);//帖子数量-1
        //判断-1之后,用户的发帖数是否小于0
        if(updateUser.getArticleCount() < 0){
            //如果小于0,则设置为0
            updateUser.setArticleCount(0);
        }
        //调用DAO
        int row = userMapper.updateByPrimaryKeySelective(updateUser);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.FAILED.toString() + ",受影响的行数不等于1.");
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED));
        }
在ArticleServiceImpl中实现⽅法
    @Override
    public void deleteById(Long id) {
        //非空校验
        if (id == null || id <= 0) {
            // 打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //根据Id查询帖子信息
        Article article = articleMapper.selectByPrimaryKey(id);
        if (article == null || article.getDeleteState() == 1) {
            //打印日志
            log.info(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString() + ",article id = " + id);
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
        }
        //构造更新对象
        Article updateArticle = new Article();
        updateArticle.setId(article.getId());
        updateArticle.setDeleteState((byte) 1);

        //更新数据库
        int row = articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row != 1) {
            log.info(ResultCode.ERROR_SERVICES.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        //更新版块中的帖子数量
        boardService.subOneArticleCountById(article.getUserId());
        //更新用户发帖数
        userService.subOneArticleCountById(article.getUserId());
        log.info("删除帖子成功,article id = " + article.getId() + ",user id = " + article.getUserId() + ".");

    }

4.3.1.4 实现Controller

在ArticleController中提供对外的API接⼝
/**
  * 根据Id删除帖⼦
  *
  * @param id 帖⼦Id
  * @return
  */
@ApiOperation("删除帖⼦")
@PostMapping("/delete")
public AppResult deleteById (HttpServletRequest request,
 @ApiParam("帖⼦Id") @RequestParam("id") @NonNull Long
                                     id) {
        // 1. ⽤⼾是否禁⾔
        HttpSession session = request.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        if (user.getState() == 1) {
             // 返回错误描述
             return AppResult.failed(ResultCode.FAILED_USER_BANNED);
             }
        //查询帖子信息
        Article article = articleService.selectById(id);
        //2.帖子是否存在或已删除
         if (article == null || article.getDeleteState() == 1) {
             // 返回错误描述
             return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
             }
         // 3. ⽤⼾是否是作者
         if (article.getUserId() != user.getId()) {
             // 返回错误描述
             return AppResult.failed(ResultCode.FAILED_FORBIDDEN);
             }
         // 调⽤Service执⾏删除
         articleService.deleteById(id);
         // 返回成功
         return AppResult.success();
         }
         

五、项目测试

测试环境
操作系统:Windows 11 家庭版

项目运行:IDEA2022.2.3、maven、JDK1.8

浏览器:Chorme、FireFox

网络:127.0.0.1:8080

测试技术: 主要采用自动化测试以及手工测试

测试人员: 李点点
 

项目名称
PersonalBlog博客园

开发时间
2023年7月--2023年8月

风险
项目上线风险:无风险
 

测试用例  ⁡⁤‍⁤‍‍‍​⁡⁣‌​‌⁢​⁢‬⁤‬‌‬‌⁡⁤‌‌‍​‬​‌⁡​⁡⁤⁤⁣⁡‬‌‌​​⁡‍云上社群系统测试用例 - 飞书云文档 (feishu.cn)

编写功能测试用例,对每个模块进行功能测试

登录模块:

场景1:

个人信息:
1. 未登录的情况下直接修改,应返回出错信息
2. 登录后,只填Id其他为空,应返回出错信息
3. 登录后,填写的Id与当前登录⽤⼾Id不匹配,应返回出错信息
4. 登录后,填写的新⽤⼾名已存在,应返回出错信息
5. 登录后,填写正确的内容,应返回成功信息
6. 成功后,⽤新⽤⼾名登录,应返回成功信息
场景2:
密码:
1. 未登录的情况下直接修改,应返回出错信息
2. 登录后,只填Id其他为空,应返回出错信息
3. 登录后,填写的Id与当前登录⽤⼾Id不匹配,应返回出错信息
4. 登录后,填写的原密码错误,应返回出错信息
5. 登录后,填写的新密码与重复密码不⼀样,应返回出错信息
6. 成功后,⽤新密码重新登录,应返回成功信息
场景3:
版块:
1. 版块Id为空,服务器返回所有帖⼦
2. 版块Id不为空,服务器返回指定版本Id的帖⼦
http://127.0.0.1:58080/article/getAllByBoardId
http://127.0.0.1:58080/article/getAllByBoardId?boardId=1
帖子模块
场景4:
发布帖子:
1. ⽤⼾在没有登录的情况下不能发新贴
2. 在测试⼯具中发贴并在数据库中查看验证是否成功
3. 查看⽤贴⽤⼾的发贴数是否增加
场景5:
帖子详情:
1. 输⼊不存在的帖⼦Id,返回空结果集
2. 输⼊存在的帖⼦Id,返回帖⼦详细信息
3. 数据库中访问数增加
场景6:
编辑帖子:
1. ⽤⼾在没有登录的情况下不能编辑帖⼦,不成功
2. 修改不是当前登录⽤⼾发的帖⼦,不成功
3. 修改已删除或已禁⽤的帖⼦,不成功
4. 修改不存在的帖⼦,不成功
5. 禁⾔⽤⼾修改帖⼦,不成功
6. ⽤⼾和帖⼦状态正常的情况下,修改⾃⼰发的帖⼦,成功
7. 在测试⼯具中编辑并在数据库中查看验证是否成功
场景7:
点赞帖子:
1. ⽤⼾在没有登录的情况下点赞,不成功
2. 修改不存在的帖⼦,不成功
3. 禁⾔⽤⼾修改帖⼦,不成功
4. ⽤⼾和帖⼦状态正常的情况下,成功
场景8:
删除帖子:
1. ⽤⼾只能删除⾃⼰的帖⼦
场景9:
回复帖子:
1. ⽤⼾在没有登录的情况下回复帖⼦,不成功
2. 回复已删除或已禁⽤的帖⼦,不成功
3. 回复不存在的帖⼦,不成功
4. 禁⾔⽤⼾回复帖⼦,不成功
5. ⽤⼾和帖⼦状态正常的情况下,回复帖⼦,成功
6. 回复成功,帖⼦回复数量加1
7. 在测试⼯具中编辑并在数据库中查看验证是否成功
场景10:
回复私信:
1. ⽤⼾在没有登录的情况下,不成功
2. 回复接收⽅不是⾃⼰的站内信,不成功
3. 在测试⼯具中编辑并在数据库中查看验证是否成功

用swagger框架进行API管理和接口规整,并对每一个接口都进行了测试

接口展示:

 

 

 用户登录:

 

单元测试

帖子部分:
@SpringBootTest
class ArticleServiceImplTest {
    @Resource
    private IArticleService articleService;
    @Resource
    private ObjectMapper objectMapper;

    @Test
    void selectAll() throws JsonProcessingException {
        List<Article> articles = articleService.selectAll();
        System.out.println(objectMapper.writeValueAsString(articles));
    }

    @Test
    void selectByBoardId() throws JsonProcessingException {
        // JAVA 版块
        List<Article> articles = articleService.selectByBoardId(1l);
        System.out.println(objectMapper.writeValueAsString(articles));

        // C++版块
        articles = articleService.selectByBoardId(2l);
        System.out.println(objectMapper.writeValueAsString(articles));

        // 不存在的版块
        articles = articleService.selectByBoardId(100l);
        System.out.println(objectMapper.writeValueAsString(articles));
    }

    @Test
    @Transactional
    void create() {
        Article article = new Article();
        article.setBoardId(9l);
        article.setUserId(1l);
        article.setTitle("单元测试标题1");
        article.setContent("单元测试内容1");
        // 调用Service
        articleService.create(article);
        System.out.println("写入成功");
    }

    @Test
    @Transactional
    void selectById() throws JsonProcessingException {
        Article article = articleService.selectById(1l);
        System.out.println(objectMapper.writeValueAsString(article));

        Article article2 = articleService.selectById(5l);
        System.out.println(objectMapper.writeValueAsString(article));

        Article article3 = articleService.selectById(3l);
        System.out.println(objectMapper.writeValueAsString(article));
    }

    @Test
    @Transactional
    void updateVisitCountById() {
        articleService.updateVisitCountById(1l);
        System.out.println("更新成功");

        articleService.updateVisitCountById(100l);
        System.out.println("更新成功");
    }

    @Test
    @Transactional
    void modify() {
        articleService.modify(11l, "测试提示效果111", "测试提示效果111");
        System.out.println("更新成功");

    }

    @Test
    @Transactional
    void updateById() {
        Article article = new Article();
        article.setId(1l);
        article.setUpdateTime(new Date());
        articleService.updateById(article);
        System.out.println("更新成功");
    }

    @Test
    void selectByUserId() throws JsonProcessingException {
        List<Article> articleList = articleService.selectByUserId(1l);
        System.out.println(objectMapper.writeValueAsString(articleList));

        articleList = articleService.selectByUserId(2l);
        System.out.println(objectMapper.writeValueAsString(articleList));

        articleList = articleService.selectByUserId(100l);
        System.out.println(objectMapper.writeValueAsString(articleList));
    }
}

登录页面:

@SpringBootTest
class UserServiceImplTest {
    @Resource
    private IUserService userService;
    @Resource
    private ObjectMapper objectMapper;
    @Test
    void selectByName() throws JsonProcessingException {
        User user = userService.selectByName("boy");
        System.out.println(objectMapper.writeValueAsString(user));
        System.out.println("============================================");
        user = userService.selectByName("boy111");
        System.out.println(objectMapper.writeValueAsString(user));
        user = userService.selectByName("");
        System.out.println(objectMapper.writeValueAsString(user));
    }

    @Test
    void createNormalUser() {
        //构造用户
        User user = new User();
        user.setUsername("TestUser");
        user.setNickname("单元测试yonghu");
        user.setPassword("123456");
        user.setSalt("123456");
        //调用Service
        userService.createNormalUser(user);
        System.out.println("注册成功");
        System.out.println("====================================");

        user.setUsername("bitboy");
        //调用Service
        userService.createNormalUser(user);
        System.out.println("注册成功");
    }

    @Test
    void login() throws JsonProcessingException {
       User user =  userService.login("ld","123456");
        System.out.println(objectMapper.writeValueAsString(user));

//
//        user = userService.login("123456","123456");
//        System.out.println(objectMapper.writeValueAsString(user));
    }


    @Test
    @Transactional
    void selectById() throws JsonProcessingException {
        User user = userService.selectById(1l);
        System.out.println(objectMapper.writeValueAsString(user));

        User user2 = userService.selectById(2l);
        System.out.println(objectMapper.writeValueAsString(user2));

        User user3 = userService.selectById(20l);
        System.out.println(objectMapper.writeValueAsString(user3));
    }

    @Test
    @Transactional
    void addOneArticleCountById() {
        userService.addOneArticleCountById(1l);
        System.out.println("更新成功");

        userService.addOneArticleCountById(2l);
        System.out.println("更新成功");
//
//        userService.addOneArticleCountById(100l);
//        System.out.println("更新成功");
    }

    @Test
    @Transactional
    void modifyInfo() {
        User user = new User();
        user.setId(2l);
        user.setNickname("girl111");
        user.setGender((byte) 0);
        user.setPhoneNum("15319706666");
        user.setEmail("qq@qq.com");
        user.setRemark("我是一个美丽的小女孩");
        // 调用Service
        userService.modifyInfo(user);
        System.out.println("更新成功");

    }

    @Test
    void modifyPassword() {
        userService.modifyPassword(1l, "654321", "123456");
        System.out.println("更新成功");
    }
}

版块页面:

@SpringBootTest
class BoardServiceImplTest {

    @Resource
    private IBoardService boardService;
    @Resource
    private ObjectMapper objectMapper;

    @Test
    void selectByNum() throws JsonProcessingException {
        List<Board> boards = boardService.selectByNum(9);
        System.out.println(objectMapper.writeValueAsString(boards));

    }

    @Test
    @Transactional
    void selectById() throws JsonProcessingException {
        // JAVA
        Board board = boardService.selectById(1l);
        System.out.println(objectMapper.writeValueAsString(board));

        // C++
        board = boardService.selectById(2l);
        System.out.println(objectMapper.writeValueAsString(board));

        // 不存在
        board = boardService.selectById(100l);
        System.out.println(objectMapper.writeValueAsString(board));
    }

    @Test
    @Transactional
    void addOneArticleCountById() {
    boardService.addOneArticleCountById(1l);
        System.out.println("更新成功");
        boardService.addOneArticleCountById(2l);
        System.out.println("更新成功");
//        boardService.addOneArticleCountById(100l);
//        System.out.println("更新成功");
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/906673.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SVF——C/C++指针分析/(数据)依赖分析框架

这篇文章包括&#xff1a; SVF介绍SVF源码解读SVF优势与不足如何扩展改进 文章包括一些个人观点&#xff0c;若觉得有误请留言纠正&#xff0c;感谢&#x1f64f; 在这篇文章之前强烈推荐看我公众号之前推的一篇文章&#xff1a;CG0’2011 “Flow-sensitive pointer analysis f…

追踪工时和控制成本 如何选对工具

研发工作中的工时管理软件是一种用于追踪、记录和分析团队成员在项目中所花费的工作时间的工具。它有助于组织、监控和优化研发项目的进展&#xff0c;确保资源得到有效利用&#xff0c;项目按时完成&#xff0c;并提供数据支持用于决策制定和资源规划。 能够记录团队成员的工…

高忆管理:“降息”了!1年期下调10个基点,5年期为何“按兵不动”?有何影响?

1年期LPR利率年内再度下调&#xff0c;5年期以上LPR利率按兵不动。 8月21日&#xff0c;新一期借款商场报价利率&#xff08;LPR&#xff09;公布。其间&#xff0c;1年期借款商场报价利率&#xff08;LPR&#xff09;报3.45%&#xff0c;上月为3.55%&#xff1b;5年期以上LPR报…

5种做法实现table表格中的斜线表头效果(HTML+CSS+JS+Canvas+SVG)

table表格&#xff0c;这个东西大家肯定都不陌生&#xff0c;代码中我们时常都能碰到&#xff0c;那么给table加一个斜线的表头有时是很有必要的&#xff0c;但是到底该怎么实现这种效果呢&#xff1f; 我总结了以下几种方法&#xff1a; 1、最最最简单的做法 直接去找公司的…

电脑图纸怎么加密?图纸加密软件有哪些?

对于企业来说&#xff0c;图纸的重要性是不言而喻的&#xff0c;为了避免图纸泄露&#xff0c;我们需要将图纸加密保护。那么电脑图纸该怎么加密呢&#xff1f;下面我们就来看一下。 图纸加密会带来哪些好处&#xff1f; 保护企业利益 为图纸加密可以有效地保护知识产权&…

Openlayers 教程 - 以单位米为半径,绘制圆形图形要素

Openlayers 教程 - 以单位米为半径&#xff0c;绘制圆形图形要素 核心代码完整代码&#xff1a;在线示例 在以往的项目维护中&#xff0c;出现一个问题&#xff0c;使用最新高清底图发现&#xff0c;设置地图最大等级&#xff08;21级&#xff09;之后&#xff0c;地图虽然可以…

Steam搬砖项目:最长久稳定的副业!

项目应该大家都有听说话&#xff0c;但是细节问题&#xff0c;如何操作可能有些不是很清楚&#xff0c;今天在这里简单分享一下。 这个Steam搬砖项目主要赚钱汇率差和价值差&#xff0c;是一个细分领取的小项目。 不用引流&#xff0c;时间也是比较自由的&#xff0c;你可以兼…

vue 可拖拽可缩放 vue-draggable-resizable 组件常用总结

特征 没有依赖 使用可拖动&#xff0c;可调整大小或两者兼备定义用于调整大小的句柄限制大小和移动到父元素或自定义选择器将元素捕捉到自定义网格将拖动限制为垂直或水平轴保持纵横比启用触控功能使用自己的样式为句柄提供自己的样式 安装和基本用法 npm install --save vue-d…

【BASH】回顾与知识点梳理(三十七)

【BASH】回顾与知识点梳理 三十七 三十七. 基础系统设定与备份策略37.1 系统基本设定网络设定 (手动设定与 DHCP 自动取得)手动设定 IP 网络参数(nmcli)自动取得 IP 参数(dhcp)修改主机名(hostnamectl) 37.2 日期与时间设定时区的显示与设定时间的调整用 ntpdate 手动网络校时 …

Istio入门体验系列——基于Istio的灰度发布实践

导言&#xff1a;灰度发布是指在项目迭代的过程中用平滑过渡的方式进行发布。灰度发布可以保证整体系统的稳定性&#xff0c;在初始发布的时候就可以发现、调整问题&#xff0c;以保证其影响度。作为Istio体验系列的第一站&#xff0c;本文基于Istio的流量治理机制&#xff0c;…

MyBatis的基本入门及Idea搭建MyBatis坏境且如何实现简单的增删改查(CRUD)---详细介绍

一&#xff0c;MaBatis是什么&#xff1f; 首先是一个开源的Java持久化框架&#xff0c;它可以帮助开发人员简化数据库访问的过程并提供了一种将SQL语句与Java代码进行解耦的方式&#xff0c;使得开发人员可以更加灵活地进行数据库操作。 1.1 Mabatis 受欢迎的点 MyBatis不仅是…

玄而又玄——我亲历的三大总线

总线是计算机系统中的桥梁和公路。对于要学习计算机系统的人来说&#xff0c;如果不理解总线&#xff0c;那么很多认知就没办法落到实处&#xff0c;想不清两样东西是如何连接起来&#xff0c;数据是如何从一点到另一点的。 最近两三年&#xff0c;做了比较多的底层开发&#x…

Scratch 之 创作小技巧 -- 让触碰效果更丝滑

今天小技巧的主题是——丝滑 a.让触碰效果更丝滑 ——非线性放大 相信大家&#xff0c;做游戏时都会有一开始按键吧&#xff0c;把鼠标放上去&#xff0c;这个按键就会有相应的变化&#xff0c;如放大&#xff0c;作为初学者&#xff0c;这段的代码可能是这样↓ 虽然看起来挺…

解析大规模开发:提升企业级开发效率与质量,加速创新

在数字化转型的大环境下&#xff0c;越来越多的企业依赖软件来驱动业务和创新。然而&#xff0c;随着开发规模日益庞大&#xff0c;如何更好地提升研发效能&#xff0c;从而塑造更强大的竞争力&#xff0c;已然成为众多企业亟待解决的共同难题。 作为国内领先的DevSecOps提供商…

凉而不冷 柔而不弱 三菱重工海尔舒适风科技助您整夜安眠

古人云&#xff1a;安寝乃人生乐事。可随着夏天的到来&#xff0c;昼长夜短&#xff0c;家里的老人、儿童、父母都存在不同的入睡苦恼。对于儿童来说&#xff0c;空调温度调的太低容易踢被子着凉&#xff0c;温度调的高又怕孩子满头大汗&#xff1b;父母自身也会因为半夜帮孩子…

盛元广通高校实验室开放预约与综合管理系统LIMS

系统概述&#xff1a; 高校实验室涉及到的课程、老师、学生多&#xff0c;管理起来费时费力&#xff0c;盛元广通高校实验室开放预约与综合管理系统LIMS提供简单易用的账号管理、实验室管理、课程管理、实验项目管理、实验时间设定&#xff1b;为学生提供简单易用的自主实验选…

使用 NBAR(基于网络的应用程序识别) 进行应用流量分析

识别和分类网络应用程序是有效管理网络带宽的关键。通过对网络流量进行分类&#xff0c;管理员可以根据企业的需要可视化、组织和确定网络流量的优先级。通过识别和分类网络流量&#xff0c;网络管理员可以有效地应用 QoS 策略&#xff0c;从而实现优化的网络带宽性能。 什么是…

docker 安装oracle19c linux命令执行sql

docker安装oracle # 下载镜像 19.3.0.0.0 docker pull registry.cn-hangzhou.aliyuncs.com/laowu/oracle:19c # 创建文件 mkdir -p /home/mymount/oracle19c/oradata # 授权&#xff0c;不授权会导致后面安装失败 chmod 777 /home/mymount/oracle19c/oradatadocker run -d \ …

0基础学习VR全景平台篇 第88篇:智慧眼-成员管理

一、功能说明 成员管理&#xff0c;是指管理智慧眼项目的成员&#xff0c;拥有相关权限的人可以进行添加成员、分配成员角色、设置成员分类、修改成员以及删除成员五项操作。但是仅限于管理自己的下级成员&#xff0c;上级成员无权管理。 二、前台操作页面 登录智慧眼后台操…

JS中如何区分变量是数组还是对象

总结&#xff1a; 这里提供三种方法&#xff1a; var arr[] var arr2{}1、constructor:数组的constructor是function Array(){};对象的constructor是function Object(){}2、instanceof&#xff1a;数组 instanceof Array&#xff1a;为true;对象 instanceof Array: 为false;3、…