目录
前言
个人论坛系统是一种在线社交平台,为用户提供了丰富的功能,让他们能够轻松地创建帖子、分享信息、讨论话题以及互动交流。
整个项目在开发过程中直接进行了单元测试,具体的自动化测试(功能、界面)在此处进行。
一、项目背景
以下是该系统的主要业务功能:
-
用户注册:允许用户创建自己的账户,填写个人信息并进行注册,以便能够使用论坛系统的各项功能。
-
用户登录:已注册用户可以通过输入用户名和密码登录到论坛系统,以便访问其个人信息和发表帖子。
-
论坛主界面:提供了一个主页面,用户可以在这里浏览各个板块的帖子列表,切换不同的板块浏览不同的内容,同时显示当前用户的信息,如用户名、头像等,还可以在此页面发布新帖子。
-
帖子详情页:用户可以点击帖子标题或摘要进入帖子详情页面,可以查看帖子的详细内容,并进行点赞、编辑、删除等操作。
-
用户中心页:用户可以在个人中心页查看和修改自己的个人信息,包括用户名、头像、个人简介等,还可以查看收到的站内信(私信),并回复他人的信息。
-
站内信功能:用户可以在论坛系统内部发送和接收私信,进行一对一的交流和沟通,也可以在帖子下面回复其他用户的信息,进行公开的讨论和互动。
以上功能使得个人论坛系统成为一个活跃的在线社交平台,为用户提供了丰富的交流和互动机会。
二、相关技术及工具
构架 | 基于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、继续配置
<!-- 编译环境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&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格式的字符串时,你可以定义一个类来封装返回的数据。这个类可以包含以下属性:
- 状态码(status code):表示请求的处理状态,比如成功、失败等。
- 描述信息(message):对处理状态的简要描述,用于提示用户或开发者。
- 返回结果数据(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.1Content-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.1Content-Type: application/x-www-form-urlencodedusername=xxx&password= xxx
响应
// 响应HTTP/ 1.1 200Content-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">