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

目录

前言

一、项目背景

二、相关技术及工具

三、数据库设计

四、软件开发

4.1、搭建环境

4.1.1、创建工程

4.1.2、配置application.yml文件

4.1.3、环境测试

创建测试接口

4.1.4、继续配置

4.2、公共组件

4.2.1、创建工程结构

4.2.2、配置数据源

添加相关依赖

配置application.yml

测试

4.2.3、编写类与映射文件

根据数据库编写实体类

编写映射文件

编写Dao类

4.2.4、生成类与映射文件

引入依赖

创建generatorConfig.xml

运行插件生成文件

添加获取主键值的选项

扫描配置

测试

4.2.5、编写公共代码

定义状态码

定义返回结果

自定义异常

全局异常处理

登录拦截器

创建LoginInterceptor

application.yml配置文件

创建AppInterceptorConfigurer

实现API自动生成

引入依赖

编写配置类

application.xml添加配置

API常用注解

访问API列表

创建工具类 

创建MD5加密工具类

创建生成UUID工具类

创建字符串工具类

4.3、实现业务功能

4.3.1、注册(后端实现)

请求

响应

创建扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

注册(前端实现)

4.3.2、登录(后端实现)

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现controller层

测试接口

登录(前端实现)

4.3.3、退出

请求

响应

实现Controller层

测试接口

退出(前端页面) 

4.3.4、个人中心

4.3.4.1、获取用户信息

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现controller层

测试接口

修复返回值 

前端代码

4.3.4.2、修改个人信息

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码 

4.3.4.3、修改密码

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.5、版块信息

4.3.5.1、获取在首页中显示的版块

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

application.yml添加配置

实现Controller层

测试接口

前端代码

4.3.5.2、获取指定版块信息 

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.6、帖子列表

4.3.6.1、版块帖子列表

请求

响应

修改Article实体类

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.6.2、用户帖子列表

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7、帖子操作

4.3.7.1、集成编辑区

编写HTML

编写JS

4.3.7.2、发布帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.3、获取帖子详情

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.4、编辑帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.5、删除帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.6、点赞帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.8、帖子回复

4.3.8.1、提交回复内容

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.8.2、帖子回复列表

请求

响应

添加关联对象

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9、站内信

4.3.9.1、发送

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.2、未读数

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.3、列表

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.4、更新状态

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.5、回复

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

五、发布部署

1、执行SQL脚本

2、修改代码中数据源的配置

3、修改配置文件中的日志级别与日志文件路径

4、打包程序

5、上传到服务器

6、验证访问


前言

        个人论坛系统是一种在线社交平台,为用户提供了丰富的功能,让他们能够轻松地创建帖子、分享信息、讨论话题以及互动交流。

        整个项目在开发过程中直接进行了单元测试,具体的自动化测试(功能、界面)在此处进行。

一、项目背景

以下是该系统的主要业务功能:

  1. 用户注册:允许用户创建自己的账户,填写个人信息并进行注册,以便能够使用论坛系统的各项功能。

  2. 用户登录:已注册用户可以通过输入用户名和密码登录到论坛系统,以便访问其个人信息和发表帖子。

  3. 论坛主界面:提供了一个主页面,用户可以在这里浏览各个板块的帖子列表,切换不同的板块浏览不同的内容,同时显示当前用户的信息,如用户名、头像等,还可以在此页面发布新帖子。

  4. 帖子详情页:用户可以点击帖子标题或摘要进入帖子详情页面,可以查看帖子的详细内容,并进行点赞、编辑、删除等操作。

  5. 用户中心页:用户可以在个人中心页查看和修改自己的个人信息,包括用户名、头像、个人简介等,还可以查看收到的站内信(私信),并回复他人的信息。

  6. 站内信功能:用户可以在论坛系统内部发送和接收私信,进行一对一的交流和沟通,也可以在帖子下面回复其他用户的信息,进行公开的讨论和互动。

以上功能使得个人论坛系统成为一个活跃的在线社交平台,为用户提供了丰富的交流和互动机会。

二、相关技术及工具

构架基于MVC实现前后端分离
服务器端技术SpringBoot、SpringMVC、MyBatis
浏览器端技术HTML、CSS、JavaScript、jQuery、Bootstrap
数据库MySQL
项目构建工具Maven
版本控制工具git+gitee
开发工具IntelliJ IDEA 2022.3.3
API文档生成工具Swagger、Springfox
前后端交互数据格式JSON

三、数据库设计

根据数据库设计的方案,利用数据库客户端工具建立数据库及数据表,并生成相应的SQL脚本,具体步骤如下所示。

无特殊要求的情况下,每张表必须有长整型的自增主键,删除状态、创建时间、更新时

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

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

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

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

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

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

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

SET FOREIGN_KEY_CHECKS = 1;

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

insert into t_article values (null, 1, 1, '测试测试111', '测试测试内容内容111', 0,0,0,0,0, '2023-07-19 14:46:00', now());
insert into t_article values (null, 1, 1, '测试测试222', '测试测试内容内容222', 0,0,0,0,0, '2023-07-19 14:46:00', now());
insert into t_article values (null, 2, 1, '测试测试333', '测试测试内容内容333', 0,0,0,0,0, '2023-07-19 14:46:00', now());

四、软件开发

4.1、搭建环境

4.1.1、创建工程

由于目前IDEA不支持JDK1.8版本,所以在搭建环境后需要手动修改

添加依赖

修改一下几处:

4.1.2、配置application.yml文件

#spring全局配置
spring:
  application:
    name: forum_system  #配置项目名称
  output:
    ansi:
      enabled: always  #控制台输出彩色日志

#服务器配置
server:
  port: 58080  #修改Tomcat的默认端口号

#日志配置
logging:
  pattern:
    dateformat: yyyy-MM-dd HH:mm:ss
  level:
    root: info  #默认日志级别
    com.example.forum_system: debug  #指定包的日志级别
  file:
    path: E:\idea-project\log\project\forum  #日志保存目录

4.1.3、环境测试

创建测试接口

创建controller包,包下创建TestController.java

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/hello")
    public String hello(){
        return "hello,baekhyun...";
    }
}

出现以下页面证明测试成功

4.1.4、继续配置

在pom.xml⽂件的properties标签下加⼊如下配置
        <!-- 编译环境JDK版本 -->
		<maven.compiler.source>${java.version}</maven.compiler.source>
		<!-- 运行环境JVM版本 -->
		<maven.compiler.target>${java.version}</maven.compiler.target>
		<!-- 构建项目指定编码集 -->
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

4.2、公共组件

4.2.1、创建工程结构

4.2.2、配置数据源

添加相关依赖
        <!-- 管理依赖版块号-->
        <!--mysql-connector 数据库连接驱动包 -->
        <mysql-connector.version>5.1.49</mysql-connector.version>
        <!-- mybatis -->
        <mybatis-starter.version>2.3.0</mybatis-starter.version>
        <!-- 数据源 -->
        <druid-starter.version>1.2.16</druid-starter.version>
        <!-- 数据库驱动 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql-connector.version}</version>
		</dependency>
		<!-- mybatis 依赖
			其中已经包含了spring-jdbc不再重复引用,
			此项目中使用spring-jdbc提供的HikariCP做为数据源, 相关配置在yml文件中
		-->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>${mybatis-starter.version}</version>
		</dependency>

		<!-- 阿里巴巴druid数据源,如果使用SpringBoot默认的数据源,删除或注释这个依赖即可 -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>${druid-starter.version}</version>
		</dependency>
配置application.yml
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_forum?characterEncoding=utf8&useSSL=false # 数据库连接串
    username: root # 数据库用户名
    password: '19930112' # 数据库密码
    driver-class-name: com.mysql.jdbc.Driver # 数据库驱动类
测试
    @Resource
    private DataSource dataSource;

    @Test
    void testDBConnection() throws SQLException {
        System.out.println("dataSource="+dataSource.getClass());
        Connection connection= dataSource.getConnection();
        System.out.println("connection="+connection.getClass());
        System.out.println(connection);
    }

4.2.3、编写类与映射文件

根据数据库编写实体类
编写映射文件
编写Dao类

由于很多项目中数据库比较庞大,一个一个写类文件里的属性比较费时,所以在此处使用mybatis生成器插件

4.2.4、生成类与映射文件

引入依赖
<!-- mybatis生成器 -->
<mybatis-generator-plugin-version>1.4.1</mybatis-generator-plugin-version>
            <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>
						<!-- 指定Maven中的执行阶段 -->
						<phase>deploy</phase>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
				<!-- 相关配置 -->
				<configuration>
					<!-- 打开日志 -->
					<verbose>true</verbose>
					<!-- 允许覆盖 -->
					<overwrite>true</overwrite>
					<!-- 配置文件路径 -->
					<configurationFile>
						src/main/resources/mybatis/generatorConfig.xml
					</configurationFile>
				</configuration>
			</plugin>
创建generatorConfig.xml
<?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="C:\Users\19756\.m2\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:3306/java_forum?characterEncoding=utf8&amp;useSSL=false"
                        userId="root"
                        password="19930112">
        </jdbcConnection>

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

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

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

        <!-- DAO类生成位置 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.forum_system.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>
运行插件生成文件

点击mybatis.generator即生成相应的文件

接下来给实体类添加@Data注解,删除实体类的get、set方法,是代码更加简洁

添加获取主键值的选项

在mapper下的所有文件中的insert标签中添加

<insert id="insert" parameterType="com.bitejiuyeke.forum.model.User"
useGeneratedKeys="true" keyProperty="id" >
扫描配置
//配置类
@Configuration
//指定Mybatis的扫描路径
@MapperScan("com.example.forum_system.dao")
public class MybatisConfig {
}

application.xml更新配置

#mybatis相关配置
mybatis:
  mapper-locations: classpath:mapper/**/*.xml  # 指定 xxxMapper.xml的扫描路径
测试
    @Resource
    private UserMapper userMapper;

    @Test
    void createUser(){
        User user=new User();
        user.setUsername("边伯贤");
        user.setPassword("1992");
        user.setNickname("啵啵虎");
        user.setGender((byte) 2);
        user.setSalt("123456");
        user.setArticleCount(0);
        user.setState((byte) 0);
        user.setDeleteState((byte) 0);
        Date date = new Date();
        user.setCreateTime(date);
        user.setUpdateTime(date);

        userMapper.insertSelective(user);
        System.out.println("写入成功");
        User user1 = userMapper.selectByPrimaryKey(user.getId());
        System.out.println(user1);
    }

4.2.5、编写公共代码

定义状态码

        在执行业务处理逻辑的过程中,会涉及到各种不同的状态,包括成功和失败。为了更好地管理和处理这些状态,可以使用枚举来定义状态码。

        这些状态码可以根据具体业务需求进行扩展和调整。当业务中遇到新的问题时,可以根据需要添加新的状态码,并为其提供适当的描述,以便更好地反映业务逻辑和处理状态。

/**
 * 系统状态码
 */
public enum ResultCode {

    //定义状态码
    SUCCESS                 (0, "操作成功"),
    FAILED                  (1000, "操作失败"),
    FAILED_UNAUTHORIZED     (1001, "未授权"),
    FAILED_PARAMS_VALIDATE  (1002, "参数校验失败"),
    FAILED_FORBIDDEN        (1003, "禁止访问"),
    FAILED_CREATE           (1004, "新增失败"),
    FAILED_NOT_EXISTS       (1005, "资源不存在"),
    FAILED_USER_EXISTS      (1101, "用户已存在"),
    FAILED_USER_NOT_EXISTS  (1102, "用户不存在"),
    FAILED_LOGIN            (1103, "用户名或密码错误"),
    FAILED_USER_BANNED      (1104, "您已被禁言, 请联系管理员, 并重新登录."),
    FAILED_TWO_PWD_NOT_SAME (1105, "两次输入的密码不一致"),

    FAILED_BOARD_NOT_EXISTS (1201, "版块不存在"),

    FAILED_ARTICLE_NOT_EXISTS (1301, "帖子不存在"),
    FAILED_ARTICLE_STATE      (1302, "帖子状态异常"),

    MESSAGE_NOT_EXISTS        (1401,"站内信不存在"),

    ERROR_SERVICES          (2000, "服务器内部错误"),
    ERROR_IS_NULL           (2001, "IS NULL.");

    //状态码
    int code;

    //错误描述
    String message;

    public int getCode() {
        return code;
    }

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

    public String getMessage() {
        return message;
    }

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

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

    @Override
    public String toString() {
        return "code="+code+",message="+message+".";
    }
}
定义返回结果

在设计前后端分离的系统,并且希望统一返回JSON格式的字符串时,你可以定义一个类来封装返回的数据。这个类可以包含以下属性:

  1. 状态码(status code):表示请求的处理状态,比如成功、失败等。
  2. 描述信息(message):对处理状态的简要描述,用于提示用户或开发者。
  3. 返回结果数据(data):实际的返回数据,可能是查询结果、操作结果等。

通过这个类,你可以方便地组织和传递请求处理的结果,使得前后端之间的通信更加清晰和规范。

public class AppResult<T> {

    //自定义状态码
    private long code;

    //描述信息
    private String message;

    //结果数据
    private T data;

    public long getCode() {
        return code;
    }

    /**
     * 构造方法
     */
    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 <T> AppResult<T> success(String message,T data){
        return new AppResult<>(ResultCode.SUCCESS.code,message,data);
    }

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

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

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

    /**
     * 失败方法
     * @return
     */

    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 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;
    }
}
自定义异常

加入状态码与状态描述属性

/**
 * 自定义异常
 */
public class ApplicationException extends RuntimeException{

    //自定义的异常描述
    private AppResult errorResult;

    //指定状态码,异常描述
    public ApplicationException(AppResult appResult) {
        //构造异常中的Message属性
        super(appResult.getMessage());
        //自定义的错误描述
        this.errorResult=appResult;
    }

    //自定义异常描述
    public ApplicationException(String message) {
        super(message);
        //根据异常描述构建返回对象
        this.errorResult=new AppResult(ResultCode.FAILED.getCode(),message);
    }

    //指定异常
    public ApplicationException(Throwable cause){
        super(cause);
    }

    //自定义异常描述,异常信息
    public ApplicationException(String message,Throwable cause){
        super(message,cause);
    }

    public AppResult getErrorResult() {
        return errorResult;
    }

    public void setErrorResult(AppResult errorResult) {
        this.errorResult = errorResult;
    }
}
全局异常处理

实现统一异常处理

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理自定义的已知异常
     * @param e ApplicationException
     * @return AppResult
     */
    // 以body形式返回
    @ResponseBody
    // 指定要处理的异常
    @ExceptionHandler(ApplicationException.class)
    public AppResult handleApplicationException (ApplicationException e) {
        // 打印异常
        e.printStackTrace(); // 在上生产之前一定要记得把这行代码注释掉
        // 记录日志
        log.error(e.getMessage());
        // 获取异常信息
        if (e.getErrorResult() != null) {
            // 返回异常类中记录的状态
            return e.getErrorResult();
        }
        // 默认返回异常信息
        return AppResult.failed(e.getMessage());
    }

    /**
     * 处理全未捕获的其他异常
     * @param e Exception
     * @return AppResult
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public AppResult handleException (Exception e) {
        // 打印异常
        e.printStackTrace(); // 在上生产之前一定要记得把这行代码注释掉
        // 记录日志
        log.error(e.getMessage());
        if (e.getMessage() == null) {
            return AppResult.failed(ResultCode.ERROR_SERVICES);
        }
        // 默认返回异常信息
        return AppResult.failed(e.getMessage());
    }
}

测试异常处理

    @GetMapping("testException")
    public AppResult testException() throws Exception {
        throw new Exception("这是一个Exception...");
    }

    @GetMapping("applicationException")
    public AppResult testApplicationException() throws Exception {
        throw new ApplicationException("这是一个自定义的ApplicationException...");
    }

访问以下链接:

http://127.0.0.1:58080/test/testException

http://127.0.0.1:58080/test/applicationException

登录拦截器
创建LoginInterceptor
@Component
public class LoginInterceptor implements HandlerInterceptor {

    //从配置文件中获取默认登录页的URL
    @Value("${forum_system.login.url}")
    private String defaultURL;

    /**
     * 请求的前置处理
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取session并做已登录用户信息校验
        HttpSession session= request.getSession(false);
        if (session!=null && session.getAttribute(AppConfig.USER_SESSION_KEY)!=null){
            //校验通过
            return true;
        }
        //保证跳转页面的路正确性
        if (!defaultURL.startsWith("/")){
            defaultURL="/"+defaultURL;
        }
        //校验未通过,跳转到登录页面
        response.sendRedirect(defaultURL);
        //中止请求
        return false;
    }
}
application.yml配置文件
#项目自定义配置
forum_system:
  login:
    url: sign-in.html  #未登录状态下强制跳转页面
创建AppInterceptorConfigurer
@Configuration
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")
                .addPathPatterns("/js/**");
    }
}
实现API自动生成

使用Springfox Swagger生成API,并导入Postman,完成API单元测试

Swagger是⼀套API定义的规范,按照这套规范的要求去定义接⼝及接⼝相关信息, 再通过可以解析这套规范⼯具,就可以⽣成各种格式的接⼝⽂档,以及在线接⼝调试⻚⾯,通过⾃动 ⽂档的⽅式,解决了接⼝⽂档更新不及时的问题。

引入依赖
<!-- springfox - Swagger -->
<springfox-boot-starter.version>3.0.0</springfox-boot-starter.version>
		<!-- 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>
编写配置类

解决SpringBoot 2.6.0以上与Springfox3.0.0 不兼容的问题,涉及SpringBoot 版本升级过程中的⼀
些内部实现变化,具体说明在修改配置文件部分

/**
 * Swagger配置类
 * @Author baekhyun
 **/

// 配置类
@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.example.forum_system.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;

    }

    // 配置API基本信息
    private ApiInfo apiInfo() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("论坛系统API")
                .description("论坛系统前后端分离API测试")
                .contact(new Contact("BAEKHYUN Tech", "https://edu.forumsystem.com", "1975688561@qq.com"))
                .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));
    }

}
application.xml添加配置
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher  #Springfox-Swagger兼容性配置
API常用注解

@Api: 作用在Controller上,对控制器类的说明

@ApiModel: 作用在响应的类上,对返回响应数据的说明

@ApiModelProerty:作用在类的属性上,对属性的说明

@ApiOperation: 作用在具体方法上,对API接口的说明

@ApiParam: 作用在方法中的每⼀个参数上,对参数的属性进行说明

修改接口测试

@Api(tags = "测试接口")
@RestController
@RequestMapping("/test")
public class TestController {

    @ApiOperation("测试打印")
    @GetMapping("/hello")
    public String hello(){
        return "hello,baekhyun...";
    }

    @ApiOperation("测试异常")
    @GetMapping("/testException")
    public AppResult testException() throws Exception {
        throw new Exception("这是一个Exception...");
    }

    @ApiOperation("测试自定义异常")
    @GetMapping("/applicationException")
    public AppResult testApplicationException() throws Exception {
        throw new ApplicationException("这是一个自定义的ApplicationException...");
    }

    @ApiOperation("测试传参")
    @GetMapping("/helloByName")
    public String testHelloByName(@ApiParam("名字") String name){
        return "hello,"+name;
    }
}
访问API列表

通过访问 http://127.0.0.1:58080/swagger-ui/index.html,可以正常显示接口信息,针对每个接口进行测试

在此处还可以使用postman进行测试,首先获取API地址

将API导入到postman中即可

创建工具类 
创建MD5加密工具类

用户传入一个密码明文

服务器生成一个扰动字符串(盐)

最终密文=MD5(MD5(密码明文)+盐)

导入依赖

<!-- 编码解码加密工具包-->

<dependency>

<groupId>commons-codec</groupId>

<artifactId>commons-codec</artifactId>

</dependency>

public class MD5Utils {

    /**
     * 返回一个用MD5加密后的字符串
     * @param str
     * @return
     */
    public static String md5(String str){
        return DigestUtils.md5Hex(str);
    }

    /**
     * 明文加盐生成最终的密文
     * @param str
     * @param salt
     * @return
     */
    public static String md5Salt(String str,String salt){
        //先对铭文进行MD5加密
        String s=DigestUtils.md5Hex(str);
        //加密后的原文与盐拼接在一起之后在进行一次MD5加密
        String ciphertext=DigestUtils.md5Hex(s+salt);
        //返回密文
        return ciphertext;
    }
}
创建生成UUID工具类

服务器生成一个扰动字符串(盐)

/**
 *  ⽣成UUID⼯具类
 */
public class UUIDUtils {

    /**
     * ⽣成32位UUID
     * @return
     */
    public static String UUID_32(){
        return UUID.randomUUID().toString().replace("-","");
    }

    /**
     * 生成36位UUID
     * @return
     */
    public static String UUID_36(){
        return UUID.randomUUID().toString();
    }
}
创建字符串工具类

校验字符串是否为空

/**
 * 字符串相关的工具类
 */
public class StringUtils {

    /**
     * 校验字符串是否为空
     * @param value
     * @return
     */
    public static boolean isEmpty(String value){
        if (value==null || value.isEmpty()){
            return true;
        }
        return false;
    }
}

4.3、实现业务功能

4.3.1、注册(后端实现)

请求

// 请求
POST /user/register HTTP/1.1

Content-Type: application/x-www-form-urlencoded
username=xxx&nickname=xxx&password=xxx&passwordRepeat=xxx

响应

// 响应
HTTP/1.1 200
Content-Type: application/json
{"code":0,"message":"成功","data":null}

创建扩展Mapper.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.example.forum_system.dao.UserMapper">
    <!--  根据用户名查询用户信息  -->
    <select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List" />
        from t_user
        where deleteState=0
        and username=#{username,jdbcType=VARCHAR}
    </select>
</mapper>
修改DAO
    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    User selectByUsername(@Param("username") String username);
创建Service接口
    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    User selectByUsername(String username);

    /**
     * 创建普通用户
     * @param user
     */
    void createNormalUser(User user);
实现Service接口
@Slf4j
@Service
public class UserServiceImpl implements IUserService {

    @Resource
    private 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));
        }
        //调用DAO
        User user=userMapper.selectByUsername(username);
        //返回结果
        return user;
    }

    @Override
    public void createNormalUser(User user) {
        //1、非空校验
        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));
        }
        //2、校验用户是否存在
        User existUser=selectByUsername(user.getUsername());
        if (existUser!=null){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));
        }
        //3、设置默认值
        if (user.getGender()==null || user.getGender()<0 || user.getGender()>2){
            //性别保密
            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);  //更新时间
        //4、写入用户数据,返回结果
        int row=userMapper.insertSelective(user);
        //判断受影响的行数
        if (row!=1){
            log.warn("用户注册时,"+ResultCode.FAILED_CREATE.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
    }
}
对Service接口进行单元测试
@SpringBootTest
class UserServiceImplTest {

    @Resource
    private IUserService userService;

    @Resource
    private ObjectMapper objectMapper;

    @Test
    void selectByUsername() throws JsonProcessingException {
        User user=userService.selectByUsername("baekhyun");
        System.out.println(objectMapper.writeValueAsString(user));
    }

    @Test
    void createNormalUser() {
        //创建一个用户对象
        User user=new User();
        user.setUsername("baekhyun");
        user.setNickname("baekhyun");
        //处理密码
        String password="1992";  //明文密码
        String salt= UUIDUtils.UUID_32();  //盐
        String ciphertext=MD5Utils.md5Salt(password,salt);
        //设置密码
        user.setPassword(ciphertext);
        //设置盐
        user.setSalt(salt);

        userService.createNormalUser(user);
        System.out.println("写入用户成功 "+user.getId());
    }
}

实现Controller层
@Slf4j
@Api(tags = "用户接口")
@RequestMapping("/user")
@RestController
public class UserController {

    @Resource
    private 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){
        //1、判断密码与确认密码是否相同
        if (!password.equals(passwordRepeat)){
            //返回错误信息
            return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
        }
        //2、判断用户是否存在
        User existUser=userService.selectByUsername(username);
        if (existUser!=null){
            //用户已存在
            return AppResult.failed(ResultCode.FAILED_USER_EXISTS);
        }
        //3、生成密码的密文
        //生成盐
        String salt= UUIDUtils.UUID_32();
        //生成密文
        String ciphertext= MD5Utils.md5Salt(password,salt);
        //构造user对象
        User user=new User();
        user.setUsername(username);  //用户名
        user.setNickname(nickname);  //昵称
        user.setPassword(ciphertext);  //密码(密文)
        user.setSalt(salt);  //盐
        //调用service
        userService.createNormalUser(user);
        //返回正常响应
        return  AppResult.success();
    }
}
测试接口

在controller层加入注解之后,我们不用再手动校验参数是否为空,只做必要的业务校验就行,而且生成的API也有了相应的文字和是否必传的标注。

注册(前端实现)
<!doctype html>

<html lang="zh-CN">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <link rel="shortcut icon" href="/favicon.ico">
  <title>BAEKHYUN论坛 - 用户注册</title>
  <!-- 导入CSS -->
  <link href="./dist/css/tabler.min.css?1674944402" rel="stylesheet" />
  <link rel="stylesheet" href="./dist/css/jquery.toast.css">
  <!-- 设置字体 -->
  <!-- <style>
      @import url('https://rsms.me/inter/inter.css');
      :root {
      	--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
      }
      body {
      	font-feature-settings: "cv03", "cv04", "cv11";
      }
    </style> -->
</head>

<body class="d-flex flex-column">
  <!-- 正文 -->
  <div class="page page-center">
    <div class="container container-tight py-4">
      <div class="text-center mb-4">
        <img src="./image/bit-forum-logo01.png" height="50" alt="">
      </div>
      <form id="signUpForm" class="card card-md" autocomplete="off" novalidate>
        <div class="card-body">
          <h2 class="text-center mb-4">用户注册</h2>
          <!-- 用户名 -->
          <div class="mb-3">
            <label class="form-label required">用户名</label>
            <input type="text" class="form-control " placeholder="请输入用户名" name="username" id="username">
            <div class="invalid-feedback">用户名不能为空</div>
          </div>
          <!-- 昵称 -->
          <div class="mb-3">
            <label class="form-label required">昵称</label>
            <input type="text" class="form-control" placeholder="请输入昵称" name="nickname" id="nickname">
            <div class="invalid-feedback">昵称不能为空</div>
          </div>
          <!-- 密码 -->
          <div class="mb-3">
            <label class="form-label required">密码</label>
            <div class="input-group input-group-flat">
              <input type="password" class="form-control" placeholder="请输入密码" autocomplete="off" name="password"
                id="password">
              <span class="input-group-text">
                <a href="javascript:void(0);" class="link-secondary" id="password_a" title="显示密码"
                  data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
                  <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
                    stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                    <path stroke="none" d="M0 0h24v24H0z" fill="none" />
                    <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
                    <path
                      d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
                  </svg>
                </a>
              </span>
              <div class="invalid-feedback">密码不能为空</div>
            </div>
          </div>
          <!-- 确认密码 -->
          <div class="mb-3">
            <label class="form-label required">确认密码</label>
            <div class="input-group input-group-flat">
              <input type="password" class="form-control" placeholder="再次输入密码" autocomplete="off" name="passwordRepeat"
                id="passwordRepeat">
              <span class="input-group-text">
                <a href="javascript:void(0);" class="link-secondary" id="passwordRepeat_a" title="显示密码"
                  data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
                  <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
                    stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
                    <path stroke="none" d="M0 0h24v24H0z" fill="none" />
                    <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
                    <path
                      d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
                  </svg>
                </a>
              </span>
              <div class="invalid-feedback">请检查确认密码</div>
            </div>
          </div>
          <div class="mb-3">
            <label class="form-check">
              <input type="checkbox" class="form-check-input" id="policy" />
              <span class="form-check-label">同意 <a href="#" tabindex="-1">BAEKHYUN论坛使用条款和隐私政策</a>.</span>
            </label>
          </div>
          <div class="form-footer">
            <button type="button" class="btn btn-primary w-100" id="submit">注册</button>
          </div>
        </div>
      </form>
      <div class="text-center text-muted mt-3">
        我已有一个账户? <a href="./sign-in.html" tabindex="-1">登录</a>
      </div>
    </div>
  </div>
</body>
<!-- 导入JS -->
<script src="./dist/js/tabler.min.js"></script>
<script src="./dist/js/jquery-3.6.3.min.js"></script>
<script src="./dist/js/jquery.toast.js"></script>
<script>

$(function () {
  // 获取表单并校验
  $('#submit').click(function () {
    let checkForm = true;
    // 校验用户名
    if (!$('#username').val()) {
      $('#username').addClass('is-invalid');
      checkForm = false;
    }
    // 校验昵称
    if (!$('#nickname').val()) {
      $('#nickname').addClass('is-invalid');
      checkForm = false;
    }
    // 校验密码非空
    if (!$('#password').val()) {
      $('#password').addClass('is-invalid');
      checkForm = false;
    }
    // 校验确认密码非空, 校验密码与重复密码是否相同
    if (!$('#passwordRepeat').val() || $('#password').val() != $('#passwordRepeat').val()) {
      $('#passwordRepeat').addClass('is-invalid');
      checkForm = false;
    }

    // 检验政策是否勾选
    if (!$('#policy').prop('checked')) {
      $('#policy').addClass('is-invalid');
      checkForm = false;
    }
    // 根据判断结果提交表单
    if (!checkForm) {
      return false;
    }
    
    // 所有校验通过之后构造要发送的数据
    let 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){
        //根据code的值判断响应是否成功
        if(respData.code==0){
          //成功
          location.assign('sign-in.html');
        }else{
          //失败
          $.toast({
            heading:'失败',
            text:respData.message,
            icon:'warning'
          })
        }
      },
      error:function(){
        $.toast({
          heading:'错误',
          text:'访问网站出现问题,请与管理员联系',
          icon:'error'
        })
      }
    });
  });

  // 表单元单独检验
  $('#username, #nickname, #password').on('blur', function () {
    if ($(this).val()) {
      $(this).removeClass('is-invalid');
      $(this).addClass('is-valid');
    } else {
      $(this).removeClass('is-valid');
      $(this).addClass('is-invalid');
    }
  })

  // 检验确认密码
  $('#passwordRepeat').on('blur', function () {
    if ($(this).val() && $(this).val() == $('#password').val()) {
      $(this).removeClass('is-invalid');
      $(this).addClass('is-valid');
    } else {
      $(this).removeClass('is-valid');
      $(this).addClass('is-invalid');
    }
  })

  // 校验政策是否勾选
  $('#policy').on('change', function () {
    if ($(this).prop('checked')) {
      $(this).removeClass('is-invalid');
      $(this).addClass('is-valid');
    } else {
      $(this).removeClass('is-valid');
      $(this).addClass('is-invalid');
    }
  })


  // 密码框右侧明文密文切换按钮
  $('#passwordRepeat_a').click(function () {
    if($('#passwordRepeat').attr('type') == 'password') {
      $('#passwordRepeat').attr('type', 'text');
    } else {
      $('#passwordRepeat').attr('type', 'password');
    }
  });
  $('#password_a').click(function () {
    if($('#password').attr('type') == 'password') {
      $('#password').attr('type', 'text');
    } else {
      $('#password').attr('type', 'password');
    }
  });
});


</script>

</html>

4.3.2、登录(后端实现)

请求
// 请求
POST /user/login HTTP/ 1.1
Content-Type: application/x-www-form-urlencoded
username=xxx&password= xxx
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{ "code" : 0 , "message" : " 成功 " , "data" : null }
创建Service接口

在IUserService新增方法

    /**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    User login(String username,String password);
实现Service接口
    @Override
    public User login(String username, String password) {
        //1、非空校验
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2、查询用户是否存在
        User user=selectByUsername(username);
        //校验用户
        if (user==null){
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN.getMessage()));
        }
        //校验密码
        //获取用户的盐
        String salt=user.getSalt();
        //生成密文
        String ciphertext= MD5Utils.md5Salt(password,salt);
        //比较密文是否一致
        if (!ciphertext.toLowerCase().equals(user.getPassword().toLowerCase())){
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN.getMessage()));
        }
        //校验通过,返回user对象
        return user;
    }
对Service接口进行单元测试
    @Test
    void login() throws JsonProcessingException {
        User user=userService.login("baekhyun","1992");
        System.out.println(objectMapper.writeValueAsString(user));
    }

实现controller层
    @ApiOperation("用户登录")
    @PostMapping("/login")
    public AppResult<User> login(HttpServletRequest request,
                                 @ApiParam("用户名") @RequestParam("username") @NonNull String username,
                                 @ApiParam("密码") @RequestParam("password") @NonNull String password){
        //调用Service
        User user=userService.login(username,password);
        if (user==null){
            //返回错误
            return AppResult.failed(ResultCode.FAILED_LOGIN);
        }
        //获取session对象
        HttpSession session= request.getSession(true);  //没有的话创建一个
        //把用户信息设置到session中
        session.setAttribute(AppConfig.USER_SESSION_KEY,user);
        //返回结果
        return AppResult.success();
    }
测试接口

登录(前端实现)
<!doctype html>

<html lang="zh-CN">
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <link rel="shortcut icon" href="/favicon.ico">
    <!-- 标题 -->
    <title>BAEKHYUN论坛 - 用户登录</title>
    <!-- 导入CSS -->
    <link href="./dist/css/tabler.min.css?1674944402" rel="stylesheet"/>
    <link rel="stylesheet" href="./dist/css/jquery.toast.css">
    <!-- 设置字体 -->
    <!-- <style>
      @import url('https://rsms.me/inter/inter.css');
      :root {
      	--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
      }
      body {
      	font-feature-settings: "cv03", "cv04", "cv11";
      }
    </style> -->
  </head>
  <body class="d-flex flex-column">
    <!-- 正文 -->
    <div class="page page-center">
        <div class="container container-normal py-4">
          <div class="row align-items-center g-4">
            <div class="col-lg">
              <div class="container-tight">
                <div class="text-center mb-4">
                    <img src="./image/bit-forum-logo01.png" height="50" alt="">
                </div>
                <div class="card card-md">
                  <div class="card-body">
                    <h2 class="text-center mb-4">用户登录</h2>
                    <form id="signInForm" method="get" autocomplete="off" novalidate>
                      <div class="mb-3">
                        <label class="form-label required">用户名</label>
                        <input type="text" class="form-control" placeholder="请输入用户名" autocomplete="off" name="username" id="username">
                        <div class="invalid-feedback">用户名不能为空</div>
                      </div>
                      <div class="mb-2">
                        <label class="form-label required">
                          密码
                          <!-- <span class="form-label-description">
                            <a href="#">忘记密码</a>
                          </span> -->
                        </label>
                        <div class="input-group input-group-flat">
                          <input type="password" class="form-control"  placeholder="请输入密码"  autocomplete="off" name="password"
                          id="password">
                          <span class="input-group-text">
                            <a href="javascript:void(0);" id="password_a" class="link-secondary" title="显示密码" data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
                              <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" /></svg>
                            </a>
                          </span>
                          <div class="invalid-feedback">密码不能为空</div>
                        </div>
                      </div>
                      <!-- <div class="mb-2">
                        <label class="form-check">
                          <input type="checkbox" class="form-check-input"/>
                          <span class="form-check-label">记住我</span>
                        </label>
                      </div> -->
                      <div class="form-footer">
                        <button id="submit" type="button" class="btn btn-primary w-100">登录</button>
                      </div>
                    </form>
                  </div>
                </div>
                <div class="text-center text-muted mt-3">
                  还没有注册吗? <a href="./sign-up.html" tabindex="-1">点击注册</a>
                </div>
              </div>
            </div>
            <div class="col-lg d-none d-lg-block">
              <img src="./dist/illustrations/undraw_joyride_hnno.svg" height="300" class="d-block mx-auto" alt="">
            </div>
          </div>
        </div>
      </div>
  </body>
  <!-- 导入JS -->
  <script src="./dist/js/jquery-3.6.3.min.js"></script>
  <script src="./dist/js/tabler.min.js"></script>
  <script src="./dist/js/jquery.toast.js"></script>
  <script>
    $(function () {
      // 获取控件
      // 用户名
      let usernameEl = $('#username');
      let passwordEl = $('#password');
      // 登录校验
      $('#submit').click(function () {
        let checkForm = true;
        // 校验用户名
        if (!usernameEl.val()) {
          usernameEl.addClass('is-invalid');
          checkForm = false;
        }
        // 校验密码
        if (!passwordEl.val()) {
          passwordEl.addClass('is-invalid');
          checkForm = false;
        }

        // 根据判断结果提交表单
        if (!checkForm) {
          return false;
        }

        // 构造数据
        let postData={
          username:usernameEl.val(),
          password:passwordEl.val(),
        };
        
        // 发送AJAX请求,成功后跳转到index.html
        $.ajax({
          type:'POST',
          url:'user/login',
          contentType:'application/x-www-form-urlencoded',
          data:postData,
          //回调
          success:function(respData){
            //根据code的值判断响应是否成功
            if(respData.code==0){
              //成功
              location.assign('index.html');
            }else{
              //失败
              $.toast({
                heading:'失败',
                text:respData.message,
                icon:'warning'
              })
            }
          },
          error:function(){
            $.toast({
              heading:'错误',
              text:'访问网站出现问题,请与管理员联系',
              icon:'error'
            })
          }
        });
      });

      //
      // 表单元单独检验
      $('#username, #password').on('blur', function () {
        if ($(this).val()) {
          $(this).removeClass('is-invalid');
          $(this).addClass('is-valid');
        } else {
          $(this).removeClass('is-valid');
          $(this).addClass('is-invalid');
        }
      });

      // 显示密码
      $('#password_a').click(function () {
        if(passwordEl.attr('type') == 'password') {
          passwordEl.attr('type', 'text');
        } else {
          passwordEl.attr('type', 'password');
        }
      });
    });
  </script>
</html>

4.3.3、退出

  1. 用户访问退出接口。
  2. 服务器注销用户的会话(Session)。
  3. 返回成功或失败的消息。
  4. 如果返回成功,浏览器将跳转到相应的页面。
  5. 结束操作。
请求
// 请求
GET http: //127.0.0.1:58080/user/logout HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{ "code" : 0 , "message" : " 成功 " , "data" : null }
实现Controller层
    @ApiOperation("用户注销")
    @GetMapping("/logout")
    public AppResult logout(HttpServletRequest request){
        //1、获取session
        HttpSession session=request.getSession(false);
        //2、注销session
        if (session!=null){
            //销毁session
            session.invalidate();
        }
        //3、返回结果
        return AppResult.success("注销成功");
    }
测试接口

退出(前端页面) 
    // ============================ 处理退出登录点击事件 ===========================
    // 成功后,跳转到sign-in.html
    $('#index_user_logout').click(function () {
      $.ajax({
        type:'GET',
        url:'user/logout',
        //回调
        success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //跳转到成功界面
            location.assign('sign-in.html');
          }else{
            //失败
            $.toast({
              heading:'失败',
              text:respData.message,
              icon:'warning'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'访问网站出现问题,请与管理员联系',
            icon:'error'
          })
        }
      });
    });

4.3.4、个人中心

4.3.4.1、获取用户信息

根据用户的请求,服务器会根据是否传入Id参数来确定返回哪个用户的详细信息:

  1. 如果没有传入用户Id,服务器会返回当前登录用户的详细信息。
  2. 如果传入了用户Id,服务器会返回指定Id的用户详细信息。
请求
// 请求
GET /user/info HTTP/ 1.1
GET /user/info?id= 1 HTTP/ 1.1
响应
// 响应
HTTP/ 1.1 200
Content-type: applicatin/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : {
"id" : 25 ,
"username" : "user223" ,
"nickname" : "user223" ,
"phoneNum" : null ,
"email" : null ,
"gender" : 1 ,
"avatarUrl" : null ,
"articleCount" : 0 ,
"isAdmin" : 0 ,
"state" : 0 ,
"createTime" : "2023-04-08 15:06:10" ,
"updateTime" : "2023-04-08 15:06:10"
}
}
创建Service接口
    /**
     * 根据用户id查询用户信息
     * @param id
     * @return
     */
    User selectById(Long id);
实现Service接口
    @Override
    public User selectById(Long id) {
        //1、非空校验
        if (id==null || id<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //2、调用DAO
        User user=userMapper.selectByPrimaryKey(id);
        //3、返回结果
        return user;
    }
对Service接口进行单元测试
    @Test
    void selectById() throws JsonProcessingException {
        User user=userService.selectById(5L);
        System.out.println(objectMapper.writeValueAsString(user));
    }

实现controller层
    @ApiOperation("获取用户详情")
    @GetMapping("/info")
    public AppResult<User> getUserInfo(HttpServletRequest request,
                                       @ApiParam("用户Id") @RequestParam(value = "id",required = false) Long id){
        //定义一个返回的User对象
        User user;
        //校验Id是否为空,根据Id的值来处理不同的逻辑
        if (id==null){
            //从session中获取用户信息
            HttpSession session= request.getSession(false);
            user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        }else {
            //从数据库中查询用户信息
            user=userService.selectById(id);
        }
        //返回用户信息
        return AppResult.success(user);
    }
测试接口

不传入用户Id, 返回当前登录用户详情(需在登录状态下查询)


传入用户Id,返回指定Id的用户详情

修复返回值 

用户的敏感数据不能在网络上明文传输,并且日期的返回格式也有问题

在类上边的对应属性上加注解

    @ApiModelProperty("密码")
    @JsonIgnore//不参与JSON序列化
    private String password;

    //敏感信息不应在网络上传输
    @JsonIgnore//不参与JSON序列化
    private String salt;

    @ApiModelProperty("删除状态 0正常 1删除")
    @JsonIgnore//不参与JSON序列化
    private Byte deleteState;

在application.yml添加配置 

spring:
  #JSON序列化配置
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss  #日期格式
    default-property-inclusion: non_null  #不为null时序列化
前端代码
    //========================= 获取用户信息 =======================
    // 成功后,手动设置用户信息
    // $('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
    $.ajax({
      //请求方法
      type:'GET',
      //请求路径
      url:'user/info',
      //回调
      success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //获取用户对象
            let user=respData.data;
            //判断头像是否为空
            if(!user.avatarUrl){
              //为头像设置默认值
              user.avatarUrl=avatarUrl;
            }
            //成功后处理具体的逻辑
            //设置头像
            $('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
            //设置昵称
            $('#index_nav_nickname').html(user.nickname);
            //设置用户分类
            let subName=user.isAdmin==1?'管理员':"普通用户";
            $('#index_nav_name_sub').html(subName);
            //记录当前登录的用户
            currentUserId=user.id;
          }else{
            //失败
            $.toast({
              heading:'失败',
              text:respData.message,
              icon:'warning'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'访问网站出现问题,请与管理员联系',
            icon:'error'
          })
        }
    });
    // 构造查询用户信息的queryString
    let userInfoQueryString = '';
    if (profileUserId) {
      userInfoQueryString = '?id=' + profileUserId
    }
    // ============= 获取用户信息 =============
    // 成功时调用initProfileUserInfo()方法,初始化用户数据
    $.ajax({
      type:'GET',
      url:'user/info'+userInfoQueryString,
      // 回调
      success : function (respData) {
        // 根据code的值判断响应是否成功
        if (respData.code == 0) {
          //成功后构建初始化页面上的用户信息
          initProfileUserInfo(respData.data);
        } else {
          // 失败
          $.toast({
            heading: '失败',
            text: respData.message,
            icon: 'warning'
          })  
        }
      },
      error : function () {
        $.toast({
          heading: '错误',
          text: '访问网站出现问题,请与管理员联系',
          icon: 'error'
        }) 
      }
    });

    // ============= 设置Profile页面用户信息 ================
    function initProfileUserInfo(user) {
      console.log(user);
      // 默认头像路径
      if (!user.avatarUrl) {
        user.avatarUrl = avatarUrl;
      }
      console.log('currentUserId = '+currentUserId);
      // 站内信按钮
      if (user.id != currentUserId) {
        // 显示站内信按钮
        $('#div_profile_send_message').show();
        // 设置站内信目标用户信息
        $('#btn_profile_send_message').click(function() {
          setMessageReceiveUserInfo(user.id, user.nickname);
        });
      }
      // 设置用户ID
      $('#profile_user_id').val(user.id);
      // 设置头像
      $('#profile_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
      // 用户昵称
      $('#profile_nickname').html(user.nickname);
      // 发贴数
      $('#profile_articleCount').html(user.articleCount);
      // 邮箱
      if (user.email) {
        $('#profile_email').html(user.email);
      }
      // 注册日期
      $('#profile_createTime').html(user.createTime);
      // 个人介绍
      if (user.remark) {
        $('#profile_remark').html(user.remark);
      }
    }
    // ================= 获取用户详情,初始化页面内容 =================
    // 发送AJAX请求,成功时 调用initUserInfo方法,完成页面数据初始化
    $.ajax({
      type:'get',
      url:'user/info',
      //回调
      // 回调
      success : function (respData) {
          // 根据code的值判断响应是否成功
          if (respData.code == 0) {
            // 成功之后调用构建初始化页面上的用户信息的方法
            initUserInfo(respData.data);
          } else {
            // 失败
            $.toast({
              heading: '失败',
              text: respData.message,
              icon: 'warning'
            })
          }
        },
        error : function () {
          $.toast({
            heading: '错误',
            text: '访问网站出现问题,请与管理员联系',
            icon: 'error'
          });
        }
    });
  });

  // ================= 设置用户信息 =================
  function initUserInfo(user) {
    // 默认头像路径
    if (!user.avatarUrl) {
      user.avatarUrl = avatarUrl;
    }
    // 用户Id
    $('#settings_user_id').val(user.id);
    // title 昵称
    $('#settings_nickname').html(user.nickname);
    // 头像
    $('#settings_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
    // 昵称
    $('#setting_input_nickname').val(user.nickname);
    // 邮箱
    $('#setting_input_email').val(user.email);
    // 电话
    $('#setting_input_phoneNum').val(user.phoneNum);
    // 个人简历
    $('#settings_textarea_remark').html(user.remark);
  }
4.3.4.2、修改个人信息

只对用户的基本信息做修改,不包括密码与头像,修改密码与修改头像提供单独的修改接口

用户进入个人信息修改页面;用户输入要修改的信息并点击提交按钮;服务器接收到请求,获取用户的登录ID,并根据提交的信息更新数据库中的数据;服务器返回操作结果,指示修改是否成功。如果成功,同时返回更新后的个人信息;浏览器自动刷新以显示最新的个人信息。

请求
// 请求
POST http: //127.0.0.1:58080/user/modifyInfo HTTP/1.1
Content-Type: application/x-www-form-urlencoded
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : {
"id" : 1 ,
"username" : "xxx" ,
"nickname" : "xxx" ,
"phoneNum" : "xxx" ,
"email" : "xxx" ,
"gender" : 1 ,
"avatarUrl" : null ,
"articleCount" : 1 ,
"isAdmin" : 1 ,
"remark" : " xxx " ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
}
}
创建Service接口
    /**
     * 修改用户信息
     * @param user
     */
    void modifyInfo(User user);
实现Service接口
    @Override
    public void modifyInfo(User user) {
        //非空校验
        if (user==null || user.getId()==null || user.getId()<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }

        //查询用户基本信息
        User existUser=userMapper.selectByPrimaryKey(user.getId());
        if (existUser==null){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }

        //定义一个标识
        boolean checkParams=false;
        //定义一个更新对象
        User updateUser=new User();
        //设置Id
        updateUser.setId(user.getId());
        //更新时间
        updateUser.setUpdateTime(new Date());

        //处理username
        if (!StringUtils.isEmpty(user.getUsername())
                && !user.getUsername().equals(existUser.getUsername())){
            //如果username不为空,那么则需要更新
            //1、查询当前数据库中是否存在相同的用户名
            User checkUser=selectByUsername(user.getUsername());
            if (checkUser!=null){
                //用户名已存在,抛出异常
                throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));
            }
            //设置要更新用户名
            updateUser.setUsername(user.getUsername());
            //设置参数检查标识
            checkParams=true;
        }

        //处理昵称
        if (!StringUtils.isEmpty(user.getNickname())
                && !user.getNickname().equals(existUser.getNickname())){
            //如果nickname不为空,那么则需要更新
            updateUser.setNickname(user.getNickname());
            //设置参数检查标识
            checkParams=true;
        }

        //处理电话号码
        if (!StringUtils.isEmpty(user.getPhoneNum())
                && !user.getPhoneNum().equals(existUser.getPhoneNum())){
            //如果nickname不为空,那么则需要更新
            updateUser.setPhoneNum(user.getPhoneNum());
            //设置参数检查标识
            checkParams=true;
        }

        //处理邮箱
        if (!StringUtils.isEmpty(user.getEmail())
                && !user.getEmail().equals(existUser.getEmail())){
            //如果nickname不为空,那么则需要更新
            updateUser.setEmail(user.getEmail());
            //设置参数检查标识
            checkParams=true;
        }

        //处理个人简介
        if (!StringUtils.isEmpty(user.getRemark())
            && !user.getRemark().equals(existUser.getRemark())){
            //如果nickname不为空,那么则需要更新
            updateUser.setRemark(user.getRemark());
            //设置参数检查标识
            checkParams=true;
        }

        //处理性别
        if (user.getGender()!=null && user.getGender()!=existUser.getGender()){
            //设置要更新的值
            updateUser.setGender(user.getGender());
            //性别是否有效
            if (updateUser.getGender()<0 || updateUser.getGender()>2){
                //默认为2(保密)
                updateUser.setGender((byte) 2);
            }
            //设置参数检查标识
            checkParams=true;
        }

        //如果所有的参数都为空
        if (!checkParams){
            //打印日志
            log.warn("用户更新时,所有的参数都为空,user id="+user.getId());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }

        //调用DAO
        int row=userMapper.updateByPrimaryKeySelective(updateUser);
        //判断受影响的行数
        if (row!=1){
            log.warn("用户更新时,"+ResultCode.ERROR_SERVICES.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }
对Service接口进行单元测试
    @Test
    void modifyInfo() {
        //创建一个要修改的用户
        User user=new User();
        user.setId(3L);
        user.setUsername("krystal111");
        user.setNickname("krystal111");
        user.setPhoneNum("123456781");
        user.setEmail("111@qq.com");
        user.setGender((byte) 0);
        user.setRemark("我是krystal111");
        //调用Service
        userService.modifyInfo(user);
        System.out.println("修改个人信息成功");
    }

实现Controller层
    @ApiOperation("修改个人信息")
    @PostMapping("/modifyInfo")
    public AppResult modifyInfo(HttpServletRequest request,
                                @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)
                && StringUtils.isEmpty(email) && StringUtils.isEmpty(phoneNum)
                && StringUtils.isEmpty(remark) && gender==null){
            //返回错误描述
            return AppResult.failed("请输入要修改的内容");
        }
        //获取当前登录的用户信息
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //构造更新对象
        User updateUser=new User();
        updateUser.setId(user.getId());  //id
        updateUser.setUsername(username);  //用户名
        updateUser.setNickname(nickname);  //昵称
        updateUser.setGender(gender);  //性别
        updateUser.setPhoneNum(phoneNum);  //电话号
        updateUser.setEmail(email);  //邮箱
        updateUser.setRemark(remark);  //个人简介
        //调用Service
        userService.modifyInfo(updateUser);
        //获取数据库中最新的user信息
        user=userService.selectById(user.getId());
        //更新session中的user对象
        session.setAttribute(AppConfig.USER_SESSION_KEY,user);
        //返回结果
        return AppResult.success(user);
    }
测试接口

前端代码 
  // ================= 封装ajax请求 =================
  function changeUserInfo(userInfo, type) {
    // 校验用户信息
    if (!userInfo) {
      $.toast({
        heading: '提示',
        text: '请检查要修改的内容是否正确或联系管理员',
        icon: 'info'
      });
      return;
    }

    // 构造query string
    let searchParams = new URLSearchParams(userInfo);
    let queryString = '?' + searchParams.toString();
    console.log(queryString);

    // 发送请求,提示响应结果
    $.ajax({
      type:'POST',
      url:'user/modifyInfo',
      contentType : 'application/x-www-form-urlencoded',
      data:userInfo,
      // 回调
      success : function (respData) {
        // 根据code的值判断响应是否成功
        if (respData.code == 0) {
            let user = respData.data;
            // 修改页面昵称
            if (user.nickname) {
              // 当前页面
              $('#settings_nickname').html(user.nickname);
              // 导航栏
              $('#index_nav_nickname').html(user.nickname);
            }
          // 成功之后弹出提示框
          $.toast({
            heading: '成功',
            text: respData.message,
            icon: 'success'
          })
        } else {
          // 失败
          $.toast({
            heading: '失败',
            text: respData.message,
            icon: 'warning'
          })
        }
      },
      error : function () {
        $.toast({
          heading: '错误',
          text: '访问网站出现问题,请与管理员联系',
          icon: 'error'
        });
      }
    });

  }

  // ================= 修改用户昵称 =================
  $('#setting_submit_nickname').click(function(){
    // 获取值
    let nicknameEl = $('#setting_input_nickname');
    // 校验
    if(!nicknameEl.val()) {
      nicknameEl.focus();
      return false;
    }
    // 构造数据
    let nicknameObj = {
      id : $('#settings_user_id').val(),
      nickname : nicknameEl.val()
    }
    // 发送请求
    changeUserInfo(nicknameObj);
  });

  // ================= 修改邮箱 =================
  $('#setting_submit_email').click(function(){
    // 获取值
    let emailEl = $('#setting_input_email');
    // 校验
    if(!emailEl.val()) {
      emailEl.focus();
      return false;
    }
    // 构造数据
    let emailObj = {
      id : $('#settings_user_id').val(),
      email : emailEl.val()
    }
    // 发送请求
    changeUserInfo(emailObj);
  });

  // ================= 修改电话 =================
  $('#setting_submit_phoneNum').click(function(){
    // 获取值
    let phoneNumEl = $('#setting_input_phoneNum');
    // 校验
    if(!phoneNumEl.val()) {
      phoneNumEl.focus();
      return false;
    }
    // 构造数据
    let phoneNumObj = {
      id : $('#settings_user_id').val(),
      phoneNum : phoneNumEl.val()
    }
    // 发送请求
    changeUserInfo(phoneNumObj);
  });

  
  // ================= 修改个人介绍 =================
  $('#settings_submit_remark').click(function(){
    // 获取值
    let remarkEl = $('#settings_textarea_remark');
    // 校验
    if(!remarkEl.val()) {
      remarkEl.focus();
      return false;
    }
    // 构造数据
    let remarkObj = {
      id : $('#settings_user_id').val(),
      remark : remarkEl.val()
    }
    // 发送请求
    changeUserInfo(remarkObj);
  });
4.3.4.3、修改密码

为修改密码提供⼀个单独的接口及操作页面

用户打开修改密码页面,然后输入原密码、新密码和重复新密码,并提交给服务器。服务器会验证输入的原密码是否正确,如果验证通过,就会更新密码并返回成功的消息;如果验证不通过,就会返回失败的消息。

请求
// 请求
POST http: //127.0.0.1:58080/user/modifyPwd HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id= 1 &oldPassword= 123456 &newPassword= 123456 &passwordRepeat= 123456
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
    /**
     * 修改用户密码
     * @param id
     * @param newPassword
     * @param oldPassword
     */
    void modifyPassword(Long id,String newPassword,String oldPassword);
实现Service接口
    @Override
    public void modifyPassword(Long id, String newPassword, String oldPassword) {
        //非空校验
        if (id==null || id<=0 || StringUtils.isEmpty(newPassword)
            || StringUtils.isEmpty(oldPassword)){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //1、查询用户详情
        User user=selectById(id);
        //2、校验用户是否存在
        if (user==null || user.getDeleteState()==1){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        //3、根据用户的盐和传入密码计算出密码的密文
        String encryptOldPassword=MD5Utils.md5Salt(oldPassword,user.getSalt());
        //4、用密文与数据库中的password字段作比较,如果相同表示密码校验通过
        if (!encryptOldPassword.equalsIgnoreCase(user.getPassword())){
            //抛出异常
            throw new ApplicationException(AppResult.failed("原密码错误"));
        }
        //5、生成一个新的盐
        String salt= UUIDUtils.UUID_32();
        //6、根据新密码与新盐计算出新密码的密文
        String encryptPassword=MD5Utils.md5Salt(newPassword,salt);
        //7、构造更新对象
        User updateUser=new User();
        updateUser.setId(user.getId());  //用户Id
        updateUser.setSalt(salt);  //盐
        updateUser.setPassword(encryptPassword);  //新密码的密文
        updateUser.setUpdateTime(new Date());  //更新时间
        //8、调用DAO
        int row=userMapper.updateByPrimaryKeySelective(updateUser);
        //判断受影响的行数
        if (row!=1){
            log.warn("用户更新密码时,"+ResultCode.ERROR_SERVICES.toString());
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }
对Service接口进行单元测试
    @Test
    void modifyPassword() {
        userService.modifyPassword(3L,"1999","1994");
        System.out.println("修改密码成功");
    }

实现Controller层
    @ApiOperation("更新密码")
    @PostMapping("/modifyPwd")
    public AppResult modifyPassword(HttpServletRequest request,
                                    @ApiParam("原密码") @RequestParam("oldPassword") @NonNull String oldPassword,
                                    @ApiParam("新密码") @RequestParam("newPassword") @NonNull String newPassword,
                                    @ApiParam("确认密码") @RequestParam("passwordRepeat") @NonNull String passwordRepeat){
        //1、校验新密码与确认密码是否相同
        if (!newPassword.equals(passwordRepeat)){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
        }
        //2、获取当前登录的用户信息
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //3、调用Service
        userService.modifyPassword(user.getId(), newPassword,oldPassword);
        //4、销毁session
        if (session!=null){
            session.invalidate();
        }
        //5、返回结果
        return AppResult.success();
    }
测试接口

前端代码
<!-- Page header -->
<div class="page-header d-print-none">
  <div class="container-xl">
    <div class="row g-2 align-items-center">
      <div class="col">
        <h2 class="page-title">
          用户中心
        </h2>
      </div>
    </div>
  </div>
</div>
<!-- Page body -->
<div class="page-body">
  <div class="container-xl">
    <div class="card">
      <div class="row g-0">
        <div class="col-3 d-none d-md-block border-end">
          <div class="card-body">
            <div class="list-group list-group-transparent">
              <a href="javascript:void(0);"
                class="list-group-item list-group-item-action d-flex align-items-center active">我的账户</a>
              <!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">修改密码</a>
                      <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">个人简介</a> -->
            </div>
          </div>
        </div>
        <div class="col d-flex flex-column">
          <div class="card-body">
            <h2 id="settings_nickname" class="mb-4">比特鑫哥</h2>
            <input type="text" style="display: none;" id="settings_user_id">
            <div class="row align-items-center">
              <div class="col-auto">
                <a id="settings_avatar" class="avatar avatar-xl" style="background-image: url(./image/avatar02.jpeg)"
                  onclick="openFileDialog()"></a>
                <!-- 文件选择 -->
                <input type="file" class="form-control" style="display: none;" id="settings_input_chooiceAvatar">
              </div>

              <div class="col-auto"><a href="javascript:void(0);" class="btn" onclick="openFileDialog()">
                  修改头像
                </a>

              </div>
            </div>
            <h3 class="card-title mt-4">昵称</h3>
            <div class="row">
              <div class="col-9">
                <input id="setting_input_nickname" type="text" class="form-control">
                <div class="invalid-feedback">昵称不能为空</div>
              </div>
              <div class="col-3">
                <a id="setting_submit_nickname" href="javascript:void(0)" class="btn">
                  修 改
                </a>
              </div>
            </div>
            <hr>
            <h3 class="card-title mt-4">邮箱地址</h3>
            <div>
              <div class="row g-3">
                <div class="col-9">
                  <input id="setting_input_email" type="text" class="form-control">
                  <div class="invalid-feedback">邮箱地址不能为空</div>
                </div>
                <div class="col-3">
                  <a id="setting_submit_email" href="javascript:void(0)" class="btn">
                    修 改
                  </a>
                </div>
              </div>
            </div>
            <hr>
            <h3 class="card-title mt-4">电话号码</h3>
            <div>
              <div class="row g-3">
                <div class="col-9">
                  <input id="setting_input_phoneNum" type="text" class="form-control">
                  <div class="invalid-feedback">电话号码不能为空</div>
                </div>
                <div class="col-3">
                  <a id="setting_submit_phoneNum" href="javascript:void(0);" class="btn">
                    修 改
                  </a>
                </div>
              </div>
            </div>
            <hr>
            <h3 class="card-title mt-4">修改密码</h3>
            <div>
              <div class="row g-3">
                <!-- 表单 -->
                <div class="col-9">
                  <form autocomplete="off" novalidate>
                    <div class="mb-2">
                      <label class="form-label required">
                        原密码
                      </label>
                      <div class="input-group input-group-flat">
                        <input id="settings_input_oldPassword" type="password" class="form-control" placeholder="请输入密码" autocomplete="off">
                        <span class="input-group-text">
                          <a href="#" class="link-secondary" title="显示密码"
                            data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
                            <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
                              viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
                              stroke-linecap="round" stroke-linejoin="round">
                              <path stroke="none" d="M0 0h24v24H0z" fill="none" />
                              <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
                              <path
                                d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
                            </svg>
                          </a>
                        </span>
                        <div class="invalid-feedback">原密码不能为空</div>
                      </div>
                    </div>

                    <div class="mb-2">
                      <label class="form-label required">
                        新原密码
                      </label>
                      <div class="input-group input-group-flat">
                        <input id="settings_input_newPassword" type="password" class="form-control" placeholder="请输入密码" autocomplete="off">
                        <span class="input-group-text">
                          <a href="#" class="link-secondary" title="显示密码"
                            data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
                            <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
                              viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
                              stroke-linecap="round" stroke-linejoin="round">
                              <path stroke="none" d="M0 0h24v24H0z" fill="none" />
                              <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
                              <path
                                d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
                            </svg>
                          </a>
                        </span>
                        <div class="invalid-feedback">新密码不能为空</div>
                      </div>
                    </div>
                    <div class="mb-2">
                      <label class="form-label required">
                        确认密码
                      </label>
                      <div class="input-group input-group-flat">
                        <input id="settings_input_passwordRepeat" type="password" class="form-control" placeholder="请输入密码" autocomplete="off">
                        <span class="input-group-text">
                          <a href="#" class="link-secondary" title="显示密码"
                            data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
                            <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
                              viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
                              stroke-linecap="round" stroke-linejoin="round">
                              <path stroke="none" d="M0 0h24v24H0z" fill="none" />
                              <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
                              <path
                                d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
                            </svg>
                          </a>
                        </span>
                        <div class="invalid-feedback">确认密码不能为空</div>
                      </div>
                    </div>

                    <div class="form-footer">
                      <button id="settings_submit_password" type="button" class="btn btn-outline-danger w-100">提交修改</button>
                    </div>
                  </form>
                </div>
              </div>
            </div>
            <hr>
            <h3 class="card-title mt-4">个人简介</h3>
            <div class="row">
              <div class="col-9">
                <textarea id="settings_textarea_remark" class="form-control" placeholder="写点自我介绍,可以让朋友们了解你..."
                  rows="5"></textarea>
                <div class="invalid-feedback">个人简介不能为空</div>
              </div>
              <div class="col-3">
                <a href="javascript:void(0);" class="btn" id="settings_submit_remark">
                  修 改
                </a>
              </div>
            </div>
          </div>

        </div>
      </div>
    </div>
  </div>
</div>
<script>
  $(function() {
    // ================= 获取用户详情,初始化页面内容 =================
    // 发送AJAX请求,成功时 调用initUserInfo方法,完成页面数据初始化
    $.ajax({
      type : 'get',
      url: 'user/info',
      // 回调
      success : function (respData) {
          // 根据code的值判断响应是否成功
          if (respData.code == 0) {
            // 成功之后调用初始化页面上的用户信息
            initUserInfo(respData.data);
          } else {
            // 失败
            $.toast({
              heading: '失败',
              text: respData.message,
              icon: 'warning'
            })
          }
        },
        error : function () {
          $.toast({
            heading: '错误',
            text: '访问网站出现问题,请与管理员联系',
            icon: 'error'
          });
        }
    });
  });

  // ================= 设置用户信息 =================
  function initUserInfo(user) {
    // 默认头像路径
    if (!user.avatarUrl) {
      user.avatarUrl = avatarUrl;
    }
    // 用户Id
    $('#settings_user_id').val(user.id);
    // title 昵称
    $('#settings_nickname').html(user.nickname);
    // 头像
    $('#settings_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
    // 昵称
    $('#setting_input_nickname').val(user.nickname);
    // 邮箱
    $('#setting_input_email').val(user.email);
    // 电话
    $('#setting_input_phoneNum').val(user.phoneNum);
    // 个人简历
    $('#settings_textarea_remark').html(user.remark);
  }

  // ================= 封装ajax请求 =================
  function changeUserInfo(userInfo, type) {
    // 校验用户信息
    if (!userInfo) {
      $.toast({
        heading: '提示',
        text: '请检查要修改的内容是否正确或联系管理员',
        icon: 'info'
      });
      return;
    }


    // 定义接口路径
    let userURL = 'user/modifyInfo';
    if (type == 1) {
      userURL = 'user/modifyInfo';
    } else if (type == 2) {
      userURL = 'user/modifyPwd';
    }

    // 发送请求,提示响应结果
    $.ajax({
      type : 'POST',
      url : userURL,
      contentType : 'application/x-www-form-urlencoded',
      data : userInfo, 
      // 回调
      success : function (respData) {
          // 根据code的值判断响应是否成功
          if (respData.code == 0) {
            let user = respData.data;
            //修改密码之后跳转到登录页面
            if(type==2){
              location.assign("sign-in.html");
              return;
            }
            // 修改页面昵称
            if (user.nickname) {
              // 当前页面
              $('#settings_nickname').html(user.nickname);
              // 导航栏
              $('#index_nav_nickname').html(user.nickname);
            }
            // 成功之后弹出提示框
            $.toast({
              heading: '成功',
              text: respData.message,
              icon: 'success'
            })
            
          } else {
            // 失败
            $.toast({
              heading: '失败',
              text: respData.message,
              icon: 'warning'
            })
          }
        },
        error : function () {
          $.toast({
            heading: '错误',
            text: '访问网站出现问题,请与管理员联系',
            icon: 'error'
          });
        }
    });

  }

  // ================= 处理选择头像事件 =================
  function openFileDialog () {
    // 触发选择文件按钮的点击事件
    $('#settings_input_chooiceAvatar').click();
  }

  // ================= 修改用户昵称 =================
  $('#setting_submit_nickname').click(function(){
    // 获取值
    let nicknameEl = $('#setting_input_nickname');
    // 校验
    if(!nicknameEl.val()) {
      nicknameEl.focus();
      return false;
    }
    // 构造数据
    let nicknameObj = {
      nickname : nicknameEl.val()
    }
    // 发送请求
    changeUserInfo(nicknameObj);
  });

  // ================= 修改邮箱 =================
  $('#setting_submit_email').click(function(){
    // 获取值
    let emailEl = $('#setting_input_email');
    // 校验
    if(!emailEl.val()) {
      emailEl.focus();
      return false;
    }
    // 构造数据
    let emailObj = {
      email : emailEl.val()
    }
    // 发送请求
    changeUserInfo(emailObj);
  });

  // ================= 修改电话 =================
  $('#setting_submit_phoneNum').click(function(){
    // 获取值
    let phoneNumEl = $('#setting_input_phoneNum');
    // 校验
    if(!phoneNumEl.val()) {
      phoneNumEl.focus();
      return false;
    }
    // 构造数据
    let phoneNumObj = {
      phoneNum : phoneNumEl.val()
    }
    // 发送请求
    changeUserInfo(phoneNumObj);
  });

  
  // ================= 修改个人介绍 =================
  $('#settings_submit_remark').click(function(){
    // 获取值
    let remarkEl = $('#settings_textarea_remark');
    // 校验
    if(!remarkEl.val()) {
      remarkEl.focus();
      return false;
    }
    // 构造数据
    let remarkObj = {
      remark : remarkEl.val()
    }
    // 发送请求
    changeUserInfo(remarkObj);
  });

  // ================= 修改密码 =================
  $('#settings_submit_password').click(function() {
    // 获取值
    let oldPasswordEl = $('#settings_input_oldPassword');
    // 校验
    if(!oldPasswordEl.val()) {
      oldPasswordEl.focus();
      return false;
    }
    // 获取值
    let newPasswordEl = $('#settings_input_newPassword');
    // 校验
    if(!newPasswordEl.val()) {
      newPasswordEl.focus();
      return false;
    }
    // 获取值
    let passwordRepeatEl = $('#settings_input_passwordRepeat');
    // 校验
    if(!passwordRepeatEl.val()) {
      passwordRepeatEl.focus();
      return false;
    }

    // 两次输入的密码是否相同
    if (newPasswordEl.val() != passwordRepeatEl.val()) {
      $.toast({
        heading: '提示',
        text: '两次输入的密码不相同',
        icon: 'warning'
      });
      // 获取焦点
      passwordRepeatEl.focus();
      return false;
    }

    // 构造数据
    let  passwrodObj = {
      oldPassword : oldPasswordEl.val(),
      newPassword : newPasswordEl.val(),
      passwordRepeat : passwordRepeatEl.val()

    }
    // 发送请求
    changeUserInfo(passwrodObj, 2);
    // 清空输入框
    oldPasswordEl.val('');
    newPasswordEl.val('');
    passwordRepeatEl.val('');
  });
  
</script>

4.3.5、版块信息

4.3.5.1、获取在首页中显示的版块

        在首页显示的区块信息,提供了一个独立的接口,可以查询前N条记录,用来控制首页中区块的数量。当用户访问首页时,服务器会查询所有有效的版块,并按照排序字段进行排序,然后返回一个版块集合。

请求
// 请求
GET http: //127.0.0.1:58080/board/topList HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : [
{
"id" : 1 ,
"name" : "Java" ,
"articleCount" : 5 ,
"sort" : 1 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 2 ,
"name" : "C++" ,
"articleCount" : 1 ,
"sort" : 2 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 3 ,
"name" : " 前端技术 " ,
"articleCount" : 0 ,
"sort" : 3 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 4 ,
"name" : "MySQL" ,
"articleCount" : 0 ,
"sort" : 4 ,
"state" : 0 ,
"createTime "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 5 ,
"name" : " ⾯试宝典 " ,
"articleCount" : 0 ,
"sort" : 5 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 6 ,
"name" : " 经验分享 " ,
"articleCount" : 0 ,
"sort" : 6 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 7 ,
"name" : " 灌⽔区 " ,
"articleCount" : 0 ,
"sort" : 9 ,
"state" : 0 ,
"createTime" : "2023-01-25 13:26:12" ,
"updateTime" : "2023-01-25 13:26:12"
}
]
}
扩展Mapper.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.example.forum_system.dao.BoardMapper">
  <!-- 查询首页的版块列表 -->
  <select id="selectByNum" resultMap="BaseResultMap" parameterType="java.lang.Integer">
    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>
修改DAO
    /**
     * 查询首页的版块列表
     * @param num
     * @return
     */
    List<Board> selectByNum(@Param("num") Integer num);
创建Service接口
public interface IBoardService {

    /**
     * 查询首页的版块列表
     * @param num
     * @return
     */
    List<Board> selectByNum(Integer num);
}
实现Service接口
@Slf4j
@Service
public class BoardServiceImpl implements IBoardService {

    @Resource
    private BoardMapper boardMapper;

    @Override
    public List<Board> selectByNum(Integer num) {
        //非空校验
        if (num==null || num<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //调用DAO
        List<Board> boards=boardMapper.selectByNum(num);
        //返回结果
        return boards;
    }
}
对Service接口进行单元测试
@SpringBootTest
class BoardServiceImplTest {

    @Resource
    private IBoardService boardService;

    @Resource
    private ObjectMapper objectMapper;

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

application.yml添加配置
#项目自定义配置
forum_system:
  login:
    url: sign-in.html  #未登录状态下强制跳转页面
  index:
    board-num: 9  #首页中显示的版块个数
实现Controller层
@Api(tags = "版块接口")
@Slf4j
@RestController
@RequestMapping("/board")
public class BoardController {

    //从配置文件中获取主页中显示的版块个数,默认为9
    @Value("${forum_system.index.board-num:9}")
    private Integer indexBoardNum;

    @Resource
    private IBoardService boardService;

    @ApiOperation("获取首页中的版块列表")
    @GetMapping("/topList")
    public AppResult<List<Board>> topList(){
        //直接调用Service
        List<Board> boards=boardService.selectByNum(indexBoardNum);
        //返回结果
        return AppResult.success(boards);
    }
}
测试接口

需在登录状态下进行查询,否则会直接跳转到登录页

前端代码
    // ========================= 获取版块信息 =======================
    // 成功后,调用buildTopBoard()方法,构建版块列表
    $.ajax({
      type:'GET',
      url:'board/topList',
      //回调
      success:function(respData){
        //根据code的值判断响应是否成功
        if(respData.code==0){
          //成功之后调用构建版块列表的方法
          buildTopBoard(respData.data);
        }else{
          //失败
          $.toast({
            heading:'失败',
            text:respData.message,
            icon:'warning'
          })
        }
      },
      error:function(){
        $.toast({
          heading:'错误',
          text:'访问网站出现问题,请与管理员联系',
          icon:'error'
        })
      }
    });
4.3.5.2、获取指定版块信息 

客户端发送请求传入版块Id,服务器响应对应版本的详情

请求

// 请求

GET http://127.0.0.1:58080/board/getById?id=1 HTTP/1.1

响应

// 响应

HTTP/1.1 200

Content-Type: application/json

{

"code": 0,

"message": "成功",

"data": {

"id": 1,

"name": "Java",

"articleCount": 5,

"sort": 1,

"state": 0,

"createTime": "xxx",

"updateTime": "xxx"

}

}

创建Service接口
    /**
     * 根据id查询版块信息
     * @param id
     * @return
     */
    Board selectById(Long id);
实现Service接口
    @Override
    public Board selectById(Long id) {
        //非空校验
        if (id==null || id<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //调用DAO
        Board board=boardMapper.selectByPrimaryKey(id);
        //返回结果
        return board;
    }
对Service接口进行单元测试
    @Test
    void selectById() throws JsonProcessingException {
        Board board=boardService.selectById(1L);
        System.out.println(objectMapper.writeValueAsString(board));
    }

实现Controller层
    @ApiOperation("获取指定版块详情")
    @GetMapping("/getById")
    public AppResult<Board> getBoardInfo(@ApiParam("版块Id") @RequestParam("id") @NonNull Long id){
        //调用Service
        Board board=boardService.selectById(id);
        return AppResult.success(board);
    }
测试接口

前端代码
    // ========================= 获取版块信息 =======================
    // 
    function getBoardInfo (boardId) {
      if (!boardId) {
        return;
      }
      // 发送请求, 成功后,显示版块相关信息
      $.ajax({
        type:'GET',
        url:'board/getById?id='+boardId,
        //回调
        success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //成功时设置相应的标签
            let board=respData.data;
            //设置版块名
            $('#article_list_board_title').html(board.name);
            //设置版块中的帖子数量
            $('#article_list_count_board').html("帖子数量:"+board.articleCount);
          }else{
            //失败
            $.toast({
              heading:'失败',
              text:respData.message,
              icon:'warning'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'访问网站出现问题,请与管理员联系',
            icon:'error'
          })
        }
      });
    }

4.3.6、帖子列表

4.3.6.1、版块帖子列表

当用户点击某个版块或首页时,将版块ID作为参数发送到服务器;服务器收到请求后,会获取版块ID,并查询该版块下的所有帖子,对应版块中显示的帖子列表以发布时间降序排列;不传入版块Id返回所有帖子;然后将查询结果返回给用户。

请求
// 请求
// 返回指定版块下的帖⼦列表
GET http: //127.0.0.1:58080/article/getAllByBoardId?boardId=1 HTTP/1.1
// 返回所有的帖⼦列表
GET http: //127.0.0.1:58080/article/getAllByBoardId HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : [
{
"id" : 1 ,
"boardId" : 1 ,
"userId" : 1 ,
"title" : " 测试删除 " ,
"visitCount" : 8 ,
"replyCount" : 1 ,
"likeCount" : 1 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx" ,
"board" : {
"id" : 1 ,
"name" : "Java"
},
"user" : {
"id" : 1 ,
"nickname" : "xxx" ,
"phoneNum" : null ,
"email" : null ,
"gender" : 1 ,
"avatarUrl" : null
},
"own" : false
}
}
修改Article实体类
    //添加关联对象
    private User user;

    //添加关联对象
    private Board board;
扩展Mapper.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.example.forum_system.dao.ArticleMapper">
    <!--自定义结果集,设置User对象和Board对象的映射关系 -->
    <resultMap id="AllInfoResultMap" type="com.example.forum_system.model.Article" extends="ResultMapWithBLOBs">
        <!-- 关联的User对象映射关系 -->
        <association property="user" resultMap="com.example.forum_system.dao.UserMapper.BaseResultMap" columnPrefix="u_" />
        <!-- 关联的Board对象映射关系 -->
        <association property="board" resultMap="com.example.forum_system.dao.BoardMapper.BaseResultMap" columnPrefix="b_" />
    </resultMap>

    <!-- 查询所有帖子 -->
    <select id="selectAll" resultMap="AllInfoResultMap">
        select
            u.id as u_id,
            u.nickname as u_nickname,
            u.avatarUrl as u_avatarUrl,
            u.gender as u_gender,
            u.state as u_state,
            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 a, t_board b, t_user u
        where a.boardId = b.id
          and a.userId = u.id
          and a.deleteState = 0
        order by a.createTime desc
    </select>

    <!--  根据版块Id查询帖子列表  -->
    <select id="selectByBoardId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">
        select
            u.id as u_id,
            u.nickname as u_nickname,
            u.avatarUrl as u_avatarUrl,
            u.gender as u_gender,
            u.state as u_state,
            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 a, t_user u
        where a.userId = u.id
          and a.deleteState = 0
          and a.boardId = #{boardId,jdbcType=BIGINT}
        order by a.createTime desc
    </select>
</mapper>
修改DAO
    /**
     * 获取所有的帖子的集合
     * @return
     */
    List<Article> selectAll();

    /**
     * 根据版块Id查询帖子列表
     * @param boardId
     * @return
     */
    List<Article> selectByBoardId(@Param("boardId") Long boardId);
创建Service接口
public interface IArticleService {

    /**
     * 获取所有的帖子的集合
     * @return
     */
    List<Article> selectAll();

    /**
     * 根据版块Id查询帖子列表
     * @param boardId
     * @return
     */
    List<Article> selectByBoardId(Long boardId);
}
实现Service接口
@Service
@Slf4j
public class ArticleServiceImpl implements IArticleService {

    @Resource
    private ArticleMapper articleMapper;

    @Override
    public List<Article> selectAll() {
        //直接调用DAO
        List<Article> articles=articleMapper.selectAll();
        //返回结果
        return articles;
    }

    @Override
    public List<Article> selectByBoardId(Long boardId) {
        //非空校验
        if (boardId==null || boardId<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //调用DAO
        List<Article> articles=articleMapper.selectByBoardId(boardId);
        //返回结果
        return articles;
    }
}
对Service接口进行单元测试
@SpringBootTest
class ArticleServiceImplTest {

    @Resource
    private IArticleService articleService;

    @Resource
    private ObjectMapper objectMapper;

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

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

        articles=articleService.selectByBoardId(2L);
        System.out.println(objectMapper.writeValueAsString(articles));
    }
}

实现Controller层
@Slf4j
@Api(tags = "帖子接口")
@RestController
@RequestMapping("/article")
public class ArticleController {

    @Resource
    private IArticleService articleService;

    /**
     * 获取帖子列表
     * @param boardId
     * @return
     */
    @ApiOperation("获取帖子列表")
    @GetMapping("/getAllByBoardId")
    public AppResult<List<Article>> getAllByBoardId(@ApiParam("版块Id") @RequestParam(value = "boardId",required = false) Long boardId){
        //定义要返回的结果
        List<Article> results;
        if (boardId==null){
            //1、boardId为空时,获取所有的帖子
            results=articleService.selectAll();
        }else {
            //2、boardId不为空时,获取指定版块下的帖子
            results=articleService.selectByBoardId(boardId);
        }
        //防止返回的结果是null
        if (results==null){
            results=new ArrayList<>();
        }
        //返回结果
        return AppResult.success(results);
    }
}
测试接口

前端代码
    // ========================= 获取帖子列表 =======================
    // 成功后,调用listBuildArticleList()方法,构建帖子列表
    $.ajax({
      type:'GET',
      url:'article/getAllByBoardId'+queryString,
      //回调
      success:function(respData){
        //根据code的值判断响应是否成功
        if(respData.code==0){
          //成功时构建帖子列表
          listBuildArticleList(respData.data);
        }else{
          //失败
          $.toast({
            heading:'失败',
            text:respData.message,
            icon:'warning'
          })
        }
      },
      error:function(){
        $.toast({
          heading:'错误',
          text:'访问网站出现问题,请与管理员联系',
          icon:'error'
        })
      }
    });

    // ========================= 构造帖子列表 =======================
    function listBuildArticleList(data) {
      if(data.length == 0) {
        $('#artical-items-body').html('还没有帖子');
        return;
      }
      // 默认头像路径
      let avatarUrl = 'image/avatar01.jpeg';
      // 遍历结果
      data.forEach(article => {
        // 设置默认头像
        if (!article.user.avatarUrl) {
          article.user.avatarUrl = avatarUrl;
        }
        // 构造HTML
        let articleHtmlStr = '<div>'
          + ' <div class="row">'
          + ' <div class="col-auto">'
          + ' <span class="avatar" style="background-image: url(' + article.user.avatarUrl + ')"></span>'
          + ' </div>'
          + ' <div class="col">'
          + ' <div class="text-truncate">'
          + ' <a href="javascript:void(0);" class="article_list_a_title">'
          + ' <strong>' + article.title + '</strong>'
          + ' </a>'
          + ' </div>'
          + ' <div class="text-muted mt-2">'
          + ' <div class="row">'
          + ' <div class="col">'
          + ' <ul class="list-inline list-inline-dots mb-0">'
          + ' <li class="list-inline-item">'
          + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user"'
          + ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'
          + ' fill="none" stroke-linecap="round" stroke-linejoin="round">'
          + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
          + ' <path d="M12 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path>'
          + ' <path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>'
          + ' </svg> '
          + article.user.nickname
          + ' </li>'
          + ' <li class="list-inline-item">'
          + ' <svg xmlns="http://www.w3.org/2000/svg"'
          + ' class="icon icon-tabler icon-tabler-clock-edit" width="24" height="24"'
          + ' viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"'
          + ' stroke-linecap="round" stroke-linejoin="round">'
          + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
          + ' <path d="M21 12a9 9 0 1 0 -9.972 8.948c.32 .034 .644 .052 .972 .052"></path>'
          + ' <path d="M12 7v5l2 2"></path>'
          + ' <path d="M18.42 15.61a2.1 2.1 0 0 1 2.97 2.97l-3.39 3.42h-3v-3l3.42 -3.39z">'
          + ' </path>'
          + ' </svg> '
          + article.createTime
          + ' </li>'
          + ' </ul>'
          + ' </div>'
          + ' <div class="col-auto d-none d-md-inline">'
          + ' <ul class="list-inline list-inline-dots mb-0">'
          + ' <li class="list-inline-item">'
          + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-eye"'
          + ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'
          + ' fill="none" stroke-linecap="round" stroke-linejoin="round">'
          + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
          + ' <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>'
          + ' <path'
          + ' d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7">'
          + ' </path>'
          + ' </svg> '
          + article.visitCount
          + ' </li>'
          + ' <li class="list-inline-item">'
          + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-heart"'
          + ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'
          + ' fill="none" stroke-linecap="round" stroke-linejoin="round">'
          + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
          + ' <path'
          + ' d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572">'
          + ' </path>'
          + ' </svg> '
          + article.likeCount
          + ' </li>'
          + ' <li class="list-inline-item">'
          + ' <svg xmlns="http://www.w3.org/2000/svg"'
          + ' class="icon icon-tabler icon-tabler-message-circle" width="24" height="24"'
          + ' viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"'
          + ' stroke-linecap="round" stroke-linejoin="round">'
          + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
          + ' <path d="M3 20l1.3 -3.9a9 8 0 1 1 3.4 2.9l-4.7 1"></path>'
          + ' <path d="M12 12l0 .01"></path>'
          + ' <path d="M8 12l0 .01"></path>'
          + ' <path d="M16 12l0 .01"></path>'
          + ' </svg> '
          + article.replyCount
          + ' </li>'
          + ' </ul>'
          + ' </div>'
          + ' </div>'
          + ' </div>'
          + ' </div>'
          + ' </div>'
          + ' </div>';
        // 转为元素对象
        let articleItem = $(articleHtmlStr);
        // 获取标题的 a 标签
        let articleTitle = articleItem.find('.article_list_a_title');
        // 处理标题点击事件
        articleTitle.click(function() {
          // 通过全局变量保存当前访问的帖子信息
          currentArticle = article;
          removeNavActive();
          $('#bit-forum-content').load('details.html');
        });
        // 添加到列表
        $('#artical-items-body').append(articleItem);
      });
    }
4.3.6.2、用户帖子列表

在用户详情页显示当前用户发布的帖子列表以发布时间降序排列

当用户访问用户详情页面时,会向服务器发送请求。服务器会按照帖子的发帖时间从新到旧的顺序排列,然后返回帖子列表。

请求
// 请求
GET http: //127.0.0.1:58080/article/getAllByUserId HTTP/1.1
GET http: //127.0.0.1:58080/article/getAllByUserId?userId=1 HTTP/1.1
响应
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : [
{
"id" : 17 ,
"boardId" : 1 ,
"userId" : 1 ,
"title" : " 测试删除 " ,
"visitCount" : 8 ,
"replyCount" : 1 ,
"likeCount" : 1 ,
"state" : 0 ,
"createTime" : "2023-07-05 04:10:46" ,
"updateTime" : "2023-07-05 11:22:43" ,
"board" : {
"id" : 1 ,
"name" : "Java"
}
"own" : true
}
}
扩展Mapper.xml
    <select id="selectByUserId" parameterType="java.lang.Long" resultMap="AllInfoResultMap">
        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_board as b,
            t_article as a
        where
            a.userId=#{userId,jdbcType=BIGINT} and
            b.id=a.boardId and
            a.deleteState=0
        order by a.createTime desc
    </select>
修改DAO
    /**
     * 根据用户Id查询帖子列表以发布时间降序排列
     * @param userId
     * @return
     */
    List<Article> selectByUserId(@Param("userId") Long userId);
创建Service接口
    /**
     * 根据用户Id查询帖子列表以发布时间降序排列
     * @param userId
     * @return
     */
    List<Article> selectByUserId(Long userId);
实现Service接口
    @Override
    public List<Article> selectByUserId(Long userId) {
        //非空校验
        if (userId==null || userId<=0){
            //打印日志
            log.info(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //调用DAO层查询结果
        List<Article> articles=articleMapper.selectByUserId(userId);
        //返回结果
        return articles;
    }
对Service接口进行单元测试
    @Test
    void selectByUserId() throws JsonProcessingException {
        List<Article> articles=articleService.selectByUserId(1L);
        System.out.println(objectMapper.writeValueAsString(articles));
    }

实现Controller层
    /**
     * 根据用户Id查询帖子列表
     * @param userId
     * @return
     */
    @ApiOperation("根据用户Id查询帖子列表")
    @GetMapping("/getAllByUserId")
    public AppResult<List<Article>> getAllByUserId(@ApiParam("用户Id") @RequestParam("userId") @NonNull Long userId){
        //查询用户的帖子列表
        List<Article> articles=articleService.selectByUserId(userId);
        log.info("查询用户帖子列表,userId="+userId);
        //返回结果
        return AppResult.success(articles);
    }
测试接口

前端代码
    // 构造查询用户信息的queryString
    let articleListQueryString = '';
    if (currentUserId) {
      articleListQueryString = '?userId=' + currentUserId
    }
    // ============= 获取当前用户发贴 =============
    // url: 'article/getAllByUserId' + articleListQueryString
    // 成功后,调用buildProfileUserArticle()方法,构造帖子列表

    $.ajax({
      type:'GET',
      url:'article/getAllByUserId'+articleListQueryString,
      // 回调
      success : function (respData) {
        // 根据code的值判断响应是否成功
        if (respData.code == 0) {
          //设置用户信息
          buildProfileUserArticle(respData.data);
        } else {
          // 失败
          $.toast({
            heading: '失败',
            text: respData.message,
            icon: 'warning'
          })  
        }
      },
      error : function () {
        $.toast({
          heading: '错误',
          text: '访问网站出现问题,请与管理员联系',
          icon: 'error'
        }) 
      }
    });

    // ============= 构建用户帖子列表 =============
    function buildProfileUserArticle(data) {
      // 没有帖子
      if(data.length == 0) {
        $('#profile_article_body').html('还没有帖子');
        return;
      }
      // 构建帖子列表
      data.forEach(article => {
        let articleHtmlStr = ' <li class="timeline-event">'
        + ' <div class="timeline-event-icon bg-twitter-lt">'
        + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-plus" width="24"'
        + ' height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"'
        + ' stroke-linecap="round" stroke-linejoin="round">'
        + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
        + ' <path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4"></path>'
        + ' <path d="M10 11l4 0"></path>'
        + ' <path d="M12 9l0 4"></path>'
        + ' </svg>'
        + ' </div>'
        + ' <div class="card timeline-event-card">'
        + ' <div class="card-body">'
        + ' <div>'
        + ' <div class="row">'
        + ' <div class="col">'
        + ' <div class="text-truncate">'
        + ' <a href="javascript:void(0);"  class="profile_article_list_a_title">'
        + ' <strong>' + article.title + '</strong>'
        + ' </a>'
        + ' </div>'
        + ' <div class="text-muted mt-2">'
        + ' <div class="row">'
        + ' <div class="col">'
        + ' <ul class="list-inline list-inline-dots mb-0">'
        + ' <li class="list-inline-item">'
        + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clock-edit"'
        + ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'
        + ' fill="none" stroke-linecap="round" stroke-linejoin="round">'
        + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
        + ' <path d="M21 12a9 9 0 1 0 -9.972 8.948c.32 .034 .644 .052 .972 .052"></path>'
        + ' <path d="M12 7v5l2 2"></path>'
        + ' <path d="M18.42 15.61a2.1 2.1 0 0 1 2.97 2.97l-3.39 3.42h-3v-3l3.42 -3.39z"></path>'
        + ' </svg> '
        + article.createTime
        + ' </li>'
        + ' </ul>'
        + ' </div>'
        + ' <div class="col-auto d-none d-md-inline">'
        + ' <ul class="list-inline list-inline-dots mb-0">'
        + ' <li class="list-inline-item">'
        + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-eye"'
        + ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'
        + ' fill="none" stroke-linecap="round" stroke-linejoin="round">'
        + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
        + ' <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>'
        + ' <path'
        + ' d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7">'
        + ' </path>'
        + ' </svg> '
        + article.visitCount
        + ' </li>'
        + ' <li class="list-inline-item">'
        + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-heart"'
        + ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'
        + ' fill="none" stroke-linecap="round" stroke-linejoin="round">'
        + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
        + ' <path'
        + ' d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572">'
        + ' </path>'
        + ' </svg> '
        + article.likeCount
        + ' </li>'
        + ' <li class="list-inline-item">'
        + ' <svg xmlns="http://www.w3.org/2000/svg"'
        + ' class="icon icon-tabler icon-tabler-message-circle" width="24" height="24"'
        + ' viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"'
        + ' stroke-linecap="round" stroke-linejoin="round">'
        + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
        + ' <path d="M3 20l1.3 -3.9a9 8 0 1 1 3.4 2.9l-4.7 1"></path>'
        + ' <path d="M12 12l0 .01"></path>'
        + ' <path d="M8 12l0 .01"></path>'
        + ' <path d="M16 12l0 .01"></path>'
        + ' </svg> '
        + article.replyCount
        + ' </li>'
        + ' </ul>'
        + ' </div>'
        + ' </div>'
        + ' </div>'
        + ' </div>'
        + ' </div>'
        + ' </div>'
        + ' </div>'
        + ' </div>'
        + ' </li>';
        
        // 追加到父标签
        let profileArtilceItem = $(articleHtmlStr);
        // 获取标题的 a 标签
        let articleTitle = profileArtilceItem.find('.profile_article_list_a_title');
        // 处理标题点击事件
        articleTitle.click(function() {
          // 通过全局变量保存当前访问的帖子信息
          currentArticle = article;
          $('#bit-forum-content').load('details.html');
        });
        // 追加到父标签
        $('#profile_article_body').append(profileArtilceItem);
        
      });
    }

 

4.3.7、帖子操作

4.3.7.1、集成编辑区

editor.md支持使用Markdown语法进行编辑,将以下代码嵌入到需要用户输入内容的页面中以集成编辑器

编写HTML
          <!-- 引⼊编辑器的CSS -->
          <link rel="stylesheet" href="./dist/editor.md/css/editormd.min.css">
          <!-- 引⼊编辑器JS -->
          <script src="./dist/editor.md/editormd.min.js"></script>
          <script src="./dist/editor.md/lib/marked.min.js"></script>
          <script src="./dist/editor.md/lib/prettify.min.js"></script>
          <script src="./dist/libs/tinymce/tinymce.min.js" defer></script>
          <div id="edit-article">
            <!-- textarea也是一个表单控件,当在editor.md中编辑好的内容会关联这个文本域上 -->
            <textarea id="article_post_content" style="display: none;"></textarea>
          </div>
编写JS
<!-- 初始化编辑器 -->
<script type="text/javascript">
  $(function () {
    var editor = editormd("edit-article", {
      width: "100%",
      height: "100%",
      // theme : "dark",
      // previewTheme : "dark",
      // editorTheme : "pastel-on-dark",
      codeFold: true,
      markdown : '', // 处理编辑区内容
      //syncScrolling : false,
      saveHTMLToTextarea: true,    // 保存 HTML 到 Textarea
      searchReplace: true,
      watch : true,                    // 关闭实时预览
      htmlDecode: "style,script,iframe|on*",            // 开启 HTML 标签解析,为了安全性,默认不开启    
      // toolbar  : false,             //关闭工具栏
      // previewCodeHighlight : false, // 关闭预览 HTML 的代码块高亮,默认开启
      emoji: true,
      taskList: true,
      tocm: true,         // Using [TOCM]
      tex: true,                     // 开启科学公式TeX语言支持,默认关闭
      // flowChart: true,               // 开启流程图支持,默认关闭
      // sequenceDiagram: true,         // 开启时序/序列图支持,默认关闭,
      placeholder: '开始创作...',     // 占位符
      path: "./dist/editor.md/lib/"
    });
4.3.7.2、发布帖子

当用户点击发新帖按钮后,系统会跳转至发帖页面,用户可以选择相应的版块,填写标题和正文内容,然后提交给服务器。服务器会对提交的信息进行校验,并将信息写入数据库中。同时,服务器还会更新用户的发帖数和版块的帖子数。最后,系统会返回提交结果给用户。

请求
// 请求
POST http: //127.0.0.1:58080/article/create HTTP/1.1
Content-Type: application/x-www-form-urlencoded
boardId= 1 &title=%E6%B5% 8 B%E8%AF% 95 %E6% 96 %B0%E5%B8% 96 %E5%AD% 90 %E6%A0% 87 %E9%A2% 98
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{ "code" : 0 , "message" : " 成功 " , "data" : null }
创建Service接口

IUserService实现方法

    /**
     * 用户发帖数+1
     * @param id
     */
    int addOneArticleCountById(Long id);

IBoardService实现方法

    /**
     * 版块帖子数量+1
     * @param id
     */
    void addOneArticleCountById(Long id);

IArticleService实现方法

    /**
     * 发布新帖
     * @param article
     */
    //事务管理
    @Transactional
    void create(Article article);
实现Service接口

UserServiceImpl实现方法

    @Override
    public void addOneArticleCountById(Long id) {
        //非空校验
        if (id==null || id<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //获取用户信息
        User user=selectById(id);
        if (user==null || user.getDeleteState()==1){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        //构造更新对象
        User updateUser=new User();
        updateUser.setId(user.getId());
        updateUser.setArticleCount(user.getArticleCount()+1);
        updateUser.setUpdateTime(new Date());
        //调用DAO
        int row=userMapper.updateByPrimaryKeySelective(updateUser);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

BoardServiceImpl实现方法

    @Override
    public void addOneArticleCountById(Long id) {
        //非空校验
        if (id==null || id<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //获取版块信息
        Board board=selectById(id);
        if (board==null || board.getDeleteState()==1){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_NOT_EXISTS));
        }
        //构造更新对象
        Board updateBoard=new Board();
        updateBoard.setId(board.getId());
        updateBoard.setArticleCount(board.getArticleCount()+1);
        updateBoard.setUpdateTime(new Date());
        //调用DAO
        int row=boardMapper.updateByPrimaryKeySelective(updateBoard);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

ArticleServiceImpl实现方法

    @Override
    public void create(Article article) {
        //非空校验
        if (article==null || article.getBoardId()==null || article.getBoardId()<=0
        || article.getUserId()==null || article.getUserId()<=0
        || 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.ERROR_SERVICES.toString()+",帖子写入失败");
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //更新用户发帖数
        userService.addOneArticleCountById(article.getUserId());
        //更新版块帖子数
        boardService.addOneArticleCountById(article.getBoardId());
        //
        log.info("发帖成功:userId"+article.getUserId()+",boardId:"+article.getBoardId());
    }
对Service接口进行单元测试
    @Test
    void addOneArticleCountById() {
        userService.addOneArticleCountById(2L);
        System.out.println("更新成功");
    }

    @Test
    void addOneArticleCountById() {
        boardService.addOneArticleCountById(9L);
        System.out.println("更新成功");
    }

    @Test
    void create() {
        Article article=new Article();
        article.setUserId(3L);
        article.setBoardId(1L);
        article.setTitle("单元测试标题");
        article.setContent("单元测试内容");
        articleService.create(article);
        System.out.println("发帖成功");
    }

实现Controller层
    /**
     * 发布帖子
     * @param request
     * @param boardId
     * @param title
     * @param content
     * @return
     */
    @ApiOperation("发布帖子")
    @PostMapping("/create")
    public AppResult create(HttpServletRequest request,
                            @ApiParam("版块id") @RequestParam("boardId") @NonNull Long boardId,
                            @ApiParam("标题") @RequestParam("title") @NonNull String title,
                            @ApiParam("内容") @RequestParam("content") @NonNull String content){
        //获取当前登录用户
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //判断用户状态
        if (user.getState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //构造帖子对象
        Article article=new Article();
        article.setUserId(user.getId());  //作者Id
        article.setBoardId(boardId);  //版块Id
        article.setTitle(title);  //标题
        article.setContent(content);  //内容
        //调用Service
        articleService.create(article);
        //返回结果
        return AppResult.success("发布成功");
    }
测试接口

前端代码
    // ================== 处理发贴按钮事件 =======================
    $('#article_post_submit').click(function () {
      let boardIdEl = $('#article_post_borad');
      let titleEl = $('#article_post_title');
      let contentEl = $('#article_post_content');
      // 非空校验
      if (!titleEl.val()) {
        titleEl.focus();
        // 提示
        $.toast({
            heading: '提示',
            text: '请输入帖子标题',
            icon: 'warning'
        });
        return;
      }
      if (!contentEl.val()) {
        // 提示
        $.toast({
            heading: '提示',
            text: '请输入帖子内容',
            icon: 'warning'
        });
        return;
      }

      // 构造帖子对象
      let postData={
        boardId:boardIdEl.val(),
        title:titleEl.val(),
        content:contentEl.val()
      };

      // 提交, 成功后调用changeNavActive($('#nav_board_index'));回到首页并加载帖子列表
      // contentType: 'application/x-www-form-urlencoded'
      $.ajax({
        type:'POST',
        url:'article/create',
        contentType: 'application/x-www-form-urlencoded',
        data:postData,
        //回调
        success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //成功
            changeNavActive($('#nav_board_index'));
          }else{
            //失败
            $.toast({
              heading:'失败',
              text:respData.message,
              icon:'warning'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'访问网站出现问题,请与管理员联系',
            icon:'error'
          })
        }
      });
    });
4.3.7.3、获取帖子详情

当用户点击帖子时,客户端会发送帖子ID作为参数的请求到服务器。 服务器会根据该ID查询帖子的信息,并且增加帖子的访问次数。 最后,服务器将查询结果返回给客户端。

请求
// 请求
GET http: //127.0.0.1:58080/article/getById?id=1 HTTP/1.1
响应
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : {
"id" : 1 ,
"boardId" : 1 ,
"userId" : 1 ,
"title" : " 单元测试 " ,
"visitCount" : 14 ,
"replyCount" : 2 ,
"likeCount" : 3 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx" ,
"content" : " 测试内容 " ,
"board" : {
"id" : 1 ,
"name" : "Java"
},
"user" : {
"id" : 1 ,
"nickname" : "xxx" ,
"phoneNum" : null ,
"email" : null ,
"gender" : 1 ,
"avatarUrl" : null
},
"own" : true
}
}
扩展Mapper.xml
    <!--  根据Id查询帖子列表  -->
    <select id="selectDetailById" resultMap="AllInfoResultMap" parameterType="java.lang.Long">
        select
            u.id as u_id,
            u.nickname as u_nickname,
            u.avatarUrl as u_avatarUrl,
            u.gender as u_gender,
            u.state as u_state,
            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 a, t_board b, t_user u
        where a.boardId = b.id
          and a.userId = u.id
          and a.deleteState = 0
          and a.id = #{id,jdbcType=BIGINT}
    </select>
修改DAO
    /**
     * 根据Id查询帖子详情
     * @param id
     * @return
     */
    Article selectDetailById(@Param("id") Long id);
创建Service接口
    /**
     * 根据Id查询帖子详情
     * @param id
     * @return
     */
    Article selectDetailById(Long id);
实现Service接口
    @Override
    public Article selectDetailById(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=articleMapper.selectDetailById(id);
        if (article==null || article.getDeleteState()==1){
            //打印日志
            log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }
        //帖子询问数量加1
        article.setVisitCount(article.getVisitCount()+1);
        //创建用于更新的对象
        Article updateArticle=new Article();
        updateArticle.setId(article.getId());  //设置Id
        updateArticle.setVisitCount(article.getVisitCount());  //访问数
        updateArticle.setUpdateTime(new Date());  //更新时间
        //调用更新方法
        articleMapper.updateByPrimaryKeySelective(updateArticle);
        //返回成功
        return article;
    }
对Service接口进行单元测试
    @Test
    void selectDetailById() throws JsonProcessingException {
        Article article=articleService.selectDetailById(1L);
        System.out.println(objectMapper.writeValueAsString(article));
    }

实现Controller层
    @ApiOperation("获取帖子详情")
    @GetMapping("/details")
    public AppResult<Article> getDetails(HttpServletRequest request,
                                         @ApiParam("帖子Id") @RequestParam("id") @NonNull Long id){
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //调用Service
        Article article=articleService.selectDetailById(id);
        //判断当前登录用户是不是帖子的作者
        if (user.getId()== article.getUserId()){
            //设置所有者标识为true
            article.setOwn(true);
        }
        //返回结果
        return AppResult.success(article);
    }
测试接口

前端代码
    // ===================== 请求帖子详情 ===================== 
    // url: '/article/getById?id=' + currentArticle.id,
    // 成功后, 调用initArticleDetails()方法,初始化页面内容
    $.ajax({
      type:'GET',
      url:'article/details?id='+currentArticle.id,
      // 回调
      success : function (respData) {
        // 根据code的值判断响应是否成功
        if (respData.code == 0) {
          // 成功后跳转到首页
          initArticleDetails(respData.data);
        } else {
          // 失败
          $.toast({
            heading: '失败',
            text: respData.message,
            icon: 'warning'
          })  
        }
      },
      error : function () {
        $.toast({
          heading: '错误',
          text: '访问网站出现问题,请与管理员联系',
          icon: 'error'
        }) 
      }
    });
4.3.7.4、编辑帖子

用户只能编辑自己发的帖子,点击编辑接钮,提交编辑后的帖子时需要进行校验

在帖子实体类添加
    //当作者是自己的时候,设置为true
    private Boolean own;

当帖子的发帖人是当前用户时,显示编辑按钮。用户点击编辑按钮后,进入编辑页面;在编辑页面,获取帖子的信息,并在对应的位置显示标题和内容;用户可以修改帖子的标题和内容;用户提交修改后,将修改内容发送到服务器。服务器验证当前用户是否是帖子的发帖人,并更新数据库中的信息;返回更新结果给用户。

请求
// 请求
POST http: //127.0.0.1:58080/article/modify HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id= 1 &content=%E5%B8% 96 %E5%AD% 90 %E5% 86 % 85 %E5%AE%B9%EF%BC% 8 C%E6%B5% 8 B%E8%AF% 95 %E7
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
    /**
     * 更新
     * @param id
     * @param title
     * @param content
     */
    void modify(Long id,String title,String content);
实现Service接口
    @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 updateArticle=new Article();
        updateArticle.setId(id);  //id
        updateArticle.setTitle(title);  //标题
        updateArticle.setContent(content);  //正文
        updateArticle.setUpdateTime(new Date());  //更新时间
        //调用DAO
        int row=articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString()+",帖子更新失败,article id="+id);
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
    }
对Service接口进行单元测试
    @Test
    void modify() {
        articleService.modify(7L,"c++测试111","c++测试内容111");
        System.out.println("更新成功");
    }

实现Controller层
    @ApiOperation("更新帖子")
    @PostMapping("/modify")
    public AppResult modify(HttpServletRequest request,
                            @ApiParam("帖子Id") @RequestParam("id") @NonNull Long id,
                            @ApiParam("帖子标题") @RequestParam("title") @NonNull String title,
                            @ApiParam("帖子内容") @RequestParam("content") @NonNull String content){
        //1、校验用户状态,禁言状态不能更新帖子
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        if (user.getState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //2、查询帖子详情
        Article article=articleService.selectDetailById(id);
        //3、校验帖子状态(是否删除,是否封帖)
        if (article==null || article.getDeleteState()==1){
            //帖子不存在或已删除返回错误描述
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        //4、校验当前登录用户是不是帖子的作者
        if (user.getId()!= article.getUserId()){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_UNAUTHORIZED);
        }
        //调用Service
        articleService.modify(id,title,content);
        //返回结果
        return AppResult.success();
    }
测试接口

前端代码
    // ========================== 获取帖子详情 ========================== 
    // 成功后,设置ID,版块名,标题,并初始编辑区同时设置正文initEditor(edit_article.content);
    $.ajax({
      type:'GET',
      url:'article/details?id='+currentArticle.id,
      // 回调
      success : function (respData) {
        // 根据code的值判断响应是否成功
        if (respData.code == 0) {
          let edit_article=respData.data;
          //设置Id
          $('#edit_article_id').val(edit_article.id);
          //设置版块名
          $('#edit_article_board_name').html(edit_article_board_name);
          //设置标题
          $('#edit_article_title').html(edit_article_title);
          //设置内容
          $('#edit_article_content').html(edit_article_content);
          // 成功之后调用构建版块列表的方法
          initEditor(edit_article.content);
        } else {
          // 失败
          $.toast({
            heading: '失败',
            text: respData.message,
            icon: 'warning'
          })  
        }
      },
      error : function () {
        $.toast({
          heading: '错误',
          text: '访问网站出现问题,请与管理员联系',
          icon: 'error'
        }) 
      }
    });
    
    // ========================== 初始化编辑器 ========================== 
    var editor;
    function initEditor (md) {
      console.log('编辑区内容:' + md);
      editor = editormd("edit_article_content_area", {
      width: "100%",
      height: "100%",
      // theme : "dark",
      // previewTheme : "dark",
      // editorTheme : "pastel-on-dark",
      codeFold: true,
      markdown : md, // 处理编辑区内容
      //syncScrolling : false,
      saveHTMLToTextarea: true,    // 保存 HTML 到 Textarea
      searchReplace: true,
      watch : true,                    // 实时预览
      htmlDecode: "style,script,iframe|on*",            // 开启 HTML 标签解析,为了安全性,默认不开启    
      // toolbar  : false,             //关闭工具栏
      // previewCodeHighlight : false, // 关闭预览 HTML 的代码块高亮,默认开启
      emoji: true,
      taskList: true,
      tocm: true,         // Using [TOCM]
      tex: true,                     // 开启科学公式TeX语言支持,默认关闭
      // flowChart: true,               // 开启流程图支持,默认关闭
      // sequenceDiagram: true,         // 开启时序/序列图支持,默认关闭,
      placeholder: '开始创作...',     // 占位符
      path: "./dist/editor.md/lib/"
    });
    }

    // ========================== 处理提交修改事件 ========================== 
    $('#edit_article_submit').click(function () {
        // ID
        let articleIdEl = $('#edit_article_id');
        if (!articleIdEl.val()) {
          // 提示
          $.toast({
              heading: '提示',
              text: '数据不正确,请刷新后重试',
              icon: 'warning'
          });
          return;
        }
        //title
        let articleTitleEl = $('#edit_article_title');
        if (!articleTitleEl.val()) {
          // 提示
          $.toast({
              heading: '提示',
              text: '请输入帖子标题',
              icon: 'warning'
          });
          return;
        }
        // content
        let articleContentEl = $('#edit_article_content');
        // 非空校验
        if (!articleContentEl.val()) {
          // 提示
          $.toast({
              heading: '提示',
              text: '请输入帖子内容',
              icon: 'warning'
          });
          return;
      }

        // 构造修改对象
        let 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) {
            // 根据code的值判断响应是否成功
            if (respData.code == 0) {
              //成功后跳转到首页
              changeNavActive($('#nav_board_index'));
            } else {
              // 失败
              $.toast({
                heading: '失败',
                text: respData.message,
                icon: 'warning'
              })  
            }
          },
          error : function () {
            $.toast({
              heading: '错误',
              text: '访问网站出现问题,请与管理员联系',
              icon: 'error'
            }) 
          }
        });
    });
4.3.7.5、删除帖子
请求
// 请求
GET http: //127.0.0.1:58080/article/delete?id=11 HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口

IUserService实现方法

    /**
     * 用户发帖数-1
     * @param id
     */
    void subOneArticleCountById(Long id);

IBoardService实现方法

    /**
     * 版块中的帖子数量-1
     * @param id
     */
    void subOneArticleCountById(Long id);

IArticleService实现方法

    /**
     * 根据帖子Id删除
     * @param id
     */
    @Transactional
    void deleteById(Long id);
实现Service接口

UserServiceImpl实现方法

    @Override
    public void subOneArticleCountById(Long id) {
        //非空校验
        if (id==null || id<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //获取用户信息
        User user=selectById(id);
        if (user==null || user.getDeleteState()==1){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        //构造更新对象
        User updateUser=new User();
        updateUser.setId(user.getId());
        updateUser.setArticleCount(user.getArticleCount()-1);
        //判断减1之后,用户的发帖数是否小于0
        if (updateUser.getArticleCount()<0){
            //如果小于0,则设置为0
            updateUser.setArticleCount(0);
        }
        //调用DAO
        int row=userMapper.updateByPrimaryKeySelective(updateUser);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }

BoardServiceImpl实现方法

    @Override
    public void subOneArticleCountById(Long id) {
        //非空校验
        if (id==null || id<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //查询版块详情
        Board board=selectById(id);
        if (board==null || board.getDeleteState()==1){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_NOT_EXISTS));
        }
        //构造更新对象
        Board updateBoard=new Board();
        updateBoard.setId(board.getId());
        updateBoard.setArticleCount(board.getArticleCount()-1);
        //判断减1之后是否小于0
        if (updateBoard.getArticleCount()<0){
            //如果小于0那么设置为0
            updateBoard.setArticleCount(0);
        }
        //调用DAO
        int row=boardMapper.updateByPrimaryKeySelective(updateBoard);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }

ArticleServiceImpl实现方法

    @Override
    public void deleteById(Long id) {
        //非空校验
        if (id==null || id<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //根据Id查询帖子信息
        Article article=articleMapper.selectByPrimaryKey(id);
        if (article==null || article.getDeleteState()==1){
            //打印日志
            log.warn(ResultCode.FAILED_BOARD_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_NOT_EXISTS));
        }
        //构造一个更新对象
        Article updateArticle=new Article();
        updateArticle.setId(article.getId());
        updateArticle.setDeleteState((byte) 1);
        //调用DAO
        int row=articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        //更新版块中的帖子数量
        boardService.subOneArticleCountById(article.getBoardId());
        //更新用户发帖数
        userService.subOneArticleCountById(article.getUserId());
        //打印日志
        log.info("删除帖子成功,article id="+article.getId()+",user id="+article.getUserId());
    }
对Service接口进行单元测试
    @Test
    void deleteById() {
        articleService.deleteById(9L);
        System.out.println("删除成功");
    }

实现Controller层
    /**
     * 根据Id删除帖子
     * @param request
     * @param id
     * @return
     */
    @ApiOperation("删除帖子")
    @PostMapping("/delete")
    public AppResult deleteById(HttpServletRequest request,
                                @ApiParam("帖子Id") @RequestParam("id") @NonNull Long id){
        //1、校验用户状态,禁言状态不能删除
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        if (user.getState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //2、查询帖子详情
        Article article=articleService.selectDetailById(id);
        //3、校验帖子状态(是否删除,是否封帖)
        if (article==null || article.getDeleteState()==1){
            //帖子不存在或已删除返回错误描述
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        //4、校验当前登录用户是不是帖子的作者
        if (user.getId()!= article.getUserId()){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_UNAUTHORIZED);
        }
        //调用Service执行删除
        articleService.deleteById(id);
        //返回结果
        return AppResult.success();
    }
测试接口

前端代码
    // ====================== 处理删除事件 ======================
    // 成功后,调用changeNavActive($('#nav-link-title'));  回到首页
    // url: 'article/delete?id=' + $('#details_article_id').val()
    $('#details_artile_delete').click(function () {
      $.ajax({
        type:'POST',
        url:'article/delete?id='+$('#details_article_id').val(),
        // 回调
        success : function (respData) {
          // 根据code的值判断响应是否成功
          if (respData.code == 0) {
            //初始化页面内容
            changeNavActive($('#nav-link-title'));
            // 提示
            $.toast({
              heading: '提示',
              text: '删除成功',
              icon: 'success'
            }); 
          } else {
            // 失败
            $.toast({
              heading: '失败',
              text: respData.message,
              icon: 'warning'
            })  
          }
        },
        error : function () {
          $.toast({
            heading: '错误',
            text: '访问网站出现问题,请与管理员联系',
            icon: 'error'
          }) 
        }
      });
    });
4.3.7.6、点赞帖子

用户在帖子详情页进行点赞操作

请求
// 请求
POST http: //127.0.0.1:58080/article/thumbsUp HTTP/1.1
Content-Type: application/x-www-form-urlencoded
id= 1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
    /**
     * 点赞
     * @param id
     */
    void thumbsUpById(Long id);
实现Service接口
    @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=articleMapper.selectByPrimaryKey(id);
        if (article==null || article.getState()==1 || article.getDeleteState()==1){
            //打印日志
            log.warn(ResultCode.FAILED_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
        }
        //构造更新对象
        Article updateArticle=new Article();
        updateArticle.setId(article.getId());
        updateArticle.setLikeCount(article.getLikeCount()+1);
        //调用DAO
        int row=articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString()+"userId="+article.getUserId());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
    }
对Service接口进行单元测试
    @Test
    void thumbsUpById() {
        articleService.thumbsUpById(1L);
        System.out.println("点赞帖子成功");
    }

实现Controller层
    /**
     * 点赞帖子
     * @param request
     * @param id
     * @return
     */
    @ApiOperation("点赞")
    @PostMapping("/thumbsUp")
    public AppResult thumbsUp(HttpServletRequest request,
                              @ApiParam("帖子Id") @RequestParam("id") @NonNull Long id){
        //获取用户信息
        HttpSession session=request.getSession(false);
        //判断是否被禁言
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        if (user.getState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //更新点赞数
        articleService.thumbsUpById(id);
        //返回结果
        return AppResult.success();
    }
测试接口

前端代码
    // ====================== 处理点赞 ======================
    // url: '/article/thumbsUp?id=' + currentArticle.id
    // 成功后,修改点赞个数 currentArticle.likeCount = currentArticle.likeCount + 1;
    $('#details_btn_like_count').click(function () {
      $.ajax({
        type:'POST',
        url: '/article/thumbsUp?id=' + currentArticle.id,
        // 回调
        success : function (respData) {
          // 根据code的值判断响应是否成功
          if (respData.code == 0) {
            //修改点赞个数
            currentArticle.likeCount = currentArticle.likeCount + 1;
            $('#details_article_likeCount').html(currentArticle.likeCount);
            // 提示
            $.toast({
              heading: '提示',
              text: respData.message,
              icon: 'success'
            });
          } else {
            // 失败
            $.toast({
              heading: '失败',
              text: respData.message,
              icon: 'warning'
            })  
          }
        },
        error : function () {
          $.toast({
            heading: '错误',
            text: '访问网站出现问题,请与管理员联系',
            icon: 'error'
          }) 
        }
      });
    });

4.3.8、帖子回复

4.3.8.1、提交回复内容

在帖子详情页面用户可以回复,用户可以在帖子正常状态下回复;用户填写回复内容,然后点击提交按钮,触发向服务器发送请求;服务器对回复内容、帖子和用户状态进行校验,通过后将回复内容写入数据库;帖子的回复数量加1;最后,返回操作结果。

请求
// 请求
POST http: //127.0.0.1:58080/reply/create HTTP/1.1
Content-Type: application/x-www-form-urlencoded
articleId= 1 &content=%E5% 9 B% 9 E%E5%A4% 8 D%E6%B2%A1%E8%AF% 95
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
public interface IArticleReplyService {

    /**
     * 新增回复
     * @param articleReply
     */
    @Transactional
    void create(ArticleReply articleReply);
}
实现Service接口
@Service
@Slf4j
public class ArticleReplyServiceImpl implements IArticleReplyService {

    @Resource
    private ArticleReplyMapper articleReplyMapper;

    @Resource
    private IArticleService articleService;

    @Override
    public void create(ArticleReply articleReply) {
        //非空校验
        if (articleReply==null || articleReply.getPostUserId()==null || articleReply.getPostUserId()<=0
            || articleReply.getArticleId()==null || articleReply.getArticleId()<=0
            || StringUtils.isEmpty(articleReply.getContent())){
            //打印日志
            log.info(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //获取帖子详情
        Article article=articleService.selectDetailById(articleReply.getArticleId());
        if (article==null || article.getDeleteState()==1){
            //帖子不存在
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }
        if (article.getState()==1){
            //帖子封帖
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_STATE));
        }
        //设置默认值
        articleReply.setLikeCount(0);  //点赞数
        articleReply.setState((byte) 0);  //状态
        articleReply.setDeleteState((byte) 0);  //删除状态
        Date date=new Date();
        articleReply.setCreateTime(date);  //创建时间
        articleReply.setUpdateTime(date);  //更新时间
        //调用DAO
        int row=articleReplyMapper.insertSelective(articleReply);
        if (row!=1){
            //打印日志
            log.info(ResultCode.ERROR_SERVICES.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        //更新帖子的回复数
        Article updateArticle=new Article();
        updateArticle.setId(article.getId());  //帖子Id
        updateArticle.setReplyCount(article.getReplyCount()+1);  //回复数
        updateArticle.setUpdateTime(new Date());  //更新时间
        //调用帖子的更新方法
        articleService.updateById(updateArticle);
        //打印日志
        log.info("回复成功,article id="+article.getId());
    }
}
对Service接口进行单元测试
@SpringBootTest
class ArticleReplyServiceImplTest {

    @Resource
    private IArticleReplyService articleReplyService;

    @Resource
    private ObjectMapper objectMapper;

    @Test
    void create() {
        ArticleReply reply=new ArticleReply();
        reply.setPostUserId(1L);
        reply.setArticleId(1L);
        reply.setContent("测试内容回复");
        articleReplyService.create(reply);
        System.out.println("回复成功");
    }
}

实现Controller层
@Api(tags = "回复接口")
@Slf4j
@RestController
@RequestMapping("/reply")
public class ArticleReplyController {

    @Resource
    private IArticleReplyService articleReplyService;

    @Resource
    private IArticleService articleService;

    @ApiOperation("回复帖子")
    @PostMapping("/create")
    public AppResult create(HttpServletRequest request,
                            @ApiParam("帖子Id") @RequestParam("articleId") @NonNull Long articleId,
                            @ApiParam("帖子正文") @RequestParam("content") @NonNull String content){
        //1、校验用户状态是否为禁言
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        if (user.getState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //2、校验帖子是否存在
        Article article=articleService.selectDetailById(articleId);
        if (article==null || article.getDeleteState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        //3、校验帖子状态是否正常
        if (article.getState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_ARTICLE_STATE);
        }
        //4、构造回复对象
        ArticleReply articleReply=new ArticleReply();
        articleReply.setPostUserId(user.getId());  //回复的作者
        articleReply.setArticleId(articleId);  //主帖Id
        articleReply.setContent(content);  //回复内容
        //5、调用Service
        articleReplyService.create(articleReply);
        //6、返回正确结果
        return AppResult.success("回复成功");
    }
}
测试接口

前端代码
    // ====================== 回复帖子 ======================
    $('#details_btn_article_reply').click(function () {
      let articleIdEl = $('#details_article_id');
      let replyContentEl = $('#details_article_reply_content');
      // 非空校验
      if (!replyContentEl.val()) {
        // 提示
        $.toast({
          heading: '提示',
          text: '请输入回复内容',
          icon: 'warning'
        });
        return;
      }

      // 构造帖子对象
      let 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) {
          // 根据code的值判断响应是否成功
          if (respData.code == 0) {
            //清空输入区
            editor.setValue('');
            //更新全局变量中replyCount的值
            currentArticle.replyCount = currentArticle.replyCount + 1;
            //设置页面中回复数的值
            $('#details_article_replyCount').html(currentArticle.replyCount);
            //加载回复列表
            loadArticleDetailsReply();
            $.toast({
              heading: '提⽰',
              text: respData.message,
              icon: 'success'
              });
          } else {
            // 失败
            $.toast({
              heading: '失败',
              text: respData.message,
              icon: 'warning'
            })  
          }
        },
        error : function () {
          $.toast({
            heading: '错误',
            text: '访问网站出现问题,请与管理员联系',
            icon: 'error'
          }) 
        }
      });
    });
4.3.8.2、帖子回复列表

在帖子详情页显示当前帖子下的回复列表以发布时间降序排列

请求
// 请求
GET http: //127.0.0.1:58080/reply/getReplies?articleId=1 HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : [
{
"id" : 9 ,
"articleId" : 1 ,
"postUserId" : 2 ,
"content" : " 回复没试 " ,
"likeCount" : 0 ,
"state" : 0 ,
"createTime" : "2023-07-09 06:39:45" ,
"updateTime" : "2023-07-09 06:39:45" ,
"user" : {
"id" : 2 ,
"nickname" : "bitgirl" ,
"phoneNum" : null ,
"email" : null ,
"gender" : 2 ,
"avatarUrl" : null
}
}
添加关联对象

对ArticleReply类中添加user的关联对象

    //关联用户对象
    private User user;
扩展Mapper.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.example.forum_system.dao.ArticleReplyMapper">
    <!-- 自定义表关联的结查集 -->
    <resultMap id="AllInfoResultMap" type="com.example.forum_system.model.ArticleReply" extends="BaseResultMap">
        <!-- 关联对象的映射 -->
        <association property="user" resultMap="com.example.forum_system.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.createTime,
            ar.updateTime
        from t_article_reply ar, t_user u
        where ar.postUserId = u.id
          and ar.deleteState = 0
          and ar.articleId = #{articleId,jdbcType=BIGINT}
        order by ar.createTime desc
    </select>
</mapper>
修改DAO
    /**
     * 根据主帖Id查询回复列表
     * @param articleId
     * @return
     */
    List<ArticleReply> selectByArticleId(@Param("articleId") Long articleId);
创建Service接口
/**
     * 根据主帖Id查询回复列表
     * @param articleId
     * @return
     */
    List<ArticleReply> selectByArticleId(Long articleId);
实现Service接口
    @Override
    public List<ArticleReply> selectByArticleId(Long articleId) {
        //非空校验
        if (articleId==null || articleId<=0){
            //打印日志
            log.info(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //校验主帖是否存在
        Article article=articleService.selectDetailById(articleId);
        if (article==null || article.getDeleteState()==1){
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }
        //调用DAO
        List<ArticleReply> articleReplies=articleReplyMapper.selectByArticleId(articleId);
        //返回结果集
        return articleReplies;
    }
对Service接口进行单元测试
    @Test
    void selectByArticleId() throws JsonProcessingException {
        List<ArticleReply> articleReplies=articleReplyService.selectByArticleId(1L);
        System.out.println("回复数为:"+articleReplies.size());
        System.out.println(objectMapper.writeValueAsString(articleReplies));

        articleReplies=articleReplyService.selectByArticleId(6L);
        System.out.println("回复数为:"+articleReplies.size());
        System.out.println(objectMapper.writeValueAsString(articleReplies));
    }

实现Controller层
    @ApiOperation("获取回复列表")
    @GetMapping("/getReplies")
    public AppResult<List<ArticleReply>> getRepliesByArticleId(@ApiParam("主帖Id") @RequestParam("articleId") @NonNull Long articleId){
        //校验帖子是否存在
        Article article=articleService.selectDetailById(articleId);
        if (article==null || article.getDeleteState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        //调用Service获取回复列表
        List<ArticleReply> articleReplies=articleReplyService.selectByArticleId(articleId);
        //校验结果是否为空,为空的话则创建一个空集合并返回([]),防止返回一个字符串形式的null
        if (articleReplies==null){
            articleReplies=new ArrayList<>();
        }
        //返回结果
        return AppResult.success(articleReplies);
    }
测试接口

前端代码
    // ====================== 加载回复列表 ======================
    // url: 'article/getReplies?articleId=' + currentArticle.id
    // 成功后,调用buildArticleReply()方法构建回复列表
    function loadArticleDetailsReply() {
      $.ajax({
        type:'GET',
        url: 'reply/getReplies?articleId=' + currentArticle.id,
        // 回调
        success : function (respData) {
          // 根据code的值判断响应是否成功
          if (respData.code == 0) {
            //构建回复列表
            buildArticleReply(respData.data);
          } else {
            // 失败
            $.toast({
              heading: '失败',
              text: respData.message,
              icon: 'warning'
            })  
          }
        },
        error : function () {
          $.toast({
            heading: '错误',
            text: '访问网站出现问题,请与管理员联系',
            icon: 'error'
          }) 
        }
      });
    }
    // 
    function buildArticleReply(data) {
      let replyArea = $('#details_reply_area');
      // 没有回复内容
      if (!data || data.length == 0) {
        replyArea.html('<p>还没有回复,第一个写下回复吧</p>');
        return;
      }
      // 清空原有内空
      $('#details_reply_area').html('');
      data.forEach(articleReply => {
        // 默认头像路径
      if (!articleReply.user.avatarUrl) {
        articleReply.user.avatarUrl = avatarUrl;
      }
        // 构造回复记录
        let replyHtml = '<div class="row" >'
          + ' <div class="col-3 card">'
          + ' <div class="card-body p-4 text-center">'
          + ' <span class="avatar avatar-xl mb-3 rounded" style="background-image: url(' + articleReply.user.avatarUrl + ')"></span>'
          + ' <h3 class="m-0 mb-1"><a href="javascript:void(0);" class="a_reply_user_profile">' + articleReply.user.nickname + '</a></h3>'
          + ' <div class="div_reply_send_message" style="margin-top: 10px;">'
          + ' <a href="javascript:void(0);" class="btn btn-primary btn_reply_send_message" data-bs-toggle="modal" data-bs-target="#index_message_modal">'
          + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" width="24" height="24"'
          + ' viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"' 
          + ' stroke-linejoin="round">'
          + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
          + ' <path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z">'
          + ' </path>'
          + ' <path d="M3 7l9 6l9 -6"></path>'
          + ' </svg>'
          + ' 发私信'
          + ' </a>'
          + ' </div>'
          + ' </div>'
          + ' </div>'
          + ' <div class="col-9 card card-lg">'
          + ' <div class="card-body">'
          + ' <div id="details_article_reply_content_' + articleReply.id + '"></div>'
          + ' </div>'
          + ' <div class="card-footer bg-transparent mt-auto"'
          + ' style="display: flex; justify-content: space-between; align-items: center;">'
          + ' <div class="row">'
          + ' <div class="col-auto d-none d-md-inline">'
          + ' <ul class="list-inline list-inline-dots mb-0">'
          + ' <li class="list-inline-item">'
          + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clock-edit" width="24"'
          + ' height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"'
          + ' stroke-linecap="round" stroke-linejoin="round">'
          + ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'
          + ' <path d="M21 12a9 9 0 1 0 -9.972 8.948c.32 .034 .644 .052 .972 .052"></path>'
          + ' <path d="M12 7v5l2 2"></path>'
          + ' <path d="M18.42 15.61a2.1 2.1 0 0 1 2.97 2.97l-3.39 3.42h-3v-3l3.42 -3.39z"></path>'
          + ' </svg> '
          + articleReply.createTime
          + ' </li>'
          + ' </ul>'
          + ' </div>'
          + ' </div>'
          + ' </div>'
          + ' </div>'
          + ' </div>';

        
        let replyItem = $(replyHtml);
        // 获取标题的 a 标签
        let replySendMessageDiv = replyItem.find('.div_reply_send_message');
        // 是否显示站内信按钮
        if (articleReply.user.id == currentUserId) {
          replySendMessageDiv.css('display', 'none');
        } else {
          console.log('显示回复中的站内信');
          // 设置站内信目标用户信息
          let replySendMessageBtn = replyItem.find('.btn_reply_send_message');
          console.log(replySendMessageBtn);
          replySendMessageBtn.click(function() {
            console.log(articleReply);
            setMessageReceiveUserInfo(articleReply.user.id, articleReply.user.nickname);
        });
        }
        // 个人帖子列表
        let replyUserProfileBtn = replyItem.find('.a_reply_user_profile');
        replyUserProfileBtn.click(function () {
          // 设置要查看用户的Id
          profileUserId = articleReply.user.id;
          $('#bit-forum-content').load('profile.html');
        });
        // 添加到回复区
        replyArea.append(replyItem);
        
        // 处理内容
        editormd.markdownToHTML('details_article_reply_content_' + articleReply.id, { markdown: articleReply.content });
      });
    }

4.3.9、站内信

4.3.9.1、发送

在目录用户详情界面,点击发送站内信按钮后,会跳转到编辑页面;在编辑页面中,填写站内信内容后,点击发送按钮;发送完成后,会显示发送结果。

请求
// 请求
POST http: //127.0.0.1:58080/message/send HTTP/1.1
Content-Type: application/x-www-form-urlencoded
receiveUserId= 2 &content=%E4%BD%A0%E5%A5%BD%E5% 95 % 8 A
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
public interface IMessageService {

    /**
     * 发送站内信息
     * @param message
     */
    void create(Message message);
}
实现Service接口
@Slf4j
@Service
public class MessageServiceImpl implements IMessageService {

    @Resource
    private MessageMapper messageMapper;

    @Resource
    private IUserService userService;

    @Override
    public void create(Message message) {
        //非空校验
        if (message==null || message.getPostUserId()==null || message.getReceiveUserId()==null
            || StringUtils.isEmpty(message.getContent())){
            //打印日志
            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.setState((byte) 0);  //表示未读状态
        message.setDeleteState((byte) 0);
        //设置创建于更新时间
        Date date=new Date();
        message.setCreateTime(date);
        message.setUpdateTime(date);
        //调用DAO
        int row=messageMapper.insertSelective(message);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.FAILED_CREATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
    }
}
对Service接口进行单元测试
@SpringBootTest
class MessageServiceImplTest {

    @Resource
    private IMessageService messageService;

    @Resource
    private ObjectMapper objectMapper;

    @Test
    void create() {
        Message message=new Message();
        message.setPostUserId(3L);
        message.setReceiveUserId(1L);
        message.setContent("你好,我是krystal");
        messageService.create(message);
        System.out.println("新增站内信成功");
    }
}

实现Controller层
@Slf4j
@Api(tags = "站内信接口")
@RestController
@RequestMapping("/message")
public class MessageController {

    @Resource
    private IMessageService messageService;

    @Resource
    IUserService userService;

    @ApiOperation("发送站内信")
    @PostMapping("/send")
    public AppResult send(HttpServletRequest request,
                          @ApiParam("接受用户Id") @RequestParam("receiveUserId") @NonNull Long receiveUserId,
                          @ApiParam("站内信内容") @RequestParam("content") @NonNull String content){
        //获取发送用户信息
        HttpSession session= request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //判断用户是否被禁言
        if (user==null || user.getDeleteState()==1){
            //打印日志
            log.warn(ResultCode.FAILED_USER_BANNED.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_BANNED));
        }
        //不能给自己发送
        if (user.getId()==receiveUserId){
            //打印日志
            log.warn("不能给自己发送站内信,postUserId="+user.getId()+",receiveUserId="+receiveUserId);
            //返回错误信息
            return AppResult.failed(ResultCode.FAILED_CREATE);
        }
        //查询接收用户
        User receiveUser=userService.selectById(receiveUserId);
        //目标用户不存在
        if (receiveUser==null || receiveUser.getDeleteState()==1){
            //打印日志
            log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));
        }
        //构造对象
        Message message=new Message();
        message.setPostUserId(user.getId());
        message.setReceiveUserId(receiveUserId);
        message.setContent(content);
        //调用Service
        messageService.create(message);
        //返回结果
        return AppResult.success();
    }
}
测试接口

前端代码
    // ============ 发送站内信 ==============
    $('#btn_index_send_message').click(function() {
      // 获取输入内容
      let receiveUserIdEl = $('#index_message_receive_user_id');
      let messageContentEl = $('#index_message_receive_content');
      // 校验
      if (!receiveUserIdEl.val()) {
        $.toast({
            heading: '警告',
            text: '出错了,请联系管理员',
            icon: 'warning'
          });
          return;
      }
      if (!messageContentEl.val()) {
        $.toast({
          heading: '警告',
          text: '请输入要发送的内容',
          icon: 'warning'
        });
        // 输入框
        messageContentEl.focus();
        retrun;
      }

      // 构造发送数据
      let postData = {
        receiveUserId : receiveUserIdEl.val(),
        content : messageContentEl.val()
      };

      // 发送站内信请求 url = message/send, 成功与失败都调用cleanMessageForm()方法,清空输入框
      $.ajax({
        type:'POST',
        url:'message/send',
        contentType : 'application/x-www-form-urlencoded',
        data:postData,
        //回调
        success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //成功
            cleanMessageForm();
            //提示信息
            $.toast({
              heading:'成功',
              text:respData.message,
              icon:'success'
            });
          }else{
            //失败
            $.toast({
              heading:'失败',
              text:respData.message,
              icon:'warning'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'访问网站出现问题,请与管理员联系',
            icon:'error'
          })
        }
      });
    });
4.3.9.2、未读数

查询当前登录用户的未读站内信数量

请求
// 请求
GET http: //127.0.0.1.41:58080/message/getUnreadCount HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{ "code" : 0 , "message" : " 成功 " , "data" : 1 }
扩展Mapper.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.example.forum_system.dao.MessageMapper">
    <select id="selectUnreadCount" parameterType="java.lang.Long"
            resultType="java.lang.Integer">
        select COUNT(*) from t_message
        WHERE state = 0
          and deleteState = 0
          and receiveUserId = #{receiveUserId,jdbcType=BIGINT}
    </select>
</mapper>
修改DAO
    /**
     * 查询当前登录用户的未读站内信数量
     * @param userId
     * @return
     */
    Integer selectUnreadCount(Long userId);
创建Service接口
    /**
     * 查询当前登录用户的未读站内信数量
     * @param userId
     * @return
     */
    Integer selectUnreadCount(Long userId);
实现Service接口
    @Override
    public Integer selectUnreadCount(Long userId) {
        //非空校验
        if (userId==null || userId<=0){
            //记录日志
            log.info(ResultCode.ERROR_IS_NULL.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_IS_NULL));
        }
        //调用DAO
        Integer result= messageMapper.selectUnreadCount(userId);
        //返回结果
        return result;
    }
对Service接口进行单元测试
    @Test
    void selectUnreadCount() {
        Integer result=messageService.selectUnreadCount(1L);
        System.out.println(result);
        System.out.println("查询成功");
    }

实现Controller层
    @ApiOperation("获取未读数消息个数")
    @GetMapping("/getUnreadCount")
    public AppResult<Integer> getUnreadCount(HttpServletRequest request){
        //获取发送用户消息
        HttpSession session=request.getSession();
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //查询未读消息个数
        Integer result= messageService.selectUnreadCount(user.getId());
        //返回结果
        return AppResult.success(result);
    }
测试接口

前端代码
    // ============ 获取用户未读站内信数量 ============
    // url = message/getUnreadCount
    // 成功后,处理小红点是否显示 #index_nva_message_badge
    function requestMessageUnreadCount () {
      $.ajax({
        type:'GET',
        url:'message/getUnreadCount',
        //成功回调
        //回调
        success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //处理提示红点
            let messageBadgeEl=$('#index_nva_message_badge');
            if(respData.data>0){
              messageBadgeEl.show();
            }else{
              messageBadgeEl.hide();
            }
          }else{
            //失败
            $.toast({
              heading:'提示',
              text:'无法获取站内信,请联系管理员',
              icon:'info'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'出错了,请与管理员联系',
            icon:'error'
          })
        }
      });
    }
    requestMessageUnreadCount();
4.3.9.3、列表

用户访问API,服务器响应当前登录用户的站内信

请求
// 请求
GET http: //127.0.0.1:58080/message/getAll HTTP/1.1
响应
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : [{
"id" : 1 ,
"postUserId" : 1 ,
"receiveUserId" : 2 ,
"content" : " 真的可以发出去吗 \n" ,
"state" : 2 ,
"createTime" : "xxx" ,
"updateTime" : "xxx" ,
"postUser" : {
"id" : 32 ,
"nickname" : "ljl" ,
"phoneNum" : null ,
"email" : null ,
"gender" : 2 ,
"avatarUrl" : null
}
}
扩展Mapper.xml
    <!-- 定义表连接查询返回的结果集映射,继承⾃源配置⽂件的映射结果集 -->
    <resultMap id="AllInfoResultMap" type="com.example.forum_system.model.Message" extends="BaseResultMap">
        <!-- 扩展⽤⼾信息结果, 注意查询结果列名的前缀为 u_ -->
        <association property="postUser" resultMap="com.example.forum_system.dao.UserMapper.BaseResultMap" columnPrefix="u_" />
    </resultMap>
    <!-- 按⽤⼾ID查询所有站内信 -->
    <select id="selectByReceiveUserId" parameterType="java.lang.Long" resultMap="AllInfoResultMap">
         select
         u.id AS u_id,
         u.nickname AS u_nickname,
         u.gender AS u_gender,
         u.avatarUrl AS u_avatarUrl,
         m.id,
         m.postUserId,
         m.receiveUserId,
         m.content,
         m.state,
         m.createTime,
         m.updateTime
         FROM
         t_message AS m,
         t_user AS u
         WHERE
         m.postUserId = u.id AND
         m.deleteState = 0 AND
         m.receiveUserId = #{receiveUserId,jdbcType=BIGINT}
         order by m.createTime DESC
    </select>
修改DAO
    /**
     * 根据接收者Id查询所有站内信
     * @param receiveUserId
     * @return
     */
    List<Message> selectByReceiveUserId(@Param("receiveUserId") Long receiveUserId);
创建Service接口
    /**
     * 根据接收者Id查询所有站内信
     * @param receiveUserId
     * @return
     */
    List<Message> selectByReceiveUserId(Long receiveUserId);
实现Service接口
    @Override
    public List<Message> selectByReceiveUserId(Long receiveUserId) {
        //非空校验
        if (receiveUserId==null || receiveUserId<=0){
            //记录日志
            log.info(ResultCode.ERROR_IS_NULL.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_IS_NULL));
        }
        //调用DAO
        List<Message> messages=messageMapper.selectByReceiveUserId(receiveUserId);
        //返回结果
        return messages;
    }
对Service接口进行单元测试
    @Test
    void selectByReceiveUserId() {
        List<Message> messages=messageService.selectByReceiveUserId(1L);
        System.out.println(messages);
        System.out.println("查询接收者所有站内信成功");
    }

实现Controller层
    /**
     * 查询用户的所有站内信
     * @param request
     * @return
     */
    @ApiOperation("查询用户的所有站内信")
    @GetMapping("/getAll")
    public AppResult<List<Message>> getAll(HttpServletRequest request){
        //获取当前登录用户
        HttpSession session= request.getSession();
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //获取用户站内信
        List<Message> messages=messageService.selectByReceiveUserId(user.getId());
        //返回结果
        return AppResult.success(messages);
    }
测试接口

前端代码
    // ============ 获取用户所有站内信 ============
    // 成功后,调用buildMessageList() 方法构建站内信列表
    function requestMessageList () {
      $.ajax({
        type:'GET',
        url:'message/getAll',
        //回调
        success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //处理站内信列表页面
            buildMessageList();
          }else{
            //失败
            $.toast({
              heading:'失败',
              text:respData.message,
              icon:'warning'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'访问网站出现问题,请与管理员联系',
            icon:'error'
          })
        }
      });
    }
    requestMessageList();
    

    // ============ 处理站内信列表页面 ============
    function buildMessageList(messageList) {
      // 获取父标签
      let messageDivEl = $('#index_div_message');
      if (!messageList || messageList.length == 0) {
        messageDivEl.html('<strong>没有站内信</strong>');
        return;
      }
      // 获取站内信列表父标签
      let messageListDivEl = $('#index_div_message_list');
      messageListDivEl.html('');
      // 遍历结果
      messageList.forEach(messageItem => {
        let itemHtml = ' <div class="list-group-item"> '
          + ' <div class="row align-items-center"> '
          + ' <div class="col-auto"><span class="status-dot d-block"></span></div> '
          + ' <div class="col text-truncate"> '
          + ' <a href="javascript:void(0);" class="text-body d-block index_message_title" data-bs-toggle="modal" data-bs-target="#index_message_reply_modal"> '
          +  ' <span class="index_message_item_statue">[已读]</span> &nbsp; '
          + ' <span>来自 <strong> '+ messageItem.postUser.nickname 
          + ' </strong> 的消息</span></a> '
          + ' <div class="d-block text-muted text-truncate mt-n1"> '
          + messageItem.content
          + ' </div> '
          + ' </div> '
          + ' <div class="col-auto"> '
          + ' <a href="javascript:void(0);" class="list-group-item-actions" data-bs-toggle="modal" data-bs-target="#index_message_reply_modal"> '
          + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted bi bi-reply" width="24" height="24" '
          + ' fill="currentColor" viewBox="0 0 16 16"> '
          + ' <path d="M6.598 5.013a.144.144 0 0 1 .202.134V6.3a.5.5 0 0 0 .5.5c.667 0 2.013.005 3.3.822.984.624 1.99 1.76 2.595 3.876-1.02-.983-2.185-1.516-3.205-1.799a8.74 8.74 0 0 0-1.921-.306 7.404 7.404 0 0 0-.798.008h-.013l-.005.001h-.001L7.3 9.9l-.05-.498a.5.5 0 0 0-.45.498v1.153c0 .108-.11.176-.202.134L2.614 8.254a.503.503 0 0 0-.042-.028.147.147 0 0 1 0-.252.499.499 0 0 0 .042-.028l3.984-2.933zM7.8 10.386c.068 0 .143.003.223.006.434.02 1.034.086 1.7.271 1.326.368 2.896 1.202 3.94 3.08a.5.5 0 0 0 .933-.305c-.464-3.71-1.886-5.662-3.46-6.66-1.245-.79-2.527-.942-3.336-.971v-.66a1.144 1.144 0 0 0-1.767-.96l-3.994 2.94a1.147 1.147 0 0 0 0 1.946l3.994 2.94a1.144 1.144 0 0 0 1.767-.96v-.667z"/> '
          + ' </svg> '
          + ' </a> '
          + ' </div> '
          + ' </div> '
          + ' </div>';
        
        // 转为jQuery对象
        let messageItemEL = $(itemHtml);
        // 设置状态 bg-green bg-red status-dot-animated
        let statusDotEl = messageItemEL.find('.status-dot');
        let statusDescEl = messageItemEL.find('.index_message_item_statue');
        if (messageItem.state == 0) {
          // 未读
          statusDotEl.addClass('status-dot-animated bg-red');
          statusDescEl.html('[未读]');
        } else if (messageItem.state == 1) {
          // 已读
          statusDescEl.html('[已读]');
        } else if (messageItem.state == 2) {
          // 已回复
          statusDotEl.addClass('bg-green');
          statusDescEl.html('[已回复]');
        }
        // 绑定数据
        messageItemEL.data('message', messageItem);
        // 绑定点击事件
        messageItemEL.find('.list-group-item-actions, .index_message_title').click(function () {
          // 详情与回复页面数据
          // 站内信Id
          $('#index_message_detail_id').val(messageItem.id);
          // 标题
          $('#index_message_detail_title').html('收到来自 <strong>' + messageItem.postUser.nickname + '</strong> 的新消息');
          // 内容
          $('#index_message_detail_content').html(messageItem.content);
          // 接收者Id
          $('#index_message_reply_receive_user_id').val(messageItem.postUser.id);
          // 接收者信息
          $('#index_message_reply_receive_user_name').html('回复给: ' + messageItem.postUser.nickname);
          // 复位回复区域
          $('#index_message_reply_div').hide();
          // 复位接钮显示 
          $('#btn_index_message_reply').show();
          $('#btn_index_send_message_reply').hide();

        });

        // 添加到列表
        messageListDivEl.append(messageItemEL);

      });
4.3.9.4、更新状态

用户点击站内信,显示详情页面;更新未读状态的站内信为已读

请求
// 请求
POST http: //127.0.0.1:58080/message/markRead HTTP/1.1
Content-Type: application/x-www-form-urlencoAded
id= 1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
    /**
     * 读取站内信
     * @param id
     * @return
     */
    Message selectById(Long id);

    /**
     * 根据Id更新
     * @param id
     * @param state
     */
    void updateStateById(Long id,Byte state);
实现Service接口
    @Override
    public 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));
        }
        //调用DAO
        Message message=messageMapper.selectByPrimaryKey(id);
        //返回结果
        return message;
    }

    @Override
    public void updateStateById(Long id, Byte state) {
        //非空校验
        if (id==null || id<=0 || state<0 || state>2){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //构造更新对象
        Message updateMessage=new Message();
        updateMessage.setId(id);  //用户Id
        updateMessage.setState(state);  //站内信状态
        Date date=new Date();
        updateMessage.setUpdateTime(date);  //更新时间
        //调用DAO
        int row=messageMapper.updateByPrimaryKeySelective(updateMessage);
        if (row!=1){
            //打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
    }
对Service接口进行单元测试
    @Test
    void selectById() throws JsonProcessingException {
        Message message=messageService.selectById(1L);
        System.out.println(objectMapper.writeValueAsString(message));
        System.out.println("读取站内信成功");
    }

    @Test
    void updateStateById() {
        messageService.updateStateById(1L, (byte) 1);
        System.out.println("更新状态成功");
    }

实现Controller层
    @ApiOperation("更新状态为已读")
    @PostMapping("/markRead")
    public AppResult markRead(HttpServletRequest request,
                              @ApiParam("站内信Id") @RequestParam("id") @NonNull Long id){
        //根据Id查询内容
        Message message=messageService.selectById(id);
        //获取用户信息
        HttpSession session= request.getSession();
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        //接收方不是自己
        if (message!=null && user.getId()!= message.getReceiveUserId()){
            //打印日志
            log.warn("查询了不属于自己的站内信:userId="+user.getId()+",receiveUserId="+message.getReceiveUserId());
            //返回错误结果
            return AppResult.failed(ResultCode.FAILED);
        }
        //更新为已读状态
        messageService.updateStateById(message.getId(), (byte) 1);
        //返回结果
        return AppResult.success();
    }
测试接口

前端代码
    // ============ 处理站内信列表页面 ============
    function buildMessageList(messageList) {
      // 获取父标签
      let messageDivEl = $('#index_div_message');
      if (!messageList || messageList.length == 0) {
        messageDivEl.html('<strong>没有站内信</strong>');
        return;
      }
      // 获取站内信列表父标签
      let messageListDivEl = $('#index_div_message_list');
      messageListDivEl.html('');
      // 遍历结果
      messageList.forEach(messageItem => {
        let itemHtml = ' <div class="list-group-item"> '
          + ' <div class="row align-items-center"> '
          + ' <div class="col-auto"><span class="status-dot d-block"></span></div> '
          + ' <div class="col text-truncate"> '
          + ' <a href="javascript:void(0);" class="text-body d-block index_message_title" data-bs-toggle="modal" data-bs-target="#index_message_reply_modal"> '
          +  ' <span class="index_message_item_statue">[已读]</span> &nbsp; '
          + ' <span>来自 <strong> '+ messageItem.postUser.nickname 
          + ' </strong> 的消息</span></a> '
          + ' <div class="d-block text-muted text-truncate mt-n1"> '
          + messageItem.content
          + ' </div> '
          + ' </div> '
          + ' <div class="col-auto"> '
          + ' <a href="javascript:void(0);" class="list-group-item-actions" data-bs-toggle="modal" data-bs-target="#index_message_reply_modal"> '
          + ' <svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted bi bi-reply" width="24" height="24" '
          + ' fill="currentColor" viewBox="0 0 16 16"> '
          + ' <path d="M6.598 5.013a.144.144 0 0 1 .202.134V6.3a.5.5 0 0 0 .5.5c.667 0 2.013.005 3.3.822.984.624 1.99 1.76 2.595 3.876-1.02-.983-2.185-1.516-3.205-1.799a8.74 8.74 0 0 0-1.921-.306 7.404 7.404 0 0 0-.798.008h-.013l-.005.001h-.001L7.3 9.9l-.05-.498a.5.5 0 0 0-.45.498v1.153c0 .108-.11.176-.202.134L2.614 8.254a.503.503 0 0 0-.042-.028.147.147 0 0 1 0-.252.499.499 0 0 0 .042-.028l3.984-2.933zM7.8 10.386c.068 0 .143.003.223.006.434.02 1.034.086 1.7.271 1.326.368 2.896 1.202 3.94 3.08a.5.5 0 0 0 .933-.305c-.464-3.71-1.886-5.662-3.46-6.66-1.245-.79-2.527-.942-3.336-.971v-.66a1.144 1.144 0 0 0-1.767-.96l-3.994 2.94a1.147 1.147 0 0 0 0 1.946l3.994 2.94a1.144 1.144 0 0 0 1.767-.96v-.667z"/> '
          + ' </svg> '
          + ' </a> '
          + ' </div> '
          + ' </div> '
          + ' </div>';
        
        // 转为jQuery对象
        let messageItemEL = $(itemHtml);
        // 设置状态 bg-green bg-red status-dot-animated
        let statusDotEl = messageItemEL.find('.status-dot');
        let statusDescEl = messageItemEL.find('.index_message_item_statue');
        if (messageItem.state == 0) {
          // 未读
          statusDotEl.addClass('status-dot-animated bg-red');
          statusDescEl.html('[未读]');
        } else if (messageItem.state == 1) {
          // 已读
          statusDescEl.html('[已读]');
        } else if (messageItem.state == 2) {
          // 已回复
          statusDotEl.addClass('bg-green');
          statusDescEl.html('[已回复]');
        }
        // 绑定数据
        messageItemEL.data('message', messageItem);
        // 绑定点击事件
        messageItemEL.find('.list-group-item-actions, .index_message_title').click(function () {
          // 详情与回复页面数据
          // 站内信Id
          $('#index_message_detail_id').val(messageItem.id);
          // 标题
          $('#index_message_detail_title').html('收到来自 <strong>' + messageItem.postUser.nickname + '</strong> 的新消息');
          // 内容
          $('#index_message_detail_content').html(messageItem.content);
          // 接收者Id
          $('#index_message_reply_receive_user_id').val(messageItem.postUser.id);
          // 接收者信息
          $('#index_message_reply_receive_user_name').html('回复给: ' + messageItem.postUser.nickname);
          // 复位回复区域
          $('#index_message_reply_div').hide();
          // 复位接钮显示 
          $('#btn_index_message_reply').show();
          $('#btn_index_send_message_reply').hide();

          //发送请求,更新状态为已读
          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;
                }
              }
            });
          }

        });

        // 添加到列表
        messageListDivEl.append(messageItemEL);

      });
    }
4.3.9.5、回复

用户在站内信的详情页面点击回复按钮,显示回复区域;用户填写回复内容并提交至服务器;服务器会检查用户是否可以回复给接收者,如果接收者不是用户自己,则不允许回复;站内信的状态被更新为“已回复”。

请求
// 请求
POST http: //127.0.0.1:58080/message/reply HTTP/1.1
Content-Type: application/x-www-form-urlencoded
repliedId= 1 &receiveUserId= 2 &content=%E4%BD%A0%E5%A5%BD%E5% 95 % 8 A
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
    /**
     * 回复站内信
     * @param repliedId 被回复的站内信Id
     * @param message
     */
    @Transactional
    void reply(Long repliedId,Message message);
实现Service接口
    @Override
    public void reply(Long repliedId, Message message) {
        //非空校验
        if (repliedId==null || repliedId<=0){
            //打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            //抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        //校验repliedId对应的站内信状态
        Message existsMessage=messageMapper.selectByPrimaryKey(repliedId);
        if (existsMessage==null || existsMessage.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);
    }
对Service接口进行单元测试
    @Test
    void reply() {
        Message message=new Message();
        message.setReceiveUserId(3L);
        message.setPostUserId(1L);
        message.setContent("我是baekhyun");
        messageService.reply(1L,message);
        System.out.println("回复站内信成功");
    }

实现Controller层
    @ApiOperation("回复站内信")
    @PostMapping("/reply")
    public AppResult reply(HttpServletRequest request,
                           @ApiParam("要回复的站内信Id") @RequestParam("repliedId") @NonNull Long repliedId,
                           @ApiParam("站内信的内容") @RequestParam("content") @NonNull String content){
        //校验当前登录用户的状态
        HttpSession session=request.getSession(false);
        User user= (User) session.getAttribute(AppConfig.USER_SESSION_KEY);
        if (user==null || user.getState()==1){
            //返回错误格式
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);
        }
        //校验要回复的站内信状态
        Message existsMessage=messageService.selectById(repliedId);
        if (existsMessage==null || existsMessage.getDeleteState()==1){
            //返回错误描述
            return AppResult.failed(ResultCode.FAILED_MESSAGE_NOT_EXISTS);
        }
        //不能给自己回复
        if (user.getId()==existsMessage.getPostUserId()){
            //返回错误描述
            return AppResult.failed("不能回复自己的站内信");
        }
        //构造对象
        Message message=new Message();
        message.setPostUserId(user.getId());  //发送者
        message.setReceiveUserId(existsMessage.getPostUserId());  //接收者
        message.setContent(content);  //内容
        //调用Service
        messageService.reply(repliedId,message);
        //返回结果
        return AppResult.success();
    }
测试接口

前端代码
    // ============ 绑定发送按钮事件 ============
    $('#btn_index_send_message_reply').click(function () {
      // 校验用户输入
      let replyReceiveContentEl = $('#index_message_reply_receive_content');
      if (!replyReceiveContentEl.val()) {
        $.toast({
          heading: '警告',
          text: '请输入要回复的内容',
          icon: 'warning'
        });
        // 输入框
        replyReceiveContentEl.focus();
        retrun;
      }

      // 构造请求数据
      let postData = {
        repliedId: $('#index_message_detail_id').val(),
        receiveUserId : $('#index_message_reply_receive_user_id').val(),
        content: replyReceiveContentEl.val()
      };

      // 发送请求 message/reply
      // 回复成功后刷新未读标识和站内信列表
      // requestMessageUnreadCount();
      // requestMessageList();
      // // 清空输入区
      // cleanMessageReplyForm ();
      $.ajax ({
        type:'POST',
        url:'message/reply',
        contentType : 'application/x-www-form-urlencoded',
        data:postData,
        //回调
        success:function(respData){
          //根据code的值判断响应是否成功
          if(respData.code==0){
            //回复成功后刷新未读标识和站内信列表
            requestMessageUnreadCount();
            requestMessageList();
            // 清空输入区
            cleanMessageReplyForm ();
            //提示信息
            $.toast({
              heading:'成功',
              text:respData.message,
              icon:'success'
            })
          }else{
            //失败
            $.toast({
              heading:'失败',
              text:respData.message,
              icon:'warning'
            })
          }
        },
        error:function(){
          $.toast({
            heading:'错误',
            text:'访问网站出现问题,请与管理员联系',
            icon:'error'
          })
        }
      });

除以上功能外,还可对论坛系统进行扩充(如分页显示、记录用户点赞的帖子、回复楼中楼等),具体实现将在之后进行 

五、发布部署

1、执行SQL脚本

将SQL脚本上传至服务器数据库内

2、修改代码中数据源的配置

确认数据库服务器的地址,数据库名,用户名,密码,并修改代码

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_forum?characterEncoding=utf8&useSSL=false # 数据库连接串
    username: root # 数据库用户名
    password:  # 数据库密码
    driver-class-name: com.mysql.jdbc.Driver # 数据库驱动类

3、修改配置文件中的日志级别与日志文件路径

#日志配置
logging:
  pattern:
    dateformat: yyyy-MM-dd HH:mm:ss
  level:
    root: info  #默认日志级别
    com.example.forum_system: debug  #指定包的日志级别
  file:
    path: /log/forum  #日志保存的路径

4、打包程序

5、上传到服务器

6、验证访问

基于ssm前后端分离的论坛系统

如需完整代码,可私信博主!!!

  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现步骤: 1.后端实现 首先,我们需要建立一个Spring Boot项目,并添加相关依赖,如mybatisspring-web等。 1.1 创建数据库表 我们需要在数据库中创建一个用户表,用于保存用户信息。在这个表中至少要包含用户ID、用户名、密码三个字段。 1.2 创建实体类 创建一个User实体类,用于映射数据库中的用户表。 1.3 创建Mapper 创建一个UserMapper接口,用于操作数据库中的用户表,包括查询、添加、修改和删除等操作。 1.4 创建Service 创建一个UserService接口,用于封装业务逻辑,如用户注册、登录等。 1.5 创建Controller 创建一个UserController,用于处理前端请求,包括注册、登录等。 2.前端实现 前端我们采用Vue.js框架,用于构建用户界面。 2.1 创建页面 创建一个登录页面和一个注册页面。在这个页面中,我们需要使用Vue.js框架来定义页面组件,包括输入框、按钮等。 2.2 定义数据模型 我们需要定义一个User类,用于保存用户信息,包括用户名、密码等。 2.3 发送请求 在用户注册或登录时,我们需要向后端发送请求,以获取后端返回的数据。我们可以使用axios库来发送请求。 3.整合前后端 在前后端分离的模式下,前端与后端之间需要通过API来通信。在这个例子中,我们可以使用JSON格式来传递数据。 3.1 定义API 我们需要定义一组API,用于处理前端发送的请求。这些API可以使用Spring MVC框架来实现。 3.2 处理请求 在后端中,我们需要根据前端发送的请求,执行相应的业务逻辑。我们可以使用Spring MVC框架来处理请求。 3.3 返回响应 在后端处理完请求后,需要将结果返回给前端。我们可以使用JSON格式来返回响应数据。 总结: 通过以上步骤,我们可以实现一个基于SSM框架的前后端分离的登录注册功能。在这个例子中,我们使用了Vue.js框架来构建用户界面,使用Spring MVC框架来处理前端请求,使用MyBatis框架来操作数据库
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值