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

news2024/12/25 23:47:42

目录

一、项目简介

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/891369.html

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

相关文章

.net core发布到IIS上出现 HTTP 错误 500.19

1.检查.net core 环境运行环境是否安装完成&#xff0c;类似如下环境 2.IIS是否安装全 本次原因就是IIS未安装全导致的 按照网上说的手动重启iis&#xff08;iisreset&#xff09;也不行

无涯教程-Perl - telldir函数

描述 此函数返回DIRHANDLE引用的目录列表中读指针的当前位置。此返回值可以由seekdir()函数使用。 语法 以下是此函数的简单语法- telldir DIRHANDLE返回值 此函数返回目录中的当前位置。 例 以下是显示其基本用法的示例代码,/tmp目录中只有两个文件- #!/usr/bin/perl …

Vue用JSEncrypt对长文本json加密以及发现解密失败

哈喽 大家好啊&#xff0c;最近发现进行加密后 超长文本后端解密失败&#xff0c;经过看其他博主修改 JSEncrypt原生代码如下&#xff1a; // 分段加密&#xff0c;支持中文JSEncrypt.prototype.encryptUnicodeLong function (string) {var k this.getKey();//根据key所能编…

由小波变换模极大值重建信号

给定信号&#xff0c; 令小波变换的尺度 则x(t)的二进小波变换为 令为取模极大值时的横坐标&#xff0c;那么就是模极大值。 目标是由坐标、模极大值及最后一级的低频分量重建信号x(t) 为了重建x(t)&#xff0c;假定有一信号集合h(t)&#xff0c;该集合中信号的小波变换和x(…

JavaScript请求数据的4种方法总结(Ajax、fetch、jQuery、axios)

JavaScript请求数据有4种主流方式&#xff0c;分别是Ajax、fetch、jQuery和axios。 一、Ajax、fetch、jQuery和axios的详细解释&#xff1a; 1、 Ajax Ajax&#xff08;Asynchronous JavaScript and XML&#xff09;是一种使用JavaScript在用户的浏览器上发送请求的技术&…

分类预测 | MATLAB实现GAPSO-LSSVM多输入分类预测

分类预测 | MATLAB实现GAPSO-LSSVM多输入分类预测 目录 分类预测 | MATLAB实现GAPSO-LSSVM多输入分类预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.分类预测 | MATLAB实现GAPSO-LSSVM多输入分类预测 2.代码说明&#xff1a;要求于Matlab 2021版及以上版本。 程序…

kernel的config加上 CONFIG_SND_ALOOP=y ,aplay不能播放声音

概念&#xff1a;CONFIG_SND_ALOOP CONFIG_SND_ALOOP 是 Linux 内核配置选项之一&#xff0c;用于启用 ALSA Loopback 驱动程序。 ALSA&#xff08;Advanced Linux Sound Architecture&#xff09;是 Linux 上的音频架构&#xff0c;提供了一个统一的音频接口&#xff0c;使应…

雷军写的代码上热搜了

“雷军写的代码”一词突然上了微博热搜&#xff1a; 一瞬间&#xff0c;我想起了这张图&#xff1a; 到底发生了什么&#xff0c;好奇的我点进去一看&#xff0c;原来是因为雷军预告年度演讲的微博里配了一张海报&#xff1a; 这张海报信息量非常大&#xff0c;一眼就能看到有很…

如何使用Vue和C++实现OJ《从零开始打造 Online Judge》

课程简介 课程链接&#xff1a;https://www.lanqiao.cn/courses/20638 邀请码&#xff1a;x8pGd60V 本课程采用前后端分离架构&#xff0c;基于 Vue.js 和 C 技术&#xff0c;从零开始打造 Online Judge。 课程介绍 OJ 是 Online Judge 系统的简称&#xff0c;用来在线检测…

算法通关村第3关【白银】| 双指针思想

1. 双指针思想 双指针不仅指两个指针&#xff0c;也可以是两个变量&#xff0c;指向两个值。 有三种类型&#xff1a; 快慢型&#xff1a;一前一后对撞型&#xff1a;从两端向中间靠拢背向型&#xff1a;从中间向两端分开 2. 删除元素专题 2.1原地移除元素 (1)快慢指针 思…

我的创作纪念日(128天)

机缘 CSDN账号创建已有3年了&#xff0c;本篇是第一篇纪念文。。。有点偷懒的感觉了。。。 从第一篇文章的发布&#xff0c;到现在已经过了128天了&#xff0c;回想起当时发布文章的原因&#xff0c;仅仅只是因为找不到合适的云笔记&#xff0c;鬼使神差的想到了CSDN&#xff…

第十一课:Qt 快捷键大全

功能描述&#xff1a;Qt 中的快捷键查看方式和自定义快捷键 一、快捷键查看/自定义 Qt Creator 中提供了各种快捷键&#xff0c;如需查看或自定义快捷键&#xff0c;选择菜单栏“工具” -> “选项” -> “环境” -> “键盘”。 快捷键按类别列出&#xff0c;可以在过…

Windows 11 + Ubuntu20.04 双系统 坑里爬起来

ThinkPad x390 安装双系统&#xff0c;原有的磁盘太小&#xff0c;扩充了磁盘重新装系统&#xff0c;出现的问题&#xff0c;加以记录。 1. windows和ubuntu谁先安装&#xff0c;两个都可以&#xff0c;一般建议先安装windows&#xff0c;后安装ubuntu 2. 安装windows后&…

小O网兜0231新版 -- 用户入门指南

本文介绍小O网兜入门功能&#xff0c;通过本文用户能够掌握数据采集的基本操作&#xff0c;使用软件提供的模板任务采集指定页面的数据。 基本概念 任务文件&#xff1a;新建任务文件&#xff0c;扩展名为 xop&#xff0c;任务的配置、采集数据等信息保存在该文件中&#xff…

Android进阶之路 - 去除EditText内边距

正如题名&#xff0c;在Android中的EditText是自带内边距的&#xff0c;常规而言设置背景为null即可&#xff0c;但是因为使用了并不熟悉的声明式框架&#xff0c;本是几分钟解决的事儿&#xff0c;却花费了小半天~ 其实这只是一个很简单的小需求&#xff0c;不想却遇到了一些小…

WIN+ALT+R无法开始录制

winr打开注册表regedit 依次展开 计算机\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\GameDVR 修改AppCaptureEnabled数值为1 wing打开 Xbox Game Bar点击捕获 WINALTR开始录制

Zabbix技术分享——Proxy加密代理:共享密钥(PSK)加密与证书加密

一、加密介绍 Zabbix版本从3.0之后&#xff0c;开始支持Zabbix server, Zabbix proxy, Zabbix agent, zabbix_sender and zabbix_get之间的通信加密&#xff0c;加密方式有预共享密钥(PSK)和证书加密&#xff0c;加密配置是可选项&#xff0c;一些proxy和agent可以使用证书认证…

PHP-MD5注入

0x00 前言 有些零散的知识未曾关注过&#xff0c;偶然捡起反而更加欢喜。 0x01 md5 注入绕过 md5函数有两个参数&#xff0c;第一个参数是要进行md5的值&#xff0c;第二个值默认为false&#xff0c;如果为true则返回16位原始二进制格式的字符串。意思就是会将md5后的结果当…

网络

mcq Java 传输层&#xff1a;拆分和组装&#xff0c;完成端到端的消息传递&#xff0c;流量控制&#xff0c;差错控制等 网络层&#xff1a; 寻址、路由&#xff0c;复用&#xff0c;拥塞控制&#xff0c;完成源到宿的传递。 显然A选项是错误的&#xff0c;有流量控制的是传输层…

谷歌推出首款量子弹性 FIDO2 安全密钥

谷歌在本周二宣布推出首个量子弹性 FIDO2 安全密钥&#xff0c;作为其 OpenSK 安全密钥计划的一部分。 Elie Bursztein和Fabian Kaczmarczyck表示&#xff1a;这一开源硬件优化的实现采用了一种新颖的ECC/Dilithium混合签名模式&#xff0c;它结合了ECC抵御标准攻击的安全性和…