0


基于 Spring 前后端分离版本的论坛系统

访问地址:http://8.130.142.126:18080/sign-in.html

代码获取:基于 Spring 前后端分离版本的论坛系统: 基于 Spring 前后端分离版本的论坛系统

一.前置知识

1.软件生命周期

  • a. 可行性研究:通过分析软件开发要求,确定软件项目的性质、目标和规模,得出可行性研究报告,如果可行性研究报告是可行的,就要制订详细的项目开发计划。此阶段交付成果为可行性研究报告;

  • b. 需求分析:是软件生命周期中重要的也是决定性的一步,通过需求分析才能把软件功能和性能的总体概念描述为具体的软件需求规格说明书,为软件开发奠定基础,此阶段交付成果为软件需求规格说明书;

  • c. 概要设计:根据软件需求规格说明建立软件系统的总体结构和模块间的关系,定义各功能模块接口,设计全局数据库或数据结构,规定设计约束,制定组装(集成)测试计划;

  • d. 详细设计:将各模块要实现的功能用相应的设计工具详细的描述出来。

  • e. 实现:写出正确的、易理解的和易维护的程序模块。程序员根据详细设计文档将详细设计转化为程序,完成单元测试;

  • f. 组装测试(集成测试):将经过单元测试的模块逐步进行组装和测试;

  • g. 确认测试:测试系统是否达到了系统要求,按照规格说明书的规定,由用户(或在用户积极参与下)对系统进行验收。必要时,还可以再通过现场测试或并行运行等⽅法对系统进行进一步测试;

  • h. 使用:将软件安装在用户确定的运行环境中,测试通过后移交用户使用。在软件使用过程中,客户和维护人员必须认真收集发现的软件错误,定期或阶段性的撰写软件问题报告和软件修改报告;

  • i. 维护:通过各种必要的维护活动使系统持久的满足用户需要;

  • j. 退役:终止对软件产品的支持,软件停止使用

2.面向对象

3.架构

**C/S架构:**即客户端 / 服务器架构模式

**B/S架构:**即浏览器 / 服务器架构模式

4.相关技术和工具

服务器端技术

  • Spring
  • Spring Boot
  • Spring MVC
  • MyBatis

**浏览器端技术 **

  • HTML, CSS, JavaScript
  • jQuery
  • Bootstrap

**数据库 **

  • MySQL

**项目构建工具 **

  • Maven

**版本控制工具 **

  • Git + GITEE

二.数据库的设计

1.创建数据库

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

2.创建表

-- 选择数据库
use forum_db;

set names utf8mb4;
set foreign_key_checks = 0;

1.用户表

-- ----------------------------
-- 创建用户表 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;

2.文章表

-- ----------------------------
-- 创建帖子表 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;

3.文章回复表

-- ----------------------------
-- 创建帖子回复表 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;

4.版块表

-- ----------------------------
-- 创建版块表 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;

5.站内信表

-- ----------------------------
-- 创建站内信表 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;

三.环境的搭建

1.创建Spring项目

项目创建完成如下:

2.开启热部署

<!--        热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

3.pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.javastudy</groupId>
    <artifactId>forum</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>forum</name>
    <description>forum</description>
    <properties>
        <java.version>1.8</java.version>
        <mybatis-starter.version>2.3.0</mybatis-starter.version>
        <druid-starter.version>1.2.16</druid-starter.version>
        <mysql-connector.version>5.1.49</mysql-connector.version>
    </properties>
    <dependencies>
        <!--        spring web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--        mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-starter.version}</version>
        </dependency>
        <!--        热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!--        lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--        spring boot test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--   mysql连接-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector.version}</version>
            <scope>runtime</scope>
        </dependency>
        <!-- 阿里巴巴druid数据源,如果使用SpringBoot默认的数据源,删除或注释这个依赖即可 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-starter.version}</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

4.application.yml文件

#配置数据源
spring:
  application:
    name: "forum" # 项目名
  output:
    ansi:
      enabled: always # 控制台输出彩色日志
  datasource:
    url: jdbc:mysql://127.0.0.1:13306/forum_db?characterEncoding=utf8&useSSL=false    # 数据库连接串
    username: root # 数据库用户名字\
    password: woaini520 # 数据库密码
    driver-class-name: com.mysql.jdbc.Driver # 数据库连接驱动
# 服务器配置
server:
  port: 8082 # 指定端口号

# mybatis 相关配置,单独配置,顶格写
mybatis:
  mapper-locations: classpath:mapper/**/*.xml # 指定 xxxMapper.xml的扫描路径
  configuration: # 配置打印 MyBatis 执行的 SQL
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志信息
logging:
  pattern:
    dateformat: yyyy-MM-dd HH:mm:ss # 日期格式
  file:
    path: logs/
  level:
    root: info

四.工程搭建

1.项目结构

2.生成类的映射文件

版本统一管理

        <mybatis-generator-plugin-version>1.4.1</mybatis-generator-plugin-version>

在 build --> plugins 标签中加入如下配置

    <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>
        </plugins>
    </build>

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:\java cave\Maven\repository\mysql\mysql-connector-java\5.1.49\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:13306/forum_db?characterEncoding=utf8&amp;useSSL=false"
                        userId="root"
                        password="woaini520">
        </jdbcConnection>

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

        <!-- 实体类生成位置 -->
        <javaModelGenerator targetPackage="com.javastudy.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.javastudy.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>
</generatorConfiguration>

下面就是创建成功的样子

之后不要忘记给所有的Mapper加上@Mapper注解

3. 配置扫描配置

@Configuration
//指定mybatis的扫描路径
@MapperScan("com.javastudy.forum.dao")
public class MybatisConfig {
}

4.测试

**插入一条数据 **

    INSERT INTO `forum_db`.`t_user` (`id`, `username`, `password`, `nickname`,
        `gender`, `salt`, `avatarurl`, `articlecount`, `isadmin`, `state`,
        `deletestate`, `createtime`, `updatetime`) VALUES (1, 'joyboy', '123456', '路飞', 
        2, '123', 'avatar.png',
         0, 1, 0, 0, '2022-12-13 22:30:10', '2022-12-13 22:30:13');
@SpringBootTest
@Slf4j
class UserMapperTest {
    @Resource
    UserMapper userMapper;

    @Resource
    ObjectMapper objectMapper;

    @Test
    void insert() {
    }

    @Test
    void insertSelective() {

    }

    @Test
    void selectByPrimaryKey() throws JsonProcessingException {
        User user = userMapper.selectByPrimaryKey(1L);
        log.info(objectMapper.writeValueAsString(user));

    }

    @Test
    void updateByPrimaryKeySelective() {
    }

    @Test
    void updateByPrimaryKey() {
    }
}

五.准备公共代码编写

1.编写公共代码

1.定义状态码

定义的状态码如下:

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, "两次输入的密码不一致"),
    ERROR_SERVICES(2000, "服务器内部错误"),
    ERROR_IS_NULL(2001, "IS NULL.");

    long code;
    String message;

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

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

    public long getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

2.定义返回结果

public class AppResult<T> {
    private Long code;
    private String message;
    private T data;

    public AppResult() {
    }

    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.getMessage());
    }

    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.getMessage(), 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;
    }

}

3.自定义异常

public class ApplicationException extends RuntimeException {
    private AppResult errorResult;

    public ApplicationException(AppResult errorResult) {
        // 加入if 报错 ,待查
        super(errorResult.getMessage());
        this.errorResult = errorResult;
    }

    public ApplicationException(String message) {
        super(message);
    }

    public ApplicationException(String message, Throwable cause) {
        super(message, cause);
    }

    public ApplicationException(Throwable cause) {
        super(cause);
    }

    public AppResult getErrorResult() {
        return errorResult;
    }
}

4.全局异常处理

@Slf4j // 日志
@ControllerAdvice
public class GlobalExceptionHandler {

    // 以JSON的形式返回BOYD中的数据
    @ResponseBody
    // 指定要处理的异常
    @ExceptionHandler(ApplicationException.class)
    public AppResult handleApplicationException(ApplicationException e) {
        // 打印异常信息, 上线生产之前要删除这个打印方式
        e.printStackTrace();
        // 打印日志
        log.error(e.getMessage());
        // 判断自定义的异常信息是否为空
        if (e.getErrorResult() != null) {
            // 返回异常类中记录的状态
            return e.getErrorResult();
        }
        // 根据异常信息,封装AppResult
        return AppResult.failed(e.getMessage());
    }

    @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.getMessage());
        }
        // 根据异常信息,封装AppResult
        return AppResult.failed(e.getMessage());
    }
}

2.Swagger自动生成

1.引入版本号

<springfox-boot-starter.version>3.0.0</springfox-boot-starter.version>

2.引入相关依赖

        <!-- API⽂档⽣成,基于swagger2 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${springfox-boot-starter.version}</version>
        </dependency>
        <!-- SpringBoot健康监控 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

3.编写配置类

package com.javastudy.forum.config;

import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * @author Chooker
 * @create 2023-08-09 12:45
 */
// 配置类
@Configuration
// 开启Springfox-Swagger
@EnableOpenApi
public class SwaggerConfig {
    /**
     * Springfox-Swagger基本配置
     *
     * @return
     */
    @Bean
    public Docket createApi() {
        Docket docket = new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.javastudy.forum.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;

    }

    // 配置API基本信息
    private ApiInfo apiInfo() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("论坛系统API")
                .description("论坛系统前后端分离API测试")
                .contact(new Contact("Chooker", "https://blog.csdn.net/qq_64580912", "[email protected]"))
                .version("1.0")
                .build();
        return apiInfo;
    }

    /**
     * 解决SpringBoot 6.0以上与Swagger 3.0.0 不兼容的问题
     * 复制即可
     **/
    @Bean
    public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
                                                                         ServletEndpointsSupplier servletEndpointsSupplier,
                                                                         ControllerEndpointsSupplier controllerEndpointsSupplier,
                                                                         EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
                                                                         WebEndpointProperties webEndpointProperties, Environment environment) {
        List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
        Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
        allEndpoints.addAll(webEndpoints);
        allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
        allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
        String basePath = webEndpointProperties.getBasePath();
        EndpointMapping endpointMapping = new EndpointMapping(basePath);
        boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment,
                basePath);
        return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
                corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
                shouldRegisterLinksMapping, null);
    }

    private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,
                                               String basePath) {
        return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
                || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
    }
}

4.application.yml 中添加配置

spring:  
  mvc:
    pathmatch:
      matching-strategy: ANT_PATH_MATCHER #Springfox-Swagger兼容性配置

5.具体API

API常用注解

  • @Api: 作用在Controller上,对控制器类的说明
  •  tags="说明该类的作用,可以在前台界面上看到的注解
    
  • @ApiModel: 作用在响应的类上,对返回响应数据的说明
  • @ApiModelProerty:作用在类的属性上,对属性的说明
  • @ApiOperation: 作用在具体方法上,对API接口的说明
  • @ApiParam: 作用在方法中的每⼀个参数上,对参数的属性进行说明

访问地址:http://127.0.0.1:18080/swagger-ui/index.html 出现以下页面

导入postman

这样就可以在postman中进行测试

六.注册功能的实现

1.在Mapper.xml中编写SQL语句

1.写入操作(注册部分的代码)

已经自动生成这部分的代码,在UserMapper.xml文件中

  <insert id="insertSelective" parameterType="com.javastudy.forum.model.User">
    insert into t_user
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="id != null">
        id,
      </if>
      <if test="username != null">
        username,
      </if>
      <if test="password != null">
        password,
      </if>
      <if test="nickname != null">
        nickname,
      </if>
      <if test="phonenum != null">
        phonenum,
      </if>
      <if test="email != null">
        email,
      </if>
      <if test="gender != null">
        gender,
      </if>
      <if test="salt != null">
        salt,
      </if>
      <if test="avatarurl != null">
        avatarurl,
      </if>
      <if test="articlecount != null">
        articlecount,
      </if>
      <if test="isadmin != null">
        isadmin,
      </if>
      <if test="remark != null">
        remark,
      </if>
      <if test="state != null">
        state,
      </if>
      <if test="deletestate != null">
        deletestate,
      </if>
      <if test="createtime != null">
        createtime,
      </if>
      <if test="updatetime != null">
        updatetime,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="id != null">
        #{id,jdbcType=BIGINT},
      </if>
      <if test="username != null">
        #{username,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        #{password,jdbcType=VARCHAR},
      </if>
      <if test="nickname != null">
        #{nickname,jdbcType=VARCHAR},
      </if>
      <if test="phonenum != null">
        #{phonenum,jdbcType=VARCHAR},
      </if>
      <if test="email != null">
        #{email,jdbcType=VARCHAR},
      </if>
      <if test="gender != null">
        #{gender,jdbcType=TINYINT},
      </if>
      <if test="salt != null">
        #{salt,jdbcType=VARCHAR},
      </if>
      <if test="avatarurl != null">
        #{avatarurl,jdbcType=VARCHAR},
      </if>
      <if test="articlecount != null">
        #{articlecount,jdbcType=INTEGER},
      </if>
      <if test="isadmin != null">
        #{isadmin,jdbcType=TINYINT},
      </if>
      <if test="remark != null">
        #{remark,jdbcType=VARCHAR},
      </if>
      <if test="state != null">
        #{state,jdbcType=TINYINT},
      </if>
      <if test="deletestate != null">
        #{deletestate,jdbcType=TINYINT},
      </if>
      <if test="createtime != null">
        #{createtime,jdbcType=TIMESTAMP},
      </if>
      <if test="updatetime != null">
        #{updatetime,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>

2.根据用户名查询用户信息

创建如图的目录结构,并在extension目录下创建UserExtMapper.xml文件,里面编写自动生成以外的代码.

可以看出 UserExtMapper.xml与UserMapper.xml是共用resultMap和Base_Column_List的

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javastudy.forum.dao.UserMapper">

    <!--
    1. 注意namespace表示命名空间,要与 UserMapper.xml中的namespace相同
    2. 统一用com.javastudy.forum.dao.UserMapper, 也就是UserMapper的完全限定名(包名+类名)
    3. 不同的映射文件指定了相同的namespace后,定义的所有用id或name标识的结果集映射都可以在不同的文件中共享
-->

<!--    根据用户名查询用户信息-->
    <select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
            from t_user where username=#{username,jdbcType=VARCHAR};
    </select>

</mapper>

2.在Mapper.java中定义方法

@Mapper
public interface UserMapper {
    int insert(User row);

    int insertSelective(User row);

    User selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(User row);

    int updateByPrimaryKey(User row);

    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    User selectByUsername(String username);
}

3.定义Service接口

public interface IUserService {
    User selectByUsername(String username);

    int createNormalUser(User user);
}

4.实现Serivce接口

@Slf4j //日志
@Service
public class UserServiceImpl implements IUserService {
    @Resource
    UserMapper userMapper;

    @Override
    public User selectByUsername(String username) {
        //非空校验
        if (StringUtils.isEmpty(username)) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));

        }
        //根据用户名查询用户信息
        User user = userMapper.selectByUsername(username);

        return user;
    }

    @Override
    public int 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 exitsUser = userMapper.selectByUsername(user.getUsername());
        if (exitsUser != null) {
            //打印日志
            log.warn(ResultCode.FAILED_USER_EXISTS.toString());
            //抛出异常
            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));
        }

        return row;
    }
}

5.单元测试

加入SpringBootTest注解

1.测试selectByUsername接口

@SpringBootTest
@Slf4j
class UserServiceImplTest {
    @Resource
    private IUserService userService;

    @Resource
    private ObjectMapper objectMapper;

    @Test
    void selectByUsername() throws JsonProcessingException {
        User user = userService.selectByUsername("joyboy");
        log.info(objectMapper.writeValueAsString(user));

        log.info("============================================");
        User joyboy222 = userService.selectByUsername("");
        log.info(objectMapper.writeValueAsString(joyboy222));

    }

    @Test
    void createNormalUser() {

    }
}

符合预期,校验完成

2.测试createNormalUser接口

    @Test
    void createNormalUser() {
        User user = new User();
        user.setUsername("testuser");
        user.setPassword("123456");
        user.setNickname("测试用户");
        user.setSalt("123456");

        userService.createNormalUser(user);
        log.info("注册成功");

        log.info("=================================");
        user.setUsername("joyboy");
        userService.createNormalUser(user);
        log.info("注册成功");

    }

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

@RestController
@Slf4j
@RequestMapping("/user")
@Api(tags = "用户接口")
public class UserController {
    @Resource
    IUserService userService;

    @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);
        //对密码进行处理
        String salt = UUIDUtils.UUID_32();
        password = MD5Utils.md5Salt(password, salt);
        user.setPassword(password);
        user.setSalt(salt);
        userService.createNormalUser(user);
        return AppResult.success("注册成功");

    }

}

7.测试API接口

打开swagger进行测试

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

可以在以下地址下载: forum_static.zip

      // 构造数据
      var postData = {
        username: $("#username").val(),
        nickname: $("#nickname").val(),
        password: $("#password").val(),
        passwordRepeat: $("#passwordRepeat").val(),
      }

      // 发送AJAX请求 
      // contentType = application/x-www-form-urlencoded
      // 成功后跳转到 sign-in.html
      $.ajax({
        type: 'post',
        url: '/user/register',
        //数据类型
        contentType: "application/x-www-form-urlencoded",
        //要提交的数据
        data: postData,
        //成功的回调函数
        success: function (respData) {
          if (respData.code == 0) {
            location.assign("sign-in.html");
          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        //http请求的失败
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });
    });

七.登录功能实现

1.在Mapper.xml中编写SQL语句

之前已经定义过了selectByName

2.在Mapper.java中定义方法

之前已经定义过了selectByName

3.定义Service接口

User login(String username,String password);

4.实现Serivce接口

    @Override
    public User login(String username, String password) {
        User user = selectByUsername(username);
        if (user == null) {
            //打印日志
            log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());
            //抛出异常 ---防止恶意猜测
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
        }

        //密码校验
        boolean flag = MD5Utils.verifyOriginalAndCiphertext(password, user.getSalt(), user.getPassword());
        if (!flag) {
            //打印日志
            log.warn("密码输入错误: username: "+username+ "  password: " + password);
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));

        }
        return user;
    }

5.单元测试

    @Test
    void login() {
        User user = userService.login("joyboy", "123456");
        log.info(user.toString());
        User user2 = userService.login("user", "123456");
        log.info(user2.toString());

    }

符合预期

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

    @ApiOperation("用户登录")
    @PostMapping("/login")
    public AppResult register(HttpServletRequest httpServletRequest,
                              @ApiParam("用户名") @RequestParam("username") @NonNull String username,
                              @ApiParam("密码") @RequestParam("password") @NonNull String password) {
        User user = userService.login(username, password);
        //1.获取session对象
        HttpSession session = httpServletRequest.getSession(true);
        //2.用户信息保存到session中
        session.setAttribute(AppConfig.SESSION_USER_KEY, user);

        return AppResult.success("登陆成功");

    }

7.测试API接口

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

      // 构造数据
      var postData = {
        username: $("#username").val(),
        password: $("#password").val(),
      }

      // 发送AJAX请求,成功后跳转到index.html
      $.ajax({
        type: "post",
        url: "/user/login",
        //数据类型
        contentType: "application/x-www-form-urlencoded",
        data: postData,
        success: function (respData) {
          if (respData.code == 0) {
            location.assign("/index.html");
          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

八.退出功能实现

退出功能直接在session中删除用户信息即可,因此不需要前面的五步

1.Controller实现方法外提供API接口

    @ApiOperation("用户注销")
    @GetMapping("/logout")
    public AppResult logout(HttpServletRequest httpServletRequest) {
        //获取Session对象
        HttpSession session = httpServletRequest.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        //返回结果
        return AppResult.success("注销成功");
        
    }

2.测试API接口

需要先登录,才能退出

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

   // 成功后,跳转到sign-in.html
    $('#index_user_logout').click(function () {
      $.ajax({
        type: "get",
        url: "/user/logout",
        success: function (respData) {
          if (respData.code == 0) {
            location.assign("/sign-in.html");
          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });
    });

九.获取用户信息实现

1.Controller实现方法外提供API接口

    @ApiOperation("获取用户信息")
    @GetMapping("/info")
    public AppResult<User> getInfo(HttpServletRequest httpServletRequest) {
        //获取Session对象
        HttpSession session = httpServletRequest.getSession(false);
        if (session == null || session.getAttribute(AppConfig.SESSION_USER_KEY) == null) {
            return AppResult.failed("用户未登录");
        }
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        return AppResult.success(user);
    }

2.测试API接口

此时我们进行测试可以得到以上的数据,但是此时还是有一定的弊端的

  • 密码和盐不能在网络上进行传输,会有安全隐患
  • 有些为空的数据没有必要传输
  • 日期信息的传输格式不是常规的

接下来一一来解决以上的问题.

1. 解决密码和盐传输的问题

在实体类属性上面加入@JsonIgnore注解

    @ApiModelProperty("密码")
    @JsonIgnore
    private String password;
    
    @ApiModelProperty("性别")
    private Byte gender;
    
    @ApiModelProperty("盐")
    @JsonIgnore
    private String salt;

    
    @ApiModelProperty("删除状态")
    @JsonIgnore
    private Byte deletestate;

2.解决空数据传输的问题和解决日期格式的问题

  # JSON序列化配置
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss # 日期格式
    default-property-inclusion: NON_NULL # 不为null时序列化

此时符合条件

登录的时候,发现data信息不见了,因为data信息为null,所以没有进行序列化,但是实际上code,message,data信息就算是null也是要进行传输的.

加入以下注解可以解决这个问题.

    @ApiModelProperty("状态码")
    @JsonInclude(JsonInclude.Include.ALWAYS) // 任何情况下都参与JSON序列化
    private Long code;
    @ApiModelProperty("错误信息")
    @JsonInclude(JsonInclude.Include.ALWAYS) // 任何情况下都参与JSON序列化
    private String message;
    @ApiModelProperty("返回的数据")
    @JsonInclude(JsonInclude.Include.ALWAYS) // 任何情况下都参与JSON序列化
    private T data;

头像同样也是需要的

    @ApiModelProperty("头像地址")
    @JsonInclude(JsonInclude.Include.ALWAYS)
    private String avatarurl;

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

    //========================= 获取用户信息 =======================
    // 成功后,手动设置用户信息
    // $('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
    $.ajax({
      type: "get",
      url: "/user/info",
      success: function (respData) {
        if (respData.code == 0) {
          //成功
          var user = respData.data;
          if (!user.avatarurl) {//设默认的头像
            user.avatarurl = 'image/avatar01.jpeg';
          }
          $('#index_nav_nickname').html(user.nickname);
          $('#index_nav_avatar').css('background-image', 'url(' + user.avatarurl + ')');
          let subName = user.isAdmin == 1 ? '管理员' : '普通用户';
          $('#index_nav_name_sub').html(subName);

        } else {
          // 失败(服务器处理业务逻辑失败), 提示用户错误信息
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }

      },
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

十.拦截器的实现

论坛中的大部分接口都需要在登录的情况下进行访问,因此我们需要一个拦截器对访问页面的时候对未登录进行校验

1.LoginInterceptor

未登录跳转的页面我们在application.yml中定义,这样为了以后的维护更加方便.

forum:
  login:
    url: sign-in.html
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Value("${forum.login.url}")
    private String defaultUrl;

    @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;
        }
        //采用重定向
        response.sendRedirect(defaultUrl);
        return true;
    }
}

2.AppInterceptorConfigurer

@Configuration // 把当前配置类加入到Spring中
public class AppInterceptorConfigurer 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下所有,与swagger相关
                .excludePathPatterns("/dist/**")        // 排除所有静态文件
                .excludePathPatterns("/image/**")
                .excludePathPatterns("/**.ico")
                .excludePathPatterns("/js/**");
    }
}

十一.获取用户信息实现2

当我们进入一个帖子的时候,我们点击发帖的用户,可以看到用户的信息,此时前端给我们一个id,我们需要从数据库中根据id插叙到用户的信息.

1.在Mapper.xml中编写SQL语句

已经自动生成了

  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from t_user
    where id = #{id,jdbcType=BIGINT}
  </select>

2.在Mapper.java中定义方法

User selectByPrimaryKey(Long id);

3.定义Service接口

    /**
     * 根据id查询用户的信息
     * @param id
     * @return
     */
    User selectById(Long id);

4.实现Serivce接口

    public User selectById(Long id) {
        if(id<0||id==null){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        User user = userMapper.selectByPrimaryKey(id);
        return user;
    }

5.单元测试

    @Test
    void selectById() {
        User user = userService.selectById(1L);
        log.info(user.toString());
        User user2 = userService.selectById(2L);
        log.info(user2.toString());
    }

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

    @ApiOperation("获取用户信息")
    @GetMapping("/info")
    public AppResult<User> getInfo(HttpServletRequest httpServletRequest,
                                   @ApiParam("用户id") @RequestParam(value = "id", required = false) Long id) {
        User user;
        if (id == null) {
            //获取Session对象
            HttpSession session = httpServletRequest.getSession(false);
            user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        } else {
            user = userService.selectById(id);
        }

        return AppResult.success(user);
    }

7.测试API接口

测试成功.

十二.获取版块信息实现

先向数据库中插入一些板块的信息.

-- 写入版块信息数据
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');

1.在Mapper.xml中编写SQL语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javastudy.forum.dao.BoardMapper">
    <select id="selectByNum" parameterType="java.lang.Integer" resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List"/>
        FROM t_board
        WHERE state=0 AND deleteState=0
        ORDER BY sort ASC
        LIMIT 0,#{num,jdbcType=INTEGER};
    </select>

</mapper>

2.在Mapper.java中定义方法

    List<Board> selectByNum(@Param("num") Integer num);

3.定义Service接口

public interface IBoardService {

    List<Board> selectByNum(Integer num);
}

4.实现Serivce接口

@Service
@Slf4j
public class BoardServiceImpl implements IBoardService {

    @Resource
    private BoardMapper boardMapper;

    @Override
    public List<Board> selectByNum(Integer num) {
        if (num < 0 || num == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        List<Board> boards = boardMapper.selectByNum(num);

        return boards;
    }
}

5.单元测试

@SpringBootTest
@Slf4j
class BoardServiceImplTest {
    @Resource
    BoardServiceImpl boardService;

    @Test
    void selectByNum() {
        List<Board> boards = boardService.selectByNum(9);
        log.info(boards.toString());

    }
}

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

@RestController
@Slf4j
@RequestMapping("/board")
@Api(tags = "板块接口")
public class BoardController {
    @Value("${forum.index.board-num}")
    Integer num;

    @Resource
    private IBoardService boardService;

    @GetMapping("/topList")
    @ApiOperation("获取板块列表")
    public AppResult<List<Board>> topList() {
        List<Board> boards = boardService.selectByNum(num);
        return AppResult.success(boards);
    }

}

7.测试API接口

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

    // ========================= 获取版块信息 =======================
    // 成功后,调用buildTopBoard()方法,构建版块列表
    $.ajax({
      type: "get",
      url: "/board/topList",
      success: function (respData) {
        if (respData.code == 0) {
          buildTopBoard(respData.data);

        } else {
          // 失败(服务器处理业务逻辑失败), 提示用户错误信息
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }

      },
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

十三.获得帖子信息实现

下面是获得所有帖子信息的实现

1.在Mapper.xml中编写SQL语句

创建ArticleExtMapper.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javastudy.forum.dao.ArticleMapper">
    <resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.Article" extends="ResultMapWithBLOBs">
        <association property="user" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_"/>
    </resultMap>

    <!-- 查询所有的帖子集合-->
    <select id="selectAll" resultMap="AllInfoResultMap">
        select u.id        as u_id,
               u.nickname  as u_nickname,
               u.gender    as u_gender,
               u.avatarUrl as u_avatarUrl,
               a.id,
               a.boardId,
               a.userId,
               a.title,
               a.visitCount,
               a.replyCount,
               a.likeCount,
               a.state,
               a.deleteState,
               a.createTime,
               a.updateTime
        from t_article as a,
             t_user as u
        where a.userId = u.id
          and a.deleteState = 0
        order by a.createTime DESC
    </select>

</mapper>

2.在Mapper.java中定义方法

    List<Article> selectAll();

3.定义Service接口

public interface IArticleService {
    List<Article> selectAll();
}

4.实现Serivce接口

@Slf4j
@Service
public class ArticleService implements IArticleService {
    @Resource
    ArticleMapper articleMapper;

    @Override
    public List<Article> selectAll() {
        List<Article> articles = articleMapper.selectAll();
        return articles;
    }
}

5.单元测试

@SpringBootTest
@Slf4j
class ArticleServiceImplTest {
    @Resource
    IArticleService articleService;

    @Test
    void selectAll() {
        List<Article> articles = articleService.selectAll();
        log.info(articles.toString());

    }
}

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

@RestController
@Slf4j
@RequestMapping("/article")
@Api(tags = "帖子接口")
public class ArticleController {
    @Resource
    private IArticleService articleService;

    @GetMapping("/getAllByBoardId")
    @ApiOperation("获得所有的帖子")
    public AppResult<List<Article>> getAllByBoardId() {
        List<Article> articles = articleService.selectAll();
        if (articles == null) {
            articles = new ArrayList<>();
        }
        return AppResult.success(articles);

    }
}

7.测试API接口

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

    // ========================= 获取帖子列表 =======================
    // 成功后,调用listBuildArticleList()方法,构建帖子列表
    $.ajax({
      type: "get",
      url: "/article/getAllByBoardId",
      success: function (respData) {
        if (respData.code == 0) {
          listBuildArticleList(respData.data);

        } else {
          // 失败(服务器处理业务逻辑失败), 提示用户错误信息
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }

      },
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

十四.获得指定帖子信息实现

1.在Mapper.xml中编写SQL语句

    <!-- 查询所有的帖子集合-->
    <select id="selectByBoardId" resultMap="AllInfoResultMap">
        select
            u.id as u_id,
            u.nickname as u_nickname,
            u.gender as u_gender,
            u.avatarUrl as u_avatarUrl,
            a.id,
            a.boardId,
            a.userId,
            a.title,
            a.visitCount,
            a.replyCount,
            a.likeCount,
            a.state,
            a.deleteState,
            a.createTime,
            a.updateTime
        from t_article as a, t_user as u
        where a.userId = u.id
          and a.deleteState = 0
          and a.boardId = #{boardId,jdbcType=BIGINT}
        order by a.createTime DESC;
    </select>

2.在Mapper.java中定义方法

List<Article> selectByBoardId();

3.定义Service接口

    List<Article> selectByBoardId(Long boardId);

4.实现Serivce接口

    @Override
    public List<Article> selectByBoardId(Long boardId) {
        if(boardId==null){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        List<Article> articles = articleMapper.selectByBoardId(boardId);
        return articles;
    }

5.单元测试

    @Test
    void selectByBoardId() {
        List<Article> articles = articleService.selectByBoardId(1L);
        log.info(articles.toString());
    }

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

    @GetMapping("/getAllByBoardId")
    @ApiOperation("获得所有的帖子")
    public AppResult<List<Article>> getAllByBoardId(@ApiParam("板块id")
                                                    @RequestParam(value = "boardId", required = false) Long boardId) {
        List<Article> articles;
        if (boardId == null) {
            articles = articleService.selectAll();
        } else {
            articles = articleService.selectByBoardId(boardId);
        }
        if (articles == null) {
            articles = new ArrayList<>();
        }
        return AppResult.success(articles);

    }

7.测试API接口

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

    // ========================= 获取帖子列表 =======================
    // 成功后,调用listBuildArticleList()方法,构建帖子列表
    $.ajax({
      type: "get",
      url: "/article/getAllByBoardId"+queryString,
      success: function (respData) {
        if (respData.code == 0) {
          listBuildArticleList(respData.data);

        } else {
          // 失败(服务器处理业务逻辑失败), 提示用户错误信息
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }

      },
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

十五.根据Id获取板块信息

1.在Mapper.xml中编写SQL语句

已经自动生成

  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from t_board
    where id = #{id,jdbcType=BIGINT}
  </select>

2.在Mapper.java中定义方法

已经自动生成

    Board selectByPrimaryKey(Long id);

3.定义Service接口

    Board selectById(Long id);

4.实现Serivce接口

    @Override
    public Board selectById(Long id) {
        if (id < 0 || id == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Board board = boardMapper.selectByPrimaryKey(id);
        return board;
    }

5.单元测试

    @Test
    void selectById() {
        Board board = boardService.selectById(1L);
        log.info(board.toString());
    }

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

    @GetMapping("/getById")
    @ApiOperation("根据id获取板块信息")
    public AppResult<Board> getById(@ApiParam("板块id") @RequestParam("id") Long id) {
        Board board = boardService.selectById(id);
        return AppResult.success(board);

    }

7.测试API接口

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

    // ========================= 获取版块信息 =======================
    // 
    function getBoardInfo(boardId) {
      if (!boardId) {
        return;
      }
      // 发送请求, 成功后,显示版块相关信息
      $.ajax({
        type: "get",
        url: "/board/getById?id=" + boardId,
        success: function (respData) {
          if (respData.code == 0) {
            $('#article_list_board_title').html(respData.data.name);
            $('#article_list_count_board').html('帖子数量: ' + respData.data.articleCount);
            $('#article_list_count_board').show();

          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

    }

十六.发布新帖操作

思考一下发布帖子会经历什么样的操作.首先最直观的就是新增了一篇文章,涉及到的是文章表(article),其次需要更新用户的文章数,涉及用户表(user),最后需要更新板块的帖子数,涉及到板块表(board),并且这三个操作要么都成功,要么都失败,因此我们需要给这三个操作加上事务.

1.在Mapper.xml中编写SQL语句

更新文章数也有两种方式

第一种:重新定义一个update的语句将articleCount字段加一.

    <update id="updateArticleCount" parameterType="java.lang.Long">
        UPDATE t_user
        SET articleCount=articleCount + 1
        WHERE id = #{id,jdbcType=BIGINT};

    </update>

第二种:使用生成的updateByPrimaryKeySelective的语句,在java程序查询到对应的用户信息,并将文章数加一之后调用更新的sql.(具体下面演示,下面采用这种方式)

1.用户表更新文章数

使用生成的updateByPrimaryKeySelective

2.板块表更新文章数

同样的使用生成的updateByPrimaryKeySelective

3.文章表添加文章

使用生成的insertSelective

2.在Mapper.java中定义方法

已经定义过了

3.定义Service接口

1.用户表

    void addOneArticleCountById(Long id);

2.板块表

    void addOneArticleCountById(Long id);

3.文章表

    @Transactional
    void create (Article article);

4.实现Serivce接口

1.用户表

​
    @Override
    public void addOneArticleCountById(Long id) {
        if (id < 0 || id == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 查询现有的用户信息
        User user = selectById(id);
        // 校验用户是否为空
        if (user == null) {
            // 打印日志
            log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        user.setArticlecount(user.getArticlecount() + 1);
        user.setUpdatetime(new Date());
        int row = userMapper.updateByPrimaryKeySelective(user);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }

    }

​

2.板块表

    @Override
    public void addOneArticleCountById(Long id) {
        if (id < 0 || id == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Board board = selectById(id);
        if (board == null) {
            //打印日志
            log.warn(ResultCode.FAILED_BOARD_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_EXISTS));
        }
        board.setArticleCount(board.getArticleCount() + 1);
        board.setUpdateTime(new Date());
        int row = boardMapper.updateByPrimaryKeySelective(board);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

3.文章表

    @Override
    public void create(Article article) {
        if (article == null || article.getBoardId() == null || article.getUserId() == null ||
                StringUtils.isEmpty(article.getTitle()) || StringUtils.isEmpty(article.getContent())) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 设置默认值
        article.setVisitCount(0); // 访问数量
        article.setReplyCount(0); // 回复数量
        article.setLikeCount(0); // 点赞数量
        article.setState((byte) 0); // 状态
        article.setDeleteState((byte) 0); // 是否删除
        Date date = new Date();
        article.setCreateTime(date); // 创建时间
        article.setUpdateTime(date); // 更新时间

        int row = articleMapper.insertSelective(article);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.FAILED_CREATE.toString() + " 新增帖子失败");
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
        userService.addOneArticleCountById(article.getUserId());

        boardService.addOneArticleCountById(article.getBoardId());
        // 打印日志   ---如何获取文章的id,可以在mapper.xml进行设置
        log.info(ResultCode.SUCCESS.toString() + " 帖子发布成功, articleId = " + article.getId());

    }

为了插入文章之后获取到文章的id,我们可以进行如下的操作.

5.单元测试

1.用户表

    @Test
    void addOneArticleCountById() {
        userService.addOneArticleCountById(1L);
        log.info("更新成功");
    }

更新前

更新后

2.板块表

    @Test
    void addOneArticleCountById() {
        boardService.addOneArticleCountById(9L);
        log.info("更新成功");
    }

更新前

更新后

3.文章表

    @Test
    void create() {
        Article article = new Article();
        article.setTitle("8.18测试的文章标题");
        article.setContent("8.18测试的文章内容");
        article.setBoardId(9L);
        article.setUserId(2L);
        articleService.create(article);
        log.info("创建成功");
    }

相应的文章数目也发生了改变

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

    @ApiOperation("发布帖子")
    @PostMapping("/create")
    public AppResult create(@ApiParam("文章标题") @RequestParam("title") @NonNull String title,
                            @ApiParam("文章内容") @RequestParam("content") @NonNull String content,
                            @ApiParam("板块id") @RequestParam("boardId") @NonNull Long boardId,
                            HttpServletRequest httpServletRequest) {
        //获取用户id
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        // 校验用户状态
        if (user.getState() == 1) {
            // 用户已禁言, 返回提示
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //设置文章信息
        Article article = new Article();
        article.setUserId(user.getId());
        article.setTitle(title);
        article.setBoardId(boardId);
        article.setContent(content);

        articleService.create(article);

        return AppResult.success("新增文章成功").
                
    }

7.测试API接口

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

      // 构造帖子对象
      var postData = {
        title: titleEl.val(),
        content: contentEl.val(),
        boardId: boardIdEl.val(),
      }
      // 提交, 成功后调用changeNavActive($('#nav_board_index'));回到首页并加载帖子列表
      // contentType: 'application/x-www-form-urlencoded'
      $.ajax({
        type: "post",
        url: "/article/create",
        data: postData,
        contentType: 'application/x-www-form-urlencoded',
        success: function (respData) {
          if (respData.code == 0) {
            // 提示
            $.toast({
              heading: '成功',
              text: respData.message,
              icon: 'success'
            });
            // 成功后跳转到首页
            changeNavActive($('#nav_board_index'));
          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

十七.获取文章详情

获取文章详情的时候也需要获取相关用户的信息,板块的信息

1.在Mapper.xml中编写SQL语句

<mapper namespace="com.javastudy.forum.dao.ArticleMapper">
    <resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.Article" extends="ResultMapWithBLOBs">
        <!-- 关联User对象 -->
        <association property="user" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_"/>
        <!-- 关联Board对象-->
        <association property="board" resultMap="com.javastudy.forum.dao.BoardMapper.BaseResultMap" columnPrefix="b_"/>
    </resultMap>

    <!-- 根据帖子Id查询帖子详情 -->
    <select id="selectById" resultMap="AllInfoResultMap" parameterType="java.lang.Long">
        select u.id        as u_id,
               u.nickname  as u_nickname,
               u.gender    as u_gender,
               u.avatarUrl as u_avatarUrl,
               b.id        as b_id,
               b.name      as b_name,
               a.id,
               a.boardId,
               a.userId,
               a.title,
               a.visitCount,
               a.replyCount,
               a.likeCount,
               a.state,
               a.deleteState,
               a.createTime,
               a.updateTime,
               a.content
        from t_article as a,
             t_user as u,
             t_board b
        where a.userId = u.id
          and a.boardId = b.id
          and a.id = #{id,jdbcType=BIGINT}
          and a.deleteState = 0
    </select>

</mapper>

2.在Mapper.java中定义方法

    Article selectById(Long id);

3.定义Service接口

    Article selectById(Long id);

4.实现Serivce接口

    @Override
    public Article selectById(Long id) {
        if (id == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Article article = articleMapper.selectById(id);
        return article;
    }

5.单元测试

    @Test
    void selectById() {
        Article article = articleService.selectById(1L);
        log.info(article.toString());
    }

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

    @ApiOperation("通过文章id获取文章的详细信息")
    @GetMapping("/getById")
    public AppResult getById(@ApiParam("文章id") @RequestParam("id") @NonNull Long id) {
        Article article = articleService.selectById(id);
        if (article == null) {
            // 返回提示
            return AppResult.failed("帖子不存在");
        }
        return AppResult.success(article);

    }

7.测试API接口

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

    // ===================== 请求帖子详情 ===================== 
    $.ajax({
      type: "get",
      url: "/article/getById?id=" + currentArticle.id,
      success: function (respData) {
        if (respData.code == 0) {
          initArticleDetails(respData.data);

        } else {
          // 失败(服务器处理业务逻辑失败), 提示用户错误信息
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }

      },
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

十八.文章访问数量的增加

1.在Mapper.xml中编写SQL语句

    <update id="updateVisitCountById" parameterType="java.lang.Long">
        update t_article
        set visitCount=visitCount + 1
        where id = #{id,jdbcType=BIGINT}
    </update>

2.在Mapper.java中定义方法

    int updateVisitCountById(Long id);

3.定义Service接口

    Integer updateVisitCountById(Long id);

4.实现Serivce接口

    public Integer updateVisitCountById(Long id) {
        if (id == null || id < 0) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        int row = articleMapper.updateVisitCountById(id);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        return null;
    }

5.单元测试

    @Test
    void updateVisitCountById() {
        articleService.updateVisitCountById(1L);
        log.info("更新成功");
    }

更新前

更新后

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

更新查询操作即可

    @ApiOperation("通过文章id获取文章的详细信息")
    @GetMapping("/getById")
    public AppResult getById(@ApiParam("文章id") @RequestParam("id") @NonNull Long id) {
        Article article = articleService.selectById(id);
        if (article == null) {
            // 返回提示
            return AppResult.failed("帖子不存在");
        }
        articleService.updateVisitCountById(id);
        article.setVisitCount(article.getVisitCount() + 1);
        return AppResult.success(article);

    }

十九.编辑文章操作

在编辑文章之前,我们需要在文章中增加一个属性,来表示这篇文章是不是属于作者的,前端进行判断来选择展示修改和删除文章的按钮

    @ApiOperation("通过文章id获取文章的详细信息")
    @GetMapping("/getById")
    public AppResult getById(@ApiParam("文章id") @RequestParam("id") @NonNull Long id,
                             HttpServletRequest httpServletRequest) {
        Article article = articleService.selectById(id);
        if (article == null) {
            // 返回提示
            return AppResult.failed("帖子不存在");
        }
        articleService.updateVisitCountById(id);
        article.setVisitCount(article.getVisitCount() + 1);
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        if (user.getId() == article.getUserId()) {
            article.setOwn(true);
        }
        return AppResult.success(article);

    }

编辑文章的时候,我们需要先查询到需要修改文章的信息,然后再上一次内容的基础上进行修改,可以直接调用getById接口

article_edit.html页面

    // ========================== 获取帖子详情 ========================== 
    // 成功后,设置ID,版块名,标题,并初始编辑区同时设置正文initEditor(edit_article.content);
    $.ajax({
      type: "get",
      url: "/article/getById?id=" + currentArticle.id,
      success: function (respData) {
        var article = respData.data;
        if (respData.code == 0) {
          $("#edit_article_id").val(article.id);
          $("#edit_article_title").val(article.title);
          $('#edit_article_board_name').html(article.board.name);

          initEditor(article.content);

        } else {
          // 失败(服务器处理业务逻辑失败), 提示用户错误信息
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }

      },
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

1.在Mapper.xml中编写SQL语句

之前已经自动生成了

2.在Mapper.java中定义方法

之前已经自动生成了

3.定义Service接口

    void modify(Long id, String title, String content);

4.实现Serivce接口

    @Override
    public void modify(Long id, String title, String content) {
        if (id == null || id < 0 || StringUtils.isEmpty(title) || StringUtils.isEmpty(content)) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Article article = new Article();
        article.setTitle(title);
        article.setContent(content);
        article.setId(id);

        article.setUpdateTime(new Date());  //设置更新时间

        int row = articleMapper.updateByPrimaryKeySelective(article);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

5.单元测试

    @Test
    void modify() {
        articleService.modify(1L, "测试数据-标题1.1","测试数据-内容2.0");
        log.info("更新成功");
    }

更新前

更新后

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

    @ApiOperation("更新帖子")
    @PostMapping("/modify")
    public AppResult modify(@ApiParam("文章标题") @RequestParam("title") @NonNull String title,
                            @ApiParam("文章内容") @RequestParam("content") @NonNull String content,
                            @ApiParam("文章id") @RequestParam("id") @NonNull Long id,
                            HttpServletRequest httpServletRequest) {
        // 从session中获取当前登录的用户
        HttpSession session = httpServletRequest.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);
        // 校验帖子是否存在
        if (article == null) {
            // 返回提示信息
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        // 判断当前登录用户是不是作者
        if (article.getUserId() != user.getId()) {
            // 返回提示信息
            return AppResult.failed("您不是帖子的作者,无权修改.");
        }
        // 校验帖子状态
        if (article.getState() == 1) {
            // 返回提示信息
            return AppResult.failed("帖子状态异常, 请联系管理员");
        }
        articleService.modify(id, title, content);
        return AppResult.success("更新成功");

    }

7.测试API接口

更新前

更新后

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

      // 构造修改对象
      var postData = {
        id: articleIdEl.val(),
        title: articleTitleEl.val(),
        content: articleContentEl.val()
      }

      // 发送修改请求, 成功后跳转至首页changeNavActive($('#nav_board_index'));
      $.ajax({
        type: "post",
        url: "/article/modify",
        contentType: 'application/x-www-form-urlencoded',
        data: postData,
        // 成功回调
        success: function (respData) {
          if (respData.code == 0) {
            // 成功后提示消息
            $.toast({
              heading: '成功',
              text: respData.message,
              icon: 'success'
            });
            // 跳转到首面
            changeNavActive($('#nav_board_index'));

          } else {
            // 失败
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }
        },
        // 失败 (HTTP)
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

二十.删除文章操作

删除文章的时候需要做以下三个操作

1.需要将删除文章的deleteState置为1(article表)

2.需要将相应板块的文章数减一(board表)

3.需要将相应用户的文章数减一(user表)

1.在Mapper.xml中编写SQL语句

1.user表

直接使用生成的updateByPrimaryKeySelective

2.board表

直接使用生成的updateByPrimaryKeySelective

3.article表

直接使用生成的updateByPrimaryKeySelective

2.在Mapper.java中定义方法

1.user表

直接使用生成的updateByPrimaryKeySelective

2.board表

直接使用生成的updateByPrimaryKeySelective

3.article表

直接使用生成的updateByPrimaryKeySelective

3.定义Service接口

1.user表

void subOneArticleCountById(Long id);

2.board表

void subOneArticleCountById(Long id);

3.article表

    @Transactional
    void delete(Long id);

4.实现Serivce接口

1.user表

    @Override
    public void subOneArticleCountById(Long id) {
        if (id < 0 || id == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 查询现有的用户信息
        User user = selectById(id);
        // 校验用户是否为空
        if (user == null) {
            // 打印日志
            log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        user.setArticlecount(user.getArticlecount() - 1);
        // 判断减1之后,用户的发帖数是否小于0
        if (user.getArticlecount() < 0) {
            // 如果小于0,则设置为0
            user.setArticlecount(0);
        }
        user.setUpdatetime(new Date());
        int row = userMapper.updateByPrimaryKeySelective(user);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

2.board表

    @Override
    public void subOneArticleCountById(Long id) {
        if (id < 0 || id == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Board board = selectById(id);
        if (board == null) {
            //打印日志
            log.warn(ResultCode.FAILED_BOARD_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_EXISTS));
        }
        board.setArticleCount(board.getArticleCount() - 1);
        if (board.getArticleCount() < 0) {
            board.setArticleCount(0);
        }
        board.setUpdateTime(new Date());
        int row = boardMapper.updateByPrimaryKeySelective(board);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

3.article表

    public void delete(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.getUserId() == null || article.getBoardId() == null) {
            //打印日志
            log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }
        article.setDeleteState((byte) 1);
        article.setUpdateTime(new Date());
        int row = articleMapper.updateByPrimaryKeySelective(article);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        userService.subOneArticleCountById(article.getUserId());
        boardService.subOneArticleCountById(article.getBoardId());

        log.info("删除帖子成功, article id = " + article.getId() + ", user id = " + article.getUserId() + ".");

    }

5.单元测试

1.user表

    @Test
    void subOneArticleCountById() {
        userService.subOneArticleCountById(1L);
        log.info("更新成功");
    }

2.board表

    @Test
    void subOneArticleCountById() {
        boardService.subOneArticleCountById(9L);
        log.info("更新成功");   
    }

3.article表

    @Test
    void delete() {
        articleService.delete(6L);
        log.info("更新成功");
    }

更新前

t_article

t_board

t_user

t_article

t_board

t_user

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

    @ApiOperation("删除帖子")
    @PostMapping("/delete")
    public AppResult delete(@ApiParam("文章id") @RequestParam("id") @NonNull Long id,
                            HttpServletRequest httpServletRequest) {
        // 从session中获取当前登录的用户
        HttpSession session = httpServletRequest.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);
        // 校验帖子是否存在
        if (article == null) {
            // 返回提示信息
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        // 判断当前登录用户是不是作者
        if (article.getUserId() != user.getId()) {
            // 返回提示信息
            return AppResult.failed("您不是帖子的作者,无权修改.");
        }
        // 校验帖子状态
        if (article.getState() == 1) {
            // 返回提示信息
            return AppResult.failed("帖子状态异常, 请联系管理员");
        }
        articleService.delete(id);
        return AppResult.success("删除成功");

    }

7.测试API接口

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

    // ====================== 处理删除事件 ======================
    $('#details_artile_delete').click(function () {
      $.ajax({
        type: "post",
        url: "/article/delete?id=" + $('#details_article_id').val(),
        success: function (respData) {
          if (respData.code == 0) {
            // 成功提示
            $.toast({
              heading: '成功',
              text: '删除成功',
              icon: 'success'
            });
            // 跳转到首页帖子列表
            changeNavActive($('#nav-link-title'));
          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

    });

二十一.点赞文章操作

1.在Mapper.xml中编写SQL语句

直接使用生成的updateByPrimaryKeySelective

2.在Mapper.java中定义方法

直接使用生成的updateByPrimaryKeySelective

3.定义Service接口

    void thumbsUpById(Long id);

4.实现Serivce接口

    @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.getLikeCount() == null) {
            //打印日志
            log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }
        article.setLikeCount(article.getLikeCount() + 1);
        article.setUpdateTime(new Date());
        int row = articleMapper.updateByPrimaryKeySelective(article);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }

    }

5.单元测试

    @Test
    void thumbsUpById() {
        articleService.thumbsUpById(1L);
        log.info("更新成功");
    }

更新前

更新后

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

    @ApiOperation("点赞帖子")
    @PostMapping("/thumbsUp")
    public AppResult thumbsUp(@ApiParam("文章id") @RequestParam("id") @NonNull Long id,
                            HttpServletRequest httpServletRequest) {
        // 从session中获取当前登录的用户
        HttpSession session = httpServletRequest.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);
        // 校验帖子是否存在
        if (article == null) {
            // 返回提示信息
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        // 判断当前登录用户是不是作者
        if (article.getUserId() != user.getId()) {
            // 返回提示信息
            return AppResult.failed("您不是帖子的作者,无权修改.");
        }
        // 校验帖子状态
        if (article.getState() == 1) {
            // 返回提示信息
            return AppResult.failed("帖子状态异常, 请联系管理员");
        }
        articleService.thumbsUpById(id);
        return AppResult.success("点赞成功");

    }

7.测试API接口

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

    // ====================== 处理点赞 ======================
    $('#details_btn_like_count').click(function () {
      $.ajax({
        type: "post",
        url: "/article/thumbsUp?id=" + currentArticle.id,
        success: function (respData) {
          if (respData.code == 0) {
            currentArticle.likeCount = currentArticle.likeCount + 1;
            // 设置页面的值 
            $('#details_article_likeCount').html(currentArticle.likeCount);
            // 成功提示
            $.toast({
              heading: '成功',
              text: '点赞成功',
              icon: 'success'
            });
          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });
    });

二十二.根据文章Id查询回复列表

先来进行插入数据

-- 写入回复表数据
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');

1.在Mapper.xml中编写SQL语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javastudy.forum.dao.ArticleReplyMapper">
  <!-- 自定义表关联的结果集映射 -->
  <resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.ArticleReply" extends="BaseResultMap">
    <!-- 关联用户表 -->
    <association property="user" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_" />
  </resultMap>

  <!-- 根据帖子Id查询回复列表 -->
  <select id="selectByArticleId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">
    select
      u.id as u_id,
      u.nickname as u_nickname,
      u.gender as u_gender,
      u.avatarUrl as u_avatarUrl,
      u.phoneNum as u_phoneNum,
      u.email as u_email,
      ar.id,
      ar.articleId,
      ar.postUserId,
      ar.replyId,
      ar.replyUserId,
      ar.content,
      ar.likeCount,
      ar.state,
      ar.deleteState,
      ar.createTime,
      ar.updateTime
    from t_article_reply ar, t_user u
    where ar.postUserId = u.id
      and ar.articleId = #{articleId,jdbcType=BIGINT}
      and ar.deleteState = 0
    order by ar.createTime desc
  </select>
</mapper>

2.在Mapper.java中定义方法

List<ArticleReply> selectByArticleId(Long articleId);

3.定义Service接口

public interface IArticleReplyService {

    List<ArticleReply> selectByArticleId(Long articleId);
}

4.实现Serivce接口

@Slf4j
@Service
public class ArticleReplyService implements IArticleReplyService {

    @Resource
    private ArticleReplyMapper articleReplyMapper;

    @Override
    public List<ArticleReply> selectByArticleId(Long articleId) {
        if (articleId == null || articleId < 0) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        List<ArticleReply> articleReplies = articleReplyMapper.selectByArticleId(articleId);

        return articleReplies;
    }
}

5.单元测试

@Slf4j
@SpringBootTest
class ArticleReplyServiceTest {
    @Resource
    private IArticleReplyService articleReplyService;

    @Test
    void selectByArticleId() {
        List<ArticleReply> articleReplies = articleReplyService.selectByArticleId(1L);
        log.info(articleReplies.toString());
    }
}

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

@RestController
@Slf4j
@RequestMapping("/reply")
@Api(tags = "文章回复接口")
public class ArticleReplyController {
    @Resource
    private IArticleReplyService articleReplyService;

    @Resource
    private IArticleService articleService;

    @GetMapping("/getReplies")
    @ApiOperation("获取回复列表")
    public AppResult<List<ArticleReply>> getAllByBoardId(@ApiParam("板块id") @RequestParam("articleId") @NonNull Long articleId) {
        Article article = articleService.selectById(articleId);
        //判断文章是否删除
        if (article == null || article.getDeleteState() == 1) {
            // 返回错误提示
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        //判断文章状态
        if (article.getState() == 1) {
            // 返回错误提示
            return AppResult.failed("帖子已封帖");
        }
        List<ArticleReply> articleReplies = articleReplyService.selectByArticleId(articleId);
        if (articleReplies == null) {
            articleReplies = new ArrayList<>();
        }

        return AppResult.success(articleReplies);

    }

}

7.测试API接口

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

      $.ajax({
        type: "get",
        url: "/reply/getReplies?articleId=" + currentArticle.id,
        success: function (respData) {
          if (respData.code == 0) {
            buildArticleReply(respData.data);

          } else {
            // 失败(服务器处理业务逻辑失败), 提示用户错误信息
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }

        },
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

二十三.回复文章

回复文章需要进行如下两个操作

1.插入一个回复(ArticleReply表)

2.增加文章的回复数(Article表)

因为涉及到多次更新的操作,因此也需要事务操作

1.在Mapper.xml中编写SQL语句

1.Article表

已经自动生成updateByPrimaryKeySelective

2.ArticleReply表

已经自动生成insertSelective

2.在Mapper.java中定义方法

1.Article表

已经自动生成updateByPrimaryKeySelective

2.ArticleReply表

已经自动生成insertSelective

3.定义Service接口

1.Article表

    void updateArticleCountById(Long id);

2.ArticleReply表

    @Transactional
    void create(ArticleReply articleReply);

4.实现Serivce接口

1.Article表

    @Override
    public void updateArticleCountById(Long id) {
        if (id == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Article article = selectById(id);
        if (article == null) {
            //打印日志
            log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }

        article.setReplyCount(article.getReplyCount() + 1);
        article.setUpdateTime(new Date());
        int row = articleMapper.updateByPrimaryKeySelective(article);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

2.ArticleReply表

    @Override
    public void create(ArticleReply articleReply) {
        if (articleReply == null || StringUtils.isEmpty(articleReply.getContent()) ||
                articleReply.getPostUserId() == null || articleReply.getPostUserId() < 0 ||
                articleReply.getArticleId() == null || articleReply.getArticleId() < 0) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }

        //设置默认值
        articleReply.setLikeCount(0); // 点赞数
        articleReply.setState((byte) 0); // 状态
        articleReply.setDeleteState((byte) 0); // 是否删除
        Date date = new Date();
        articleReply.setCreateTime(date); // 创建时间
        articleReply.setUpdateTime(date); // 更新时间

        int row = articleReplyMapper.insertSelective(articleReply);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        //更新文章数
        articleService.updateArticleCountById(articleReply.getArticleId());

        // 打印日志
        log.info(ResultCode.SUCCESS.toString() + " 回复发布成功, articleId = " + articleReply.getArticleId());
        
    }

5.单元测试

1.Article表

    @Test
    void updateArticleCountById() {
        articleService.updateArticleCountById(1L);
        log.info("更新成功");
    }

2.ArticleReply表

    @Test
    void create() {
        ArticleReply articleReply = new ArticleReply();
        articleReply.setArticleId(3L);
        articleReply.setPostUserId(2L);
        articleReply.setContent("测试评论内容");
        articleReplyService.create(articleReply);
    }

更新前

更新后

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

    @PostMapping("/create")
    @ApiOperation("发布回复")
    public AppResult<List<ArticleReply>> create(@ApiParam("文章id") @RequestParam("articleId") @NonNull Long articleId,
                                                @ApiParam("文章内容") @RequestParam("content") @NonNull String content,
                                                HttpServletRequest httpServletRequest) {
        HttpSession session = httpServletRequest.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(articleId);
        //判断文章是否删除
        if (article == null || article.getDeleteState() == 1) {
            // 返回错误提示
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        //判断文章状态
        if (article.getState() == 1) {
            // 返回错误提示
            return AppResult.failed("文章已封帖");
        }
        ArticleReply articleReply=new ArticleReply();
        articleReply.setContent(content);
        articleReply.setPostUserId(user.getId());
        articleReply.setArticleId(articleId);
        
        articleReplyService.create(articleReply);

        return AppResult.success("发布回复成功");

    }

7.测试API接口

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

      // 构造帖子对象
      var postData = {
        articleId: articleIdEl.val(),
        content: replyContentEl.val()
      }

      // 发送请求,成功后 
      // 1. 清空回复区域
      // 2. 更新回贴数 currentArticle.replyCount = currentArticle.replyCount + 1;
      // 3. 调用loadArticleDetailsReply()方法,重新构建回贴列表

      $.ajax({
        type: "post",
        url: "/reply/create",
        contentType: 'application/x-www-form-urlencoded',
        data: postData,
        // 成功回调
        success: function (respData) {
          if (respData.code == 0) {
            // 提示
            $.toast({
              heading: '成功',
              text: respData.message,
              icon: 'success'
            });
            // 清空回复区域
            editor.setValue('');
            // 更新回贴数
            // 1. 更新全局变量中的回贴数
            currentArticle.replyCount = currentArticle.replyCount + 1;
            // 2. 更新页面的值
            $('#details_article_replyCount').html(currentArticle.replyCount);
            // 3. 重新加载回复列表
            loadArticleDetailsReply();

          } else {
            // 失败
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }
        },
        // 失败 (HTTP)
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });
    });

二十四.用户个人信息和文章展示

已经有根据id获取用户信息的方法,现在我们只需要根据userId获取所有文章信息的接口即可

1.在Mapper.xml中编写SQL语句

    <!-- 根据用户Id查询帖子列表-->
    <select id="selectByUserId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">
        select
            b.id as b_id,
            b.name as b_name,
            a.id,
            a.boardId,
            a.userId,
            a.title,
            a.visitCount,
            a.replyCount,
            a.likeCount,
            a.state,
            a.deleteState,
            a.createTime,
            a.updateTime
        from t_article as a, t_board b
        where a.boardId = b.id
          and a.userId = #{userId,jdbcType=BIGINT}
          and a.deleteState = 0
        order by a.createTime desc
    </select>

2.在Mapper.java中定义方法

List<Article> selectByUserId(Long userId);

3.定义Service接口

List<Article> selectByUserId(Long userId);

4.实现Serivce接口

    @Override
    public List<Article> selectByUserId(Long userId) {
        if (userId == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        List<Article> articles = articleMapper.selectByUserId(userId);
        return articles;
    }

5.单元测试

    @Test
    void selectByUserId() {
        List<Article> articles = articleService.selectByUserId(1L);
        log.info(articles.toString());
    }

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

    @ApiOperation("获取用户文章列表")
    @GetMapping("/getAllByUserId")
    public AppResult<List<Article>> getAllByUserId(@ApiParam("用户id") @RequestParam(value = "userId", required = false) Long userId,
                                                   HttpServletRequest httpServletRequest) {
        if (userId == null) {
            HttpSession session = httpServletRequest.getSession(false);
            User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
            userId = user.getId();
        }
        List<Article> articles = articleService.selectByUserId(userId);
        if (articles == null) {
            articles = new ArrayList<>();
        }
        return AppResult.success(articles);
    }

7.测试API接口

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

profile.html页面

1.获取用户信息

    $.ajax({
      type: 'get',
      url: '/user/info' + userInfoQueryString,
      // 成功回调
      success: function (respData) {
        if (respData.code == 0) {
          // 把查询到的数据设置到页面上
          initProfileUserInfo(respData.data);
        } else {
          // 失败
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }
      },
      // 失败 (HTTP)
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

2.获取用户发布的文章

    $.ajax({
      type: "get",
      url: "/article/getAllByUserId" + articleListQueryString,
      success: function (respData) {
        if (respData.code == 0) {
          buildProfileUserArticle(respData.data);

        } else {
          // 失败(服务器处理业务逻辑失败), 提示用户错误信息
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }

      },
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

二十五.修改用户信息

修改用户信息,首先需要获取用户的信息,前面的接口已经实现,我们只需要在前端根据接口获取信息展示到页面上即可

1.在Mapper.xml中编写SQL语句

已经自动生成了updateByPrimaryKeySelective

2.在Mapper.java中定义方法

已经自动生成了updateByPrimaryKeySelective

3.定义Service接口

    void modifyInfo(User user);

4.实现Serivce接口

    @Override
    public void modifyInfo(User user) {
        if (user == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 定义一个标识
        boolean checkParam = false;
        //更新用户
        User updateUser = new User();
        updateUser.setId(user.getId());
        //用户名
        if (!StringUtils.isEmpty(user.getUsername())) {
            User user1 = selectByUsername(user.getUsername());
            if (user1 == null) {
                updateUser.setUsername(user.getUsername());
                checkParam = true;
            } else {
                throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));
            }

        }
        //昵称
        if (!StringUtils.isEmpty(user.getNickname())) {
            updateUser.setNickname(user.getNickname());
            checkParam = true;
        }
        //性别
        if (user.getGender() != null) {
            // 设置性别
            updateUser.setGender(user.getGender());
            // 校验有效性 0女 1男 2保密
            if (updateUser.getGender() < 0 || updateUser.getGender() > 2) {
                // 处理方式1:设置成默认值
                // 处理方式2:设置为null, 表示保持原来的性别,不更新数据库
                updateUser.setGender((byte) 2);
            }
            // 更新标识
            checkParam = true;

        }
        //电话号码
        if (!StringUtils.isEmpty(user.getPhonenum())) {
            updateUser.setPhonenum(user.getPhonenum());
            checkParam = true;
        }
        //邮箱
        if (!StringUtils.isEmpty(user.getEmail())) {
            updateUser.setEmail(user.getEmail());
            checkParam = true;
        }
        //简介
        if (!StringUtils.isEmpty(user.getRemark())) {
            updateUser.setRemark(user.getRemark());
            checkParam = true;
        }

        if (checkParam == false) {
            throw new ApplicationException("所有信息不能同时为空");
        }
        int row = userMapper.updateByPrimaryKeySelective(updateUser);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

5.单元测试

    @Test
    void modifyInfo() {
        User user=new User();
        user.setId(1L);
        user.setNickname("小张三");
        user.setRemark("我是快乐小张三");
        user.setGender((byte) 1);
        user.setPhonenum("123456");
        user.setEmail("[email protected]");
        userService.modifyInfo(user);
    }

更新前

更新后

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

    @ApiOperation("修改个人信息")
    @PostMapping("/modifyInfo")
    public AppResult<User> modifyInfo(HttpServletRequest httpServletRequest,
                                      @ApiParam("用户名(用于登录)") @RequestParam(value = "username", required = false) String username,
                                      @ApiParam("昵称") @RequestParam(value = "nickname", required = false) String nickname,
                                      @ApiParam("性别") @RequestParam(value = "gender", required = false) Byte gender,
                                      @ApiParam("邮箱") @RequestParam(value = "email", required = false) String email,
                                      @ApiParam("电话") @RequestParam(value = "phoneNum", required = false) String phoneNum,
                                      @ApiParam("个人简介") @RequestParam(value = "remark", required = false) String remark) {
        if (StringUtils.isEmpty(username) && StringUtils.isEmpty(nickname) && gender == null &&
                StringUtils.isEmpty(email) && StringUtils.isEmpty(phoneNum) && StringUtils.isEmpty(remark)) {
            return AppResult.failed("要修改的信息不能全为空");
        }
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        User updateUser = new User();
        updateUser.setId(user.getId());
        updateUser.setUsername(username); // 用户名
        updateUser.setNickname(nickname); // 昵称
        updateUser.setGender(gender); // 性别
        updateUser.setPhonenum(phoneNum); // 电话
        updateUser.setEmail(email); // 邮箱
        updateUser.setRemark(remark); // 个人简介

        userService.modifyInfo(updateUser);

        user = userService.selectById(user.getId());

        session.setAttribute(AppConfig.SESSION_USER_KEY, user);

        return AppResult.success(user);
    }

7.测试API接口

修改前

修改后

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

1.获取用户信息

    $.ajax({
      type: 'get',
      url: '/user/info',
      // 成功回调
      success: function (respData) {
        if (respData.code == 0) {
          // 把查询到的数据设置到页面上
          initUserInfo(respData.data);
        } else {
          // 失败
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }
      },
      // 失败 (HTTP)
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }
    });

2.修改用户信息

    $.ajax({
      type: "post",
      url: userURL,
      contentType: 'application/x-www-form-urlencoded',
      data: userInfo,
      // 成功回调
      success: function (respData) {
        if (respData.code == 0) {
          // 提示
          $.toast({
            heading: '成功',
            text: respData.message,
            icon: 'success'
          });
          // 修改个人信息成功后,更新页面的用户昵称
          if (type == 1) {
            let user = respData.data;
            // 个人中心
            $('#settings_nickname').html(user.nickname);
            // 导航栏
            $('#index_nav_nickname').html(user.nickname);
          } else if (type == 2) {
            // 跳转到登录页面
            location.assign("/sign-in.html");
            return;
          }
        } else {
          // 失败
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }
      },
      // 失败 (HTTP)
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

二十六.修改密码

1.在Mapper.xml中编写SQL语句

已经自动生成了updateByPrimaryKeySelective

2.在Mapper.java中定义方法

已经自动生成了updateByPrimaryKeySelective

3.定义Service接口

void modifyPassword(Long id, String newPassword, String oldPassword);

4.实现Serivce接口

    @Override
    public void modifyPassword(Long id, String newPassword, String oldPassword) {
        if (id < 0 || id == null || StringUtils.isEmpty(newPassword) || StringUtils.isEmpty(oldPassword)) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        User user = selectById(id);
        if (user == null || user.getDeletestate() == 1) {
            // 打印日志
            log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString() + " user id = " + id);
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        if (!MD5Utils.verifyOriginalAndCiphertext(oldPassword, user.getSalt(), user.getPassword())) {
            // 打印日志
            log.warn(ResultCode.FAILED_PASSWORD_CHECK.toString() + " user id = " + id + ", old password = " + oldPassword);
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PASSWORD_CHECK));
        }
        String salt = UUIDUtils.UUID_32();
        User updateUser = new User();
        updateUser.setId(user.getId());
        updateUser.setSalt(salt);
        updateUser.setPassword(MD5Utils.md5Salt(newPassword, salt));
        updateUser.setUpdatetime(new Date());
        int row = userMapper.updateByPrimaryKeySelective(updateUser);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }

    }

5.单元测试

    @Test
    void modifyPassword() {
        userService.modifyPassword(2L, "654321","123456");
        log.info("更新成功");
    }

更新前

更新后

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

    @ApiOperation("修改密码")
    @PostMapping("/modifyPwd")
    public AppResult modifyPassword(@ApiParam("原密码") @RequestParam("oldPassword") @NonNull String oldPassword,
                                    @ApiParam("新密码") @RequestParam("newPassword") @NonNull String newPassword,
                                    @ApiParam("确认密码") @RequestParam("passwordRepeat") @NonNull String passwordRepeat,
                                    HttpServletRequest httpServletRequest) {
        //判断两次输入的密码
        if (!newPassword.equals(passwordRepeat)) {
            return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);

        }
        //获取当前登录的用户信息
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        //修改密码
        userService.modifyPassword(user.getId(), newPassword, oldPassword);
        //销毁当前的session
        session.invalidate();
        return AppResult.success("修改成功");

    }

7.测试API接口

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

    // 发送请求,提示响应结果
    $.ajax({
      type: "post",
      url: userURL,
      contentType: 'application/x-www-form-urlencoded',
      data: userInfo,
      // 成功回调
      success: function (respData) {
        if (respData.code == 0) {
          // 提示
          $.toast({
            heading: '成功',
            text: respData.message,
            icon: 'success'
          });
          // 修改个人信息成功后,更新页面的用户昵称
          if (type == 1) {
            let user = respData.data;
            // 个人中心
            $('#settings_nickname').html(user.nickname);
            // 导航栏
            $('#index_nav_nickname').html(user.nickname);
          } else if (type == 2) {
            // 提示
            $.toast({
              heading: '成功',
              text: respData.message,
              icon: 'success'
            });
            // 跳转到登录页面
            location.assign("/sign-in.html");
            return;
          }
        } else {
          // 失败
          $.toast({
            heading: '警告',
            text: respData.message,
            icon: 'warning'
          });
        }
      },
      // 失败 (HTTP)
      error: function () {
        $.toast({
          heading: '错误',
          text: '访问出现问题,请联系管理员',
          icon: 'error'
        });
      }

    });

二十七.站内信功能

这个功能涉及到的是message表

1.在Mapper.xml中编写SQL语句

直接使用z自动生成的

2.在Mapper.java中定义方法

3.定义Service接口

public interface IMessageService {

    void create(Message message);
}

4.实现Serivce接口

@Slf4j
@Service
public class MessageServiceImpl implements IMessageService {
    @Resource
    private MessageMapper messageMapper;

    @Resource
    private IUserService userService;

    @Override
    public void create(Message message) {
        if (message == null || StringUtils.isEmpty(message.getContent()) || message.getPostUserId() == null || message.getReceiveUserId() == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //校验接受者
        User user = userService.selectById(message.getReceiveUserId());
        if (user == null || user.getDeletestate() == 1) {
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        message.setCreateTime(new Date());
        message.setUpdateTime(new Date());
        message.setState((byte) 0);

        //发送消息
        int row = messageMapper.insertSelective(message);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }

    }
}

5.单元测试

@SpringBootTest
@Slf4j
class MessageServiceImplTest {
    @Resource
    IMessageService messageService;

    @Test
    void create() {
        Message message=new Message();
        message.setPostUserId(1L);
        message.setReceiveUserId(2L);
        message.setContent("hello,路飞");

        messageService.create(message);
        log.info("插入成功");

    }
}

更新前

更新后

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

@RestController
@RequestMapping("/message")
@Slf4j
@Api(tags = "消息接口")
public class MessageController {
    @Resource
    private IMessageService messageService;

    @Resource
    private IUserService userService;

    @PostMapping("/create")
    @ApiOperation("发送消息")
    public AppResult create(@ApiParam("消息内容") @RequestParam("content") @NonNull String content,
                            @ApiParam("消息接收方") @RequestParam("receiveUserId") @NonNull Long receiveUserId,
                            HttpServletRequest httpServletRequest) {
        if (StringUtils.isEmpty(content) || receiveUserId == null) {
            return AppResult.failed("内容和接收者id不能为null");
        }
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        //校验用户是否存在
        if (user == null || user.getDeletestate() == null) {
            return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);
        }
        //校验用户状态
        if (user.getState() == 1) {
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        if (user.getId() == receiveUserId) {
            return AppResult.failed("不能给自己发送消息");
        }
        //查询接受者
        User receiveUser = userService.selectById(receiveUserId);
        //校验接受者状态
        if (receiveUser == null || receiveUser.getDeletestate() == 1) {
            return AppResult.failed("接收者状态异常");
        }
        //构建message对象
        Message message = new Message();
        message.setContent(content);
        message.setPostUserId(user.getId());
        message.setReceiveUserId(receiveUserId);
        messageService.create(message);

        return AppResult.success("发送成功");

    }

}

7.测试API接口

发送前

发送后

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

在index.html页面

      // 发送站内信请求 url = message/send, 成功与失败都调用cleanMessageForm()方法,清空输入框
      $.ajax({
        type: "post",
        url: "/message/create",
        contentType: 'application/x-www-form-urlencoded',
        data: postData,
        success: function (respData) {
          if (respData.code == 0) {
            //清空
            cleanMessageForm()
            // 提示
            $.toast({
              heading: '成功',
              text: respData.message,
              icon: 'success'
            });
            

          } else {
            // 失败
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }
        },
        // 失败 (HTTP)
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

二十八.查看未读数

1.在Mapper.xml中编写SQL语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javastudy.forum.dao.MessageMapper">

    <select id="selectByNum" resultType="java.lang.Long" parameterType="java.lang.Long">
        select count(*)
        from t_message
        where state = 0
          and deleteState = 0
          and receiveUserId = #{userId,jdbcType=BIGINT};
    </select>

</mapper>

2.在Mapper.java中定义方法

Long selectByNum(Long userId);

3.定义Service接口

Long selectUnreadCount(Long userId);

4.实现Serivce接口

    @Override
    public Long selectUnreadCount(Long userId) {
        if (userId == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        User user = userService.selectById(userId);
        if (user == null || user.getDeletestate() == 1) {
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Long num = messageMapper.selectByNum(userId);
        return num;
    }

5.单元测试

    @Test
    void selectUnreadCount() {
        Long aLong = messageService.selectUnreadCount(1L);
        log.info("id=1的用户未读数:"+aLong);
    }

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

    @GetMapping("/getUnreadCount")
    @ApiOperation("获取未读数")
    public AppResult<Long> getUnreadCount(HttpServletRequest httpServletRequest) {
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        //检验用户
        if (user == null || user.getDeletestate() == 1) {
            return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);
        }
        Long num = messageService.selectUnreadCount(user.getId());
        return AppResult.success(num);

    }

7.测试API接口

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

    // ============ 获取用户未读站内信数量 ============
    // 成功后,处理小红点是否显示 #index_nva_message_badge
    function requestMessageUnreadCount() {
      $.ajax({
        type: "get",
        url: "/message/getUnreadCount",
        success: function (respData) {
          if (respData.code == 0) {
            var messageBadgeEl = $('#index_nva_message_badge');
            if (respData.data > 0) {
              messageBadgeEl.show();
            } else {
              messageBadgeEl.hide();
            }

          } else {
            // 失败
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }
        },
        // 失败 (HTTP)
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });
    }

二十九.获取消息列表

在获取消息的时候,我们也需要发送者的用户信息,因此在message实体类中加上一个属性

private User postUser;

1.在Mapper.xml中编写SQL语句

    <resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.Message" extends="BaseResultMap">
        <!-- 扩展⽤⼾信息结果, 注意查询结果列名的前缀为 u_ -->
        <association property="postUser" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap"
                     columnPrefix="u_"></association>
    </resultMap>

    <select id="selectByReceiveUserId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">
        select u.id        as u_id,
               u.avatarurl as u_avatarurl,
               u.nickname  as u_nickname,
               u.gender    as u_gender,
               m.id,
               m.postUserId,
               m.receiveUserId,
               m.content,
               m.state,
               m.createTime,
               m.updateTime
        from t_message m,
             t_user u
        where m.postUserId = u.id
          and m.deleteState = 0
          and m.receiveUserId = #{receiveUserId,jdbcType=BIGINT}
        order by m.createTime desc, m.state asc
    </select>

2.在Mapper.java中定义方法

List<Message> selectByReceiveUserId(Long receiveUserId);

3.定义Service接口

List<Message> selectById(Long id);

4.实现Serivce接口

    @Override
    public List<Message> selectById(Long id) {
        if (id == null || id < 0) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        List<Message> messages = messageMapper.selectByReceiveUserId(id);
        return messages;
    }

5.单元测试

    @Test
    void selectById() {
        List<Message> messages = messageService.selectById(1L);
        log.info(messages.toString());

    }

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

    @GetMapping("/getAll")
    @ApiOperation("查询用户的所有站内信")
    public AppResult<List<Message>> getAll(HttpServletRequest httpServletRequest) {
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        //检验用户
        if (user == null || user.getDeletestate() == 1 || user.getId() == null) {
            return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);
        }
        List<Message> messages = messageService.selectById(user.getId());
        return AppResult.success(messages);

    }

7.测试API接口

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

    // ============ 获取用户所有站内信 ============
    function requestMessageList() {
      $.ajax({
        type:"get",
        url:"/message/getAll",
        success: function (respData) {
          if (respData.code == 0) {
            buildMessageList(respData.data);
          } else {
            // 失败
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }
        },
        // 失败 (HTTP)
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });
    }

三十.站内信已读状态更新

1.在Mapper.xml中编写SQL语句

使用已经生成的updateByPrimaryKeySelective

2.在Mapper.java中定义方法

使用已经生成的updateByPrimaryKeySelective

3.定义Service接口

void updateStateById(Long id,Byte state);

4.实现Serivce接口

    @Override
    public void updateStateById(Long id, Byte state) {
        // 非空校验, state : 0 未读 1 已读 2 已回复
        if (id == null || state == null || id < 0 || state < 0 || state > 2) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Message message = new Message();
        message.setId(id);
        message.setState(state);
        message.setUpdateTime(new Date());
        int row = messageMapper.updateByPrimaryKey(message);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }

    }

5.单元测试

    @Test
    void updateStateById() {
        messageService.updateStateById(1L, (byte) 1);
        log.info("更新成功");
    }

更新前

更新后

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

    @PostMapping("/markRead")
    @ApiOperation("更新为已读")
    public AppResult markRead(HttpServletRequest httpServletRequest,
                              @ApiParam("消息id") @RequestParam("id") Long id) {
        Message message = messageService.selectById(id);
        //校验消息的状态
        if (message == null || message.getDeleteState() == 1) {
            return AppResult.failed(ResultCode.FAILED_MESSAGE_NOT_EXISTS);
        }
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        //校验接受者是否和当前用户一致
        if (user.getId() != message.getReceiveUserId()) {
            return AppResult.failed(ResultCode.FAILED_FORBIDDEN);
        }

        messageService.updateStateById(id, (byte) 1);
        return AppResult.success("更改状态成功");

    }

7.测试API接口

更新前

更新后

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

          // 发送请求,更新状态为已读
          if (messageItem.state == 0 && statusDotEl.hasClass('status-dot-animated bg-red')) {
            $.ajax({
              type: 'post',
              url: '/message/markRead',
              contentType: 'application/x-www-form-urlencoded',
              data: { id: messageItem.id },
              // 成功回调
              success: function (respData) {
                if (respData.code == 0) {
                  // 更新页面显示效果和messageItem.state
                  statusDotEl.removeClass('status-dot-animated bg-red');
                  // 修改未读为已读
                  statusDescEl.html('[已读]');
                  // 修改本地的对象状态属性
                  messageItem.state = 1;
                }
              }
            });
          }

三十一.站内信回复功能

1.在Mapper.xml中编写SQL语句

使用已经生成的updateByPrimaryKeySelective

2.在Mapper.java中定义方法

使用已经生成的updateByPrimaryKeySelective

3.定义Service接口

void reply(Long repliedId, Message message);

4.实现Serivce接口

    @Override
    public void reply(Long repliedId, Message message) {
        if (repliedId == null || repliedId < 0 || message == null) {
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        Message repliedMessage = selectById(repliedId);
        if (repliedMessage == null || repliedMessage.getDeleteState() == 1) {
            // 打印日志
            log.warn(ResultCode.FAILED_MESSAGE_NOT_EXISTS.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_MESSAGE_NOT_EXISTS));
        }
        //更新状态
        updateStateById(repliedId, (byte) 2);
        //发送信息
        create(message);
    }

5.单元测试

    @Test
    void reply() {
        Message message = new Message();
        message.setContent("我也爱你");
        message.setPostUserId(1L);
        message.setReceiveUserId(2L);
        messageService.reply(2L, message);
    }

更新前

更新后

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

    @PostMapping("/reply")
    @ApiOperation("回复站内信")
    public AppResult reply(HttpServletRequest httpServletRequest,
                           @ApiParam("要回复的站内信Id") @RequestParam("repliedId") @NonNull Long repliedId,
                           @ApiParam("站内信的内容") @RequestParam("content") @NonNull String content) {
        //校验当前用户状态
        HttpSession session = httpServletRequest.getSession(false);
        User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
        if (user.getState() == 1) {
            // 返回错误描述
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //校验要回复的站内信状态
        Message message = messageService.selectById(repliedId);
        if (message == null || message.getDeleteState() == 1) {
            return AppResult.failed(ResultCode.FAILED_MESSAGE_NOT_EXISTS);
        }
        // 不能给自己回复
        if (user.getId() == message.getPostUserId()) {
            // 返回错误描述
            return AppResult.failed("不能回复自己的站内信");
        }
        //构造对象
        Message postMessage = new Message();
        postMessage.setContent(content);
        postMessage.setUpdateTime(new Date());
        postMessage.setCreateTime(new Date());
        postMessage.setPostUserId(user.getId());
        postMessage.setReceiveUserId(message.getPostUserId());

        messageService.reply(repliedId, postMessage);
        return AppResult.success("回复成功");

    }

7.测试API接口

更新前

更新后

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

      $.ajax({
        type: "post",
        url: "/message/reply",
        contentType: 'application/x-www-form-urlencoded',
        data: postData,
        success: function (respData) {
          if (respData.code == 0) {
            // 回复成功后刷新未读标识和站内信列表
            requestMessageUnreadCount();
            requestMessageList();
            // 清空输入区
            cleanMessageReplyForm();
            // 提示
            $.toast({
              heading: '成功',
              text: respData.message,
              icon: 'success'
            });

          } else {
            // 失败
            $.toast({
              heading: '警告',
              text: respData.message,
              icon: 'warning'
            });
          }
        },
        // 失败 (HTTP)
        error: function () {
          $.toast({
            heading: '错误',
            text: '访问出现问题,请联系管理员',
            icon: 'error'
          });
        }

      });

三十二.项目的部署与发布

1.部署linux服务器

1.创建数据库

可以将sql语句变成一个文件,然后执行下面的

source /root/java78/table.sql

2.将代码打包

打包完成之后,将jar包拖拽到linux服务器上

3.运行代码

后台启动项目

nohup java -jar Blog_Spring-0.0.1-SNAPSHOT.jar &

查看日志

cd logs/

tail -f spring.log

​​​​​

** 终止当前的服务**

ps -ef | grep [ ]

kill -9 pid

注意:如果开启多个服务,需要开端口,给防火墙添加端口号

查看防火墙状态(如果没开启,建议开启,不开启可以直接访问,开启了需要进行已下的操作访问)

systemctl status firewalld

启动防火墙和关闭防火墙

systemctl start firewalld

systemctl stop firewalld

查看开放的端口号

firewall-cmd --list-ports

开启8080端口

firewall-cmd --permanent --add-port=8080/tcp

重启防火墙

firewall-cmd --reload

设置开机启动

systemctl enable firewalld

添加安全组

2.访问测试

访问地址:比特论坛 - 用户登录

待拓展的功能

头像上传

分页展示

记录用户点赞的帖子

回复楼中楼
回复站内信楼中楼
热帖+Redis缓存
站内信及时通知
用户权限管理
版块管理
用户管理

最后.项目描述

论坛项目围绕帖子与用户两个核心角色进行业务处理,大家要能够理清每个功能的业务流程,并正确的写出实现代码。
在描述项目时,建议先介绍一下基本功能,比如对帖子的操作(增删改查),点赞,回复,对用户信息的修改,个人中心等,再具体描述技术实现过程,一般采用的格式为:用XXX技术实现了XXX功能,达到了XXX效果,以下是几个示例:

  1. 使用统一返回格式+全局错误信息定义处理前后端交互时的返回结果
  2. 使用@ControllerAdvice+@ExceptionHandler实现全局异常处理
  3. 使用拦截器实现用户登录校验
  4. 使用MybatisGeneratorConfig生成常的增删改查方法
  5. 集成Swagger实现自动生成API测试接口
  6. 使用jQuery完成AJAX请求,并处理HTML页面标签
  7. 对数据库中常用的查询字段建立索引,并使用查询计划分析索引是否生效
标签: spring java 后端

本文转载自: https://blog.csdn.net/qq_64580912/article/details/132131130
版权归原作者 允歆辰丶 所有, 如有侵权,请联系我们删除。

“基于 Spring 前后端分离版本的论坛系统”的评论:

还没有评论