项目概述
实现了基于 Spring 的前后端分离版本的在线论坛系统,实现了用户登录注册、站内私信,帖⼦列表, 发布帖⼦, 回复帖⼦,点赞帖子,搜索帖子等功能。
项目体验:http://175.178.163.221:58080/sign-in.html
项目源码:https://gitee.com/dragon-yushuang/forum.git
技术实现过程
- 使⽤统⼀返回格式+全局错误信息定义处理前后端交互时的返回结果
- 使⽤@ControllerAdvice+@ExceptionHandler实现全局异常处理
- 使⽤拦截器实现⽤⼾登录校验
- 使⽤MybatisGeneratorConfig⽣成常的增删改查⽅法
- 集成Swagger实现⾃动⽣成API测试接⼝
- 使⽤jQuery完成AJAX请求,并处理HTML⻚⾯标签
相关技术及⼯具
服务器端技术 :Spring 、Spring Boot 、Spring MVC 、MyBatis
浏览器端技术 :HTML, CSS, JavaScript 、jQuery 、Bootstrap
数据库 :MySQL
项⽬构建⼯具 :Maven
版本控制⼯具 :Git + GITEE
核心功能
- 登录注册
- 帖⼦按版块分类,对帖子热度进行排行,按关键字搜索帖子
- 帖⼦列表, 发布帖⼦, 删除帖⼦, 回复帖⼦,点赞帖子等功能
- 站内私信其他用户,个⼈主⻚的展⽰,个人信息密码编辑,密码修改
技术实现过程
- 使⽤统⼀返回格式+全局错误信息定义处理前后端交互时的返回结果
- 使⽤@ControllerAdvice+@ExceptionHandler实现全局异常处理
- 使⽤拦截器实现⽤⼾登录校验
- 使用MD5加盐加密实现密码加密,保护用户账号安全性
- 使⽤MybatisGeneratorConfig⽣成常的增删改查⽅法
- 集成Swagger实现⾃动⽣成API测试接⼝
- 使⽤jQuery完成AJAX请求,并处理HTML⻚⾯标签
数据库设计
经过简单分析:“版块类别” 和 “版块帖⼦数量” 都可以归结到 “版块” 类,做为 “版块” 类的属性;“帖⼦标题” 和 “帖⼦正⽂” 都可以归结到 “帖⼦” 类,做为 “帖⼦” 类的属性;“权限” 可以归结到 “⽤⼾”类,做为“⽤⼾”类的属性。⾄此,针对发布帖⼦这个⽤例,就确定了三个
类,分别是:⽤⼾、版块、帖⼦。
再者就是帖子回复在帖子详情里,站内私信在主页,因此还有帖子回复表和站内私信表。
-- 创建数据库,并指定字符集
-- ----------------------------
drop database if exists forum_db;
create database forum_db character set utf8mb4 collate utf8mb4_general_ci;
-- 选择数据库
use forum_db;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 创建帖⼦表 t_article
-- ----------------------------
DROP TABLE IF EXISTS `t_article`;
CREATE TABLE `t_article` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '帖⼦编号,主键,⾃增',
`boardId` bigint(20) NOT NULL COMMENT '关联板块编号,⾮空',
`userId` bigint(20) NOT NULL COMMENT '发帖⼈,⾮空,关联⽤⼾编号',
`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT
NULL COMMENT '标题,⾮空,最⼤⻓度100个字符',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL
COMMENT '帖⼦正⽂,⾮空',
`visitCount` int(11) NOT NULL DEFAULT 0 COMMENT '访问量,默认0',
`replyCount` int(11) NOT NULL DEFAULT 0 COMMENT '回复数据,默认0',
`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0正常 1 禁⽤,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0 否 1 是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,⾮空',
`updateTime` datetime NOT NULL COMMENT '修改时间,精确到秒,⾮空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE =
utf8mb4_general_ci COMMENT = '帖⼦表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `t_article_reply`;
CREATE TABLE `t_article_reply` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号,主键,⾃增',
`articleId` bigint(20) NOT NULL COMMENT '关联帖⼦编号,⾮空',
`postUserId` bigint(20) NOT NULL COMMENT '楼主⽤⼾,关联⽤⼾编号,⾮空',
`replyId` bigint(20) NULL DEFAULT NULL COMMENT '关联回复编号,⽀持楼中楼',
`replyUserId` bigint(20) NULL DEFAULT NULL COMMENT '楼主下的回复⽤⼾编号,⽀持楼
中楼',
`content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT
NULL COMMENT '回贴内容,⻓度500个字符,⾮空',
`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1禁⽤,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,⾮空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,⾮空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE =
utf8mb4_general_ci COMMENT = '帖⼦回复表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建版块表 t_board
-- ----------------------------
DROP TABLE IF EXISTS `t_board`;
CREATE TABLE `t_board` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '版块编号,主键,⾃增',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL
COMMENT '版块名,⾮空',
`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '帖⼦数量,默认0',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序优先级,升序,默认0,',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态,0 正常,1禁⽤,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,⾮空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,⾮空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE =
utf8mb4_general_ci COMMENT = '版块表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建站内信表 for t_message
-- ----------------------------
DROP TABLE IF EXISTS `t_message`;
CREATE TABLE `t_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '站内信编号,主键,⾃增',
`postUserId` bigint(20) NOT NULL COMMENT '发送者,并联⽤⼾编号',
`receiveUserId` bigint(20) NOT NULL COMMENT '接收者,并联⽤⼾编号',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT
NULL COMMENT '内容,⾮空,⻓度255个字符',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0未读 1已读,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,⾮空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,⾮空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE =
utf8mb4_general_ci COMMENT = '站内信表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建⽤⼾表 for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '⽤⼾编号,主键,⾃增',
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT
NULL COMMENT '⽤⼾名,⾮空,唯⼀',
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT
NULL COMMENT '加密后的密码',
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT
NULL COMMENT '昵称,⾮空',
`phoneNum` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL
DEFAULT NULL COMMENT '⼿机号',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL
DEFAULT NULL COMMENT '邮箱地址',
`gender` tinyint(4) NOT NULL DEFAULT 2 COMMENT '0⼥ 1男 2保密,⾮空,默认2',
`salt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL
COMMENT '为密码加盐,⾮空',
`avatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
NULL DEFAULT NULL COMMENT '⽤⼾头像URL,默认系统图⽚',
`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '发帖数量,⾮空,默认0',
`isAdmin` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否管理员,0否 1是,默认0',
`remark` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL
DEFAULT NULL COMMENT '备注,⾃我介绍',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1 禁⾔,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `user_username_uindex`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE =
utf8mb4_general_ci COMMENT = '⽤⼾表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
-- 写⼊版块信息数据
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (1, 'Java', 0, 1, 0, 0,
'2023-01-14 19:02:18', '2023-01-14 19:02:18');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (2, 'C++', 0, 2, 0, 0, '2023-
01-14 19:02:41', '2023-01-14 19:02:41');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (3, '前端技术', 0, 3, 0, 0,
'2023-01-14 19:02:52', '2023-01-14 19:02:52');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (4, 'MySQL', 0, 4, 0, 0,
'2023-01-14 19:03:02', '2023-01-14 19:03:02');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (5, '⾯试宝典', 0, 5, 0, 0,
'2023-01-14 19:03:24', '2023-01-14 19:03:24');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (6, '经验分享', 0, 6, 0, 0,
'2023-01-14 19:03:48', '2023-01-14 19:03:48');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (7, '招聘信息', 0, 7, 0, 0,
'2023-01-25 21:25:33', '2023-01-25 21:25:33');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (8, '福利待遇', 0, 8, 0, 0,
'2023-01-25 21:25:58', '2023-01-25 21:25:58');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`,
`deleteState`, `createTime`, `updateTime`) VALUES (9, '灌⽔区', 0, 9, 0, 0,
'2023-01-25 21:26:12', '2023-01-25 21:26:12');
项目搭建与软件开发
application.yml配置
spring:
application:
name: 线上论坛 # 项目名
output:
ansi:
enabled: ALWAYS # 控制台输出彩色日志
profiles:
active: dev
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER #Springfox-Swagger兼容性配置
favicon:
enable: false
datasource:
url: jdbc:mysql://127.0.0.1:3306/forum_db?characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# JSON序列化配置
jackson:
# date-format: yyyy-MM-dd HH:mm:ss # 日期格式
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: NON_NULL # 不为null时序列化
# 设置 Mybatis 的 xml 保存路径
mybatis:
mapper-locations: classpath:mapper/**/*.xml # 指定 xxxMapper.xml的扫描路径
# mapper-locations: classpath:/resources/mapper/*Mapper.xml
type-aliases-package: com.example.forum.dao
configuration: # 配置打印 MyBatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #自动驼峰转换
# 配置打印 MyBatis 执行的 SQL
# 日志配置
logging:
pattern:
dateformat: yyyy-MM-dd HH:mm:ss # 日期格式
level:
root: info # 默认日志级别
com.bitejiuyeke.forum: debug # 指定包的日志级别
file:
path: ./logs # 日志保存目录
server:
port: 58080
# 项目自定义相关配置
forum:
login: # 关于登录的配置项
url: sign-in.html # 未登录状况下强制跳转页面
index: # 关于首页的配置项
board-num: 10 # 首页要显示的版块数量
公共部分
创建⼯程结构
⼯具层(common) => 统⼀返回类, 定义状态码
配置层(config) => session,Mybatis,Swagger配置
控制器层(controller) => 提供URL映射,⽤来接收参数并做校验,调⽤Service中的业务代码,返回执⾏结果
持久层(dao) => 数据库访问
异常捕获(exception) => 统⼀异常处理
拦截(interceptor) => 登录拦截器
实体层(model) => 实体类
服务层(service) => 业务处理相关的接⼝与实现,所有业务都在Services中实现
工具类(utils) => MD5+盐加密
resources/mapper => Mybaits映射⽂件,配置数据库实体与类之间的映射关系
resources/static => 前端资源
resources/mybatis => 前端资源
定义状态码
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_EXISTS (1201, "版块已存在"),
FAILED_BOARD_NOT_EXISTS (1202, "版块不存在"),
ERROR_SERVICES (2000, "服务器内部错误"),
ERROR_IS_NULL (2001, "IS NULL.");
long code;
String message;
public long getCode() {
return code;
}
public String getMessage() {
return message;
}
ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() {
return "code = " + code + ", message = " + message + ".";
}
}
关于数据库的操作
使用Mybatis-Generator生成
- 实体类
- 映射⽂件xxxMapper.xml
- Dao类,xxxMapper.java
pom.xml配置:
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>${mybatis-generator-plugin-version}</version>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<phase>deploy</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<!-- 相关配置 -->
<configuration>
<!-- 打开日志 -->
<verbose>true</verbose>
<!-- 允许覆盖 -->
<overwrite>true</overwrite>
<!-- 配置文件路径 -->
<configurationFile>
src/main/resources/mybatis/generatorConfig.xml
</configurationFile>
</configuration>
</plugin>
generatorConfig.xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 驱动包路径,location中路径替换成自己本地路径 -->
<classPathEntry location="D:\JavaUltimate\.m2\repository\com\mysql\mysql-connector-j\8.0.33\mysql-connector-j-8.0.33.jar"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 禁用自动生成的注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
<property name="suppressDate" value="true"/>
</commentGenerator>
<!-- 连接配置 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/forum_db?characterEncoding=utf8&useSSL=false"
userId="root"
password="123456">
</jdbcConnection>
<javaTypeResolver>
<!-- 小数统一转为BigDecimal -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 实体类生成位置 -->
<javaModelGenerator targetPackage="com.example.forum.model" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- mapper.xml生成位置 -->
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- DAO类生成位置 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.example.forum.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 配置生成表与实例, 只需要修改表名tableName, 与对应类名domainObjectName 即可-->
<table tableName="t_article" domainObjectName="Article" enableSelectByExample="false"
enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
enableUpdateByExample="false">
<!-- 类的属性用数据库中的真实字段名做为属性名, 不指定这个属性会自动转换 _ 为驼峰命名规则-->
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_article_reply" domainObjectName="ArticleReply" enableSelectByExample="false"
enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_board" domainObjectName="Board" enableSelectByExample="false" enableDeleteByExample="false"
enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_message" domainObjectName="Message" enableSelectByExample="false"
enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_user" domainObjectName="User" enableSelectByExample="false" enableDeleteByExample="false"
enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
</context>
</generatorConfiguration>
自己编写
加盐加密
项⽬中使⽤commons-codec,它是Apache提供的⽤于摘要运算、编码解码的⼯具包。常⻅的编码 解码⼯具Base64、MD5、Hex、SHA1、DES等。
<!-- 编码解码加密⼯具包-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
盐生成:
public class UUIDUtils {
/**
* 生成一个标准的36字符的UUID
* @return
*/
public static String UUID_36 () {
return UUID.randomUUID().toString();
}
/**
* 生成一个32字符的UUID
* @return
*/
public static String UUID_32 () {
return UUID.randomUUID().toString().replace("-", "");
}
}
MD5加密:
public class MD5Utils {
/**
* 普通MD5加密
* @param str 原始字符串
* @return 一次MD5加密后的密文
*/
public static String md5 (String str) {
return DigestUtils.md5Hex(str);
}
/**
* 原始字符串与Key组合进行一次MD5加密
* @param str 原始字符串
* @param key
* @return 组合字符串一次MD5加密后的密文
*/
public static String md5 (String str, String key) {
return DigestUtils.md5Hex(str + key);
}
/**
* 原始字符串加密后与扰动字符串组合再进行一次MD5加密
* @param str 原始字符串
* @param salt 扰动字符串
* @return 加密后的密文
*/
public static String md5Salt (String str, String salt) {
return DigestUtils.md5Hex(DigestUtils.md5Hex(str) + salt);
}
/**
* 校验原文与盐加密后是否与传入的密文相同
* @param original 原字符串
* @param salt 扰动字符串
* @param ciphertext 密文
* @return true 相同, false 不同
*/
public static boolean verifyOriginalAndCiphertext (String original, String salt, String ciphertext) {
String md5text = md5Salt(original, salt);
if (md5text.equalsIgnoreCase(ciphertext)) {
return true;
}
return false;
}
}
以登录为例
在dao层的UserMapper定义方法
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return
*/
User selectByName (String username);
在对应映射文件UserExtMapper.xml中编写sql语句:
<!-- 根据用户名查询用户信息-->
<select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user
where username = #{username,jdbcType=VARCHAR}
</select>
在service层的IUserService定义接口:
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return
*/
User selectByName (String username);
/**
* 创建普通用户
* @param user 用户信息
*/
void createNormalUser(User user);
/**
* 用户登录
* @param username 用户名
* @param password 密码
* @return
*/
User login(String username, String password);
在UserServiceImpl中实现接口:
@Resource
private UserMapper userMapper;
@Override
public User selectByName(String username) {
// 非空校验
if (StringUtils.isEmpty(username)) {
// 打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 根据用户名查询用户信息
User user = userMapper.selectByName(username);
// 返回结果
return user;
}
@Override
public User login(String username, String password) {
// 非空校验
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
// 打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 根据用户名查询用户信息
User user = selectByName(username);
// 校验用户是否存在
if (user == null) {
// 打印日志
log.info(ResultCode.FAILED_USER_NOT_EXISTS + ", username = " + username);
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
}
// 校验密码是否正确
String encryptPassword = MD5Utils.md5Salt(password, user.getSalt());
if (!encryptPassword.equalsIgnoreCase(user.getPassword())) {
// 打印日志
log.info( "密码输入错误 , username = " + username + ", password = " + password);
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
}
// 返回用户信息
return user;
}
测试
@Test
void login() throws JsonProcessingException {
// 正常用户
User user = userService.login("yuni", "123456");
System.out.println(objectMapper.writeValueAsString(user));
// // 错误用户
// user = userService.login("123456", "123456");
// System.out.println(objectMapper.writeValueAsString(user));
}
在controller层的UserController中编写(session):
@ApiOperation("用户登录")
@PostMapping("login")
@ResponseBody
public AppResult login(HttpServletRequest request,
@ApiParam("用户名") @RequestParam("username") @NonNull String username,
@ApiParam("密码") @RequestParam("password") @NonNull String password){
// 调用Service
User user = userService.login(username, password);
// 在session中保存当前登录的用户信息
// 1. 获取session对象
HttpSession session = request.getSession(true);
// 2. 把用户信息保存在Session中
session.setAttribute(AppConfig.SESSION_USER_KEY, user);
// 返回成功
return AppResult.success("登录成功");
}
进行接口测试(Swagger接口详情在下边介绍)
http://127.0.0.1:58080/swagger-ui/index.html
编写前端接口:
// 发送AJAX请求,成功后跳转到index.html
$.ajax({
// 请求的方法类型
type : 'POST',
// API的URL
url : 'user/login',
// 数据格式
contentType : 'application/x-www-form-urlencoded',
// 提交的数据
data : postData,
// 成功回调
success : function(respData) {
if (respData.code == 0) {
// 成功
location.assign('/index.html');
} else {
// 失败
$.toast({
heading: '警告',
text: respData.message,
icon: 'warning'
});
}
},
// 失败 (HTTP)
error : function() {
$.toast({
heading: '错误',
text: '访问出现问题,请联系管理员',
icon: 'error'
});
}
});
拦截器
public class LoginInterceptor implements HandlerInterceptor {
// 从配置⽂件中获取默认登录⻚的URL
// 从配置文件中读取配置内容
@Value("${forum.login.url}")
private String defaultURL;
/**
* 预处理(请求的前置处理)回调方法<br/>
*
* @return true 继续请求流程 </br> false 中止请求流程
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取session
HttpSession session = request.getSession(false);
// 判断session和session中保存的用户信息是否有效
if (session != null && session.getAttribute(AppConfig.SESSION_USER_KEY) != null) {
// 校验通过
return true;
}
// 校验不通过时要处理的逻辑
// 1. 返回一个错误的HTTP状态码
// response.setStatus(403);
// 2. 跳转到某一个页面
// response.sendRedirect("/sign-in.html");
// 对URL前缀做校验(确保目标URL从根目录开发)
if (!defaultURL.startsWith("/")) {
defaultURL = "/" + defaultURL;
}
response.sendRedirect(defaultURL);
// 校验不能过
return false;
}
}
@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下所有,与swag
.excludePathPatterns("/dist/**") // 排除所有静态⽂件
.excludePathPatterns("/image/**")
.excludePathPatterns("/**.ico")
.excludePathPatterns("/js/**");
}
}
接口
http://127.0.0.1:58080/swagger-ui/index.html#/
解决SpringBoot 2.6.0以上与Springfox3.0.0 不兼容的问题,涉及SpringBoot 版本升级过程中的⼀些内部实现变化,具体说明在修改配置⽂件部分
使用Swagger生成接口:
@EnableOpenApi
@Configuration
public class SwaggerConfig {
@Bean
public Docket createApi() {
Docket docket = new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.forum.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
// 配置API基本信息
private ApiInfo apiInfo() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("线上论坛系统API")
.description("线上论坛系统前后端分离API测试")
.contact(new Contact("Bit Tech",
"https://edu.bitejiuyeke.com", "3304419133@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));
}
}
spring
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER #Springfox-Swagger兼容性配置
API常⽤注解 :
• @Api: 作⽤在Controller上,对控制器类的说明
◦ tags=“说明该类的作⽤,可以在前台界⾯上看到的注解”
• @ApiModel: 作⽤在响应的类上,对返回响应数据的说明
• @ApiModelProerty:作⽤在类的属性上,对属性的说明
• @ApiOperation: 作⽤在具体⽅法上,对API接⼝的说明
• @ApiParam: 作⽤在⽅法中的每⼀个参数上,对参数的属性进⾏说明
格式规范:
// 请求
GET /user/info HTTP/1.1
GET /user/info?id=1 HTTP/1.1
// 响应
HTTP/1.1 200
Content-type: applicatin/json
{
"code": 200,
"message": "成功",
"data": null
}
用户接口
板块接口
帖子接口
站内私信接口
帖子回复接口
<!-- API⽂档⽣成,基于swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>$
package com.example.forum.config;
import org.springframework.core.env.Environment;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author longyushuang
* @description: $
* @param:
* @return:
* @date:
*/
@EnableOpenApi
@Configuration
public class SwaggerConfig {
@Bean
public Docket createApi() {
Docket docket = new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.forum.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
// 配置API基本信息
private ApiInfo apiInfo() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("线上论坛系统API")
.description("线上论坛系统前后端分离API测试")
.contact(new Contact("Bit Tech",
"https://edu.bitejiuyeke.com", "3304419133@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));
}
}
成果展示
登录页面
注册:
主页:
帖子详情:
![(3Z~QN$TM}KSSB_]5H0Z{]S.png](https://img-blog.csdnimg.cn/img_convert/20569172ce81ecb0db2f01d90c39b28e.png#averageHue=#e4e6cd&clientId=u44a7ef89-db93-4&from=paste&height=1603&id=u4bbae20a&originHeight=2004&originWidth=1912&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=137536&status=done&style=none&taskId=ud684c8d4-a674-4c78-a993-00dea371b8c&title=&width=1529.6)
个人主页:
个人中心:
![@BC(V]4PD2}8@_%4TD{%ACH.png](https://img-blog.csdnimg.cn/img_convert/3ede061fd71da1fc066973ee59de1765.png#averageHue=#f7f1cc&clientId=u44a7ef89-db93-4&from=paste&height=1182&id=u4a13f06f&originHeight=1477&originWidth=1910&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=98270&status=done&style=none&taskId=u7ab8e635-7a3a-43ae-83c2-475c35566bd&title=&width=1528)
发新帖:
![@6W$KM%[C5U}YZYB]E]}CIE.png](https://img-blog.csdnimg.cn/img_convert/f264b17c4a911ad3d8f4e4dc98fa1285.png#averageHue=#efd8ad&clientId=u44a7ef89-db93-4&from=paste&height=944&id=u792a3e9c&originHeight=1180&originWidth=1849&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=70483&status=done&style=none&taskId=u19cd8777-e4e6-49a4-916e-d8411b5940b&title=&width=1479.2)
私信:
测试
测试用例
测试环境
操作系统:Windows 10家庭版
项目运行:CentOS、maven、JDK1.8
浏览器:Chorme、Edge、FireFox
自动化脚本环境:IDEA2022.2.3
网络:http://175.178.163.221:58080/
测试技术: 主要采用自动化测试以及手工测试
测试人员: 我
功能测试
对登陆页面进行的测试
场景1:输入账号和密码可以正常登录,点击登录
预期结果:页面跳转到首页
实际结果:页面跳转到首页,与预期结果相符
场景2:输入正确的用户名,密码错误,点击登录
预期结果:提示密码错误
实际结果:提示密码错误,与预期结果相符
对注册页面进行的测试
场景1:输入数据库不存在的用户名,昵称,密码与确认密码相同,点击注册
预期结果:页面跳转到登录页面
实际结果:页面跳转到登录页面,与预期结果相符
场景2:输入数据库不存在的用户名,昵称,密码与确认密码不相同,点击注册
预期结果:显示‘请检查确认密码’
实际结果:显示‘请检查确认密码’,与预期结果相符
场景3:输入数据库存在的用户名,昵称,密码与确认密码相同,点击注册
预期结果:提示用户已经存在
实际结果:提示用户已经存在,与预期结果相符
对首页进行的测试
场景1:未登录状态输入首页地址
预期结果:跳转到登录页面
实际结果:跳转到登录页面,与预期结果相符
场景2:登录后,跳转到首页,在搜索栏输入关键字‘数据库’,点击搜索
预期结果:展示与数据库有关的帖子列表
实际结果:展示与数据库有关的帖子列表,与预期结果相符
场景3:登录后,跳转到首页,在搜索栏输入关键字‘1’(数据库中不存在的标题),点击搜索
预期结果:展示‘还没有帖子’
实际结果:展示‘还没有帖子’,与预期结果相符
场景4:登录后,搜索后,点击任意一个板块
预期结果:展示具体板块的帖子列表
实际结果:展示与搜索关键字有关的帖子列表,与预期结果不相符
场景5:登录后,点击板块‘Java’,
预期结果:展示‘Java’板块的帖子列表,显示板块名,帖子总数
实际结果:展示‘Java’板块的帖子列表,与预期结果相符
场景6:登录后,点击板块‘Java’,
预期结果:展示‘Java’板块的帖子列表
实际结果:展示‘Java’板块的帖子列表,与预期结果相符
场景6:登录后,点击板块‘排行榜’,
预期结果:展示按(50%点赞数量+50%浏览数量)降序排列的帖子列表,展示帖子总数量
实际结果:展示按(50%点赞数量+50%浏览数量)降序排列的帖子列表,帖子总数量为0,与预期结果不相符
场景7:登录后,点击具体的一个帖子
预期结果:跳转到帖子详情页面,展示帖子的具体信息,帖子的作者,创作时间,浏览量
实际结果:跳转到帖子详情页面,展示帖子的具体信息,帖子的作者,创作时间,浏览量,与预期结果相符
场景8:登录后,点击‘发布帖子’,
预期结果:跳转到发布帖子页面
实际结果:跳转到发布帖子页面,与预期结果相符
场景9:登录后,右上点击‘铃铛’,
预期结果:跳出所有站内私信
实际结果:跳出所有站内私信,与预期结果相符
场景9:登录后,右上点击用户,点击我的帖子
预期结果:跳转到我的帖子页面,展示所有作者发布的帖子,个人介绍,发贴数,邮箱,注册日期
实际结果:跳转到我的帖子页面,展示所有作者发布的帖子,未展示个人介绍,发贴数、邮箱、注册日期不正确,与预期结果不相符
场景9:登录后,右上点击用户,点击个人中心
预期结果:跳转到个人中心,展示个人信息
实际结果:跳转到个人中心,展示个人信息,与预期结果相符
其他测试不一一展示了
兼容性测试
编号 | 具体内容 | 测试数据 | 步骤 | 预期结果 | 实际结果 |
---|---|---|---|---|---|
1 | IE浏览器 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 与预期结果一致 |
2 | QQ浏览器 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 与预期结果一致 |
3 | Chorme浏览器 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 与预期结果一致 |
4 | Firefox浏览器 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 与预期结果一致 |
5 | 各个浏览器的不同版本测试 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 未测试 |
6 | Mac系统 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 未测试 |
7 | Linux系统 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 未测试 |
8 | 华硕、联想、苹果、华为等不同电脑 | 账号:test01,密码:123456 | 打开浏览器,输入http://175.178.163.221:58080/sign-in.html,输入账号密码,点击登录 | 可以正常使用 | 未测试 |
性能测试
登录:
Action()
{
lr_rendezvous("login");
lr_start_transaction("Login");
web_submit_data("login",
"Action=http://175.178.163.221:58080/user/login",
"Method=POST",
"RecContentType=application/json",
"Referer=http://175.178.163.221:58080/sign-in.html",
"Snapshot=t1.inf",
"Mode=HTML",
ITEMDATA,
"Name=username", "Value={user_name}", ENDITEM,
"Name=password", "Value={password}", ENDITEM,
EXTRARES,
"Url=info", "Referer=http://175.178.163.221:58080/index.html", ENDITEM,
"Url=../board/topList", "Referer=http://175.178.163.221:58080/index.html", ENDITEM,
"Url=../message/getUnreadCount", "Referer=http://175.178.163.221:58080/index.html", ENDITEM,
"Url=../message/getAll", "Referer=http://175.178.163.221:58080/index.html", ENDITEM,
"Url=../article/getAllByBoardId", "Referer=http://175.178.163.221:58080/index.html", ENDITEM,
LAST);
lr_end_transaction("Login",LR_AUTO);
return 0;
}