基于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">
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值