Online forum system

Project Overview

Implemented a Spring-based front-end and back-end separated version of the online forum system, which implemented user login registration, site private messages, post list, publish posts, reply to posts, like posts, search posts and other functions.
Project experience: http://175.178.163.221:58080/sign-in.html
Project source code: a>https://gitee.com/dragon-yushuang/forum.git

Technology implementation process

  1. Use unified return format + global error information to define return results when processing front-end and back-end interactions
  2. Use @ControllerAdvice+@ExceptionHandler to implement global exception handling
  3. Use interceptors to implement user login verification
  4. Use MybatisGeneratorConfig to generate regular add, delete, modify and search methods
  5. Integrate Swagger to automatically generate API test interfaces
  6. Use jQuery to complete AJAX requests and process HTML page tags

Related technologies and tools

Server-side technologies: Spring, Spring Boot, Spring MVC, MyBatis
Browser-side technologies: HTML, CSS, JavaScript, jQuery, Bootstrap
Database: MySQL
Project construction tool: Maven
Version control tool: Git + GITEE

Core functions

  • log in Register
  • Posts are classified by sections, ranked by post popularity, and searched for posts by keywords.
  • Post list, publish posts, delete posts, reply to posts, like posts and other functions
  • Private messages to other users on the site, display of personal homepage, personal information password editing, password modification

Technology implementation process

  1. Use unified return format + global error information to define return results when processing front-end and back-end interactions
  2. Use @ControllerAdvice+@ExceptionHandler to implement global exception handling
  3. Use interceptors to implement user login verification
  4. Use MD5 salt encryption to encrypt passwords and protect user account security
  5. Use MybatisGeneratorConfig to generate regular add, delete, modify and search methods
  6. Integrate Swagger to automatically generate API test interfaces
  7. Use jQuery to complete AJAX requests and process HTML page tags

Database Design

After simple analysis: "Section category" and "Section number" can be attributed to the "Section" class, as attributes of the "Section" class; "Post title" and "Post text" All can be attributed to the "Post" class, as an attribute of the "Post" class; "Permissions" can be attributed to the "User" class, as an attribute of the "User" class. So far, for the use case of posting posts, three categories have been identified: users, sections, and posts. Furthermore, post replies are in the post details, and site private messages are on the homepage, so there is also a post reply table and a site private message table.

-- 创建数据库,并指定字符集
-- ----------------------------
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 '是否删除 01 是,默认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 '是否删除 01是,默认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 '012保密,⾮空,默认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 '是否管理员,01是,默认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 '是否删除 01是,默认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');

    

Project construction and software development

application.yml configuration

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 # 首页要显示的版块数量



Public section

Create project structure

Tool layer (common) => Unified return class, define status code
Configuration layer (config) => session, Mybatis, Swagger configuration< a i=2> Controller layer (controller) => Provides URL mapping, which is used to receive parameters and verify them, call the business code in the Service, and return execution results Persistence layer ( dao) => Database access Exception catching (exception) => Unified exception handling Interceptor => Login interceptor Service layer (service) => Business processing related interfaces and implementation, all businesses are implemented in Services< /span> resources/mapper => Mybaits mapping file, configures the mapping relationship between database entities and classes< /span> resources/mybatis => Front-end resources resources/static => Front-end resources Utilities (utils) => MD5+salt encryption Entity layer (model) => Entity class










1692065823387.png

Define status code
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 + ".";
    }
}

About database operations

Generated using Mybatis-Generator
  1. Entity class
  2. Mapping file xxxMapper.xml
  3. Dao class, xxxMapper.java

pom.xml configuration:

<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 configuration:

<?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&amp;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>

1692061498215.png
1692061513811.png
1692061527174.png
1692061540658.png
1692061556219.png

Write it yourself

1692061269275.png
1692061302888.png
1692061316357.png
1692061387559.png
1692061414067.png

Salted encryption

Commons-codec is used in the project, which is a tool package provided by Apache for digest operations, encoding and decoding. Common encoding and decoding tools include Base64, MD5, Hex, SHA1, DES, etc.

<!-- 编码解码加密⼯具包-->
<dependency>
 <groupId>commons-codec</groupId>
 <artifactId>commons-codec</artifactId>
</dependency>

Salt generation:

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 encryption:

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;
    }
}

Take login as an example

UserMapper definition method in dao layer
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return
*/
User selectByName (String username);
Write the sql statement in the corresponding mapping file UserExtMapper.xml:
<!-- 根据用户名查询用户信息-->
<select id="selectByName" parameterType="java.lang.String" resultMap="BaseResultMap">
  select
  <include refid="Base_Column_List" />
  from t_user
  where username = #{username,jdbcType=VARCHAR}
</select>
The IUserService definition interface in the service layer:
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return
*/
User selectByName (String username);

/**
* 创建普通用户
* @param user 用户信息
*/
void createNormalUser(User user);

/**
* 用户登录
* @param username 用户名
* @param password 密码
* @return
*/
User login(String username, String password);
Implement the interface in 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
@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));
}
Write in the UserController of the controller layer (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("登录成功");
    }
Conduct interface testing (Swagger interface details are introduced below)

http://127.0.0.1:58080/swagger-ui/index.html

1692089733789.png

Write the front-end interface:
// 发送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'
    });
  }
});

Interceptor

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/**");
    }
}

interface

http://127.0.0.1:58080/swagger-ui/index.html#/
Solve the problem of incompatibility between SpringBoot 2.6.0 and above and Springfox3.0.0, involving SpringBoot Some internal implementation changes during the version upgrade process, detailed instructions are in the configuration file modification section
Use Swagger to generate the interface:

@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", "[email protected]"))
                .version("1.0")
                .build();
        return apiInfo;
    }
    /**
     * 解决SpringBoot 6.0以上与Swagger 3.0.0 不兼容的问题
     * 复制即可
     **/
    @Bean
    public WebMvcEndpointHandlerMapping
    webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,

                                     ServletEndpointsSupplier servletEndpointsSupplier,

                                     ControllerEndpointsSupplier controllerEndpointsSupplier,

                                     EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,

                                     WebEndpointProperties webEndpointProperties, Environment environment) {
    
    
        List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
        Collection<ExposableWebEndpoint> webEndpoints =
                webEndpointsSupplier.getEndpoints();
        allEndpoints.addAll(webEndpoints);
        allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
        allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
        String basePath = webEndpointProperties.getBasePath();
        EndpointMapping endpointMapping = new EndpointMapping(basePath);
        boolean shouldRegisterLinksMapping =
                this.shouldRegisterLinksMapping(webEndpointProperties, environment,
                        basePath);
        return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints,
                endpointMediaTypes,
                corsProperties.toCorsConfiguration(), new
                EndpointLinksResolver(allEndpoints, basePath),
                shouldRegisterLinksMapping, null);
    }
    private boolean shouldRegisterLinksMapping(WebEndpointProperties
                                                       webEndpointProperties, Environment environment,
                                               String basePath) {
    
    
        return webEndpointProperties.getDiscovery().isEnabled() &&
                (StringUtils.hasText(basePath)
                        ||
                        ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
    }
}
spring
  mvc:
    pathmatch:
      matching-strategy: ANT_PATH_MATCHER #Springfox-Swagger兼容性配置

Common API annotations:
• @Api: used on Controller, description of the controller class
◦ tags="Describe the The role of the class, the annotation that can be seen on the front-end interface"
• @ApiModel: Acts on the response class, the description of the returned response data
• @ApiModelProerty: Acts on the attributes of the class, description of the attributes
• @ApiOperation: Acts on the specific method, description of the API interface< a i=6> • @ApiParam: Acts on each parameter in the method, describing the properties of the parameter

Format specifications:

// 请求
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
}
user interface

1692063100679.png

Section interface

1692063125302.png

Post interface

1692087803923.png

Site private message interface

1692063193236.png

Post reply interface

1692063435918.png

<!-- 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", "[email protected]"))
                .version("1.0")
                .build();
            return apiInfo;
        }
        /**
* 解决SpringBoot 6.0以上与Swagger 3.0.0 不兼容的问题
* 复制即可
**/
        @Bean
        public WebMvcEndpointHandlerMapping
        webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,

                                         ServletEndpointsSupplier servletEndpointsSupplier,

                                         ControllerEndpointsSupplier controllerEndpointsSupplier,

                                         EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,

                                         WebEndpointProperties webEndpointProperties, Environment environment) {
    
    
            List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
        Collection<ExposableWebEndpoint> webEndpoints =
        webEndpointsSupplier.getEndpoints();
        allEndpoints.addAll(webEndpoints);
        allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
        allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
        String basePath = webEndpointProperties.getBasePath();
        EndpointMapping endpointMapping = new EndpointMapping(basePath);
        boolean shouldRegisterLinksMapping =
        this.shouldRegisterLinksMapping(webEndpointProperties, environment,
        basePath);
        return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints,
        endpointMediaTypes,
        corsProperties.toCorsConfiguration(), new
        EndpointLinksResolver(allEndpoints, basePath),
        shouldRegisterLinksMapping, null);
        }
        private boolean shouldRegisterLinksMapping(WebEndpointProperties
        webEndpointProperties, Environment environment,
        String basePath) {
    
    
        return webEndpointProperties.getDiscovery().isEnabled() &&
        (StringUtils.hasText(basePath)
        ||
        ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
        }
        }

Achievements display

Login page
1692064031733.png
Registration:
1692064116654.png
Home page:
1692087845780.png
Post details:
![(3Z~QN$TM}KSSB_]5H0Z{]S.png](https://img-blog.csdnimg.cn/img_convert/20569172ce81ecb0db2f01d90c39b28e.png#averageHue=#e4e6cd&clientId=u44a7ef89-db93-4& from = paste =ud684c8d4-a674-4c78-a993-00dea371b8c&title=&width=1529.6)

个人主页:
1692065078567.png
个人中心:
![@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)
私信:
1692065187854.png
1692065230007.png
1692065283460.png

test

test case

1692088260413.png

test environment

Operating system: Windows 10 Home Edition
Project running: CentOS, maven, JDK1.8
Browser: Chorme, Edge, FireFox Tester: Me Testing technology: Mainly using automated testing and manual testinghttp://175.178.163.221:58080/ Network:
Automated script environment: IDEA2022.2.3


function test

Testing of landing pages

Scenario 1: You can log in normally by entering your account and password, click Login
Expected result: The page jumps to the homepage
Actual result: The page jumps Go to home page, consistent with expected results

Scenario 2: Enter the correct user name, wrong password, click to log in
Expected result: Prompt for wrong password
Actual result: Prompt for wrong password, Consistent with expected results

Testing the registration page

Scenario 1: Enter a username, nickname, password that does not exist in the database, and the same password as the confirmation password, click Register
Expected result: The page jumps to the login page
Actual result: The page jumps to the login page, consistent with the expected result

Scenario 2: Enter a username, nickname, password that does not exist in the database, and the password is different from the confirmation password, click Register
Expected result: 'Please check the confirmation password' is displayed
Actual result: 'Please check to confirm password' is displayed, which is consistent with the expected result

Scenario 3: Enter the username, nickname, password and confirmation password that exist in the database, and click Register
Expected result: Prompt that the user already exists
Actual result: Prompt that the user already exists, consistent with the expected result

Testing the home page

Scenario 1: Enter the home page address when not logged in
Expected result: jump to the login page
Actual result: jump to the login page, and Expected results

Scenario 2: After logging in, jump to the homepage, enter the keyword 'database' in the search bar, and click search
Expected results: Display a list of posts related to the database
Actual result: Displays a list of posts related to the database, consistent with the expected result

Scenario 3: After logging in, jump to the homepage, enter the keyword '1' (a title that does not exist in the database) in the search bar, and click search
Expected results: Show' There are no posts yet'
Actual results: displaying 'There are no posts yet', consistent with the expected results

Scenario 4: After logging in, after searching, click on any section
Expected result: Display the post list of the specific section
Actual result: Display and The list of posts related to the search keyword does not match the expected results

Scenario 5: After logging in, click on the section 'Java',
Expected results: Display the post list of the 'Java' section, display the section name, and the total number of posts
Actual results: Display the list of posts from the 'Java' section, consistent with the expected results

Scenario 6: After logging in, click on the section 'Java',
Expected result: Display the post list of the 'Java' section
Actual result: Display List of posts in the 'Java' section, consistent with expected results

Scenario 6: After logging in, click on the section 'Ranking',
Expected results: Display a list of posts sorted in descending order by (50% number of likes + 50% number of views), Display the total number of posts
Actual result: Display a list of posts sorted in descending order (50% number of likes + 50% number of views). The total number of posts is 0, which is not consistent with the expected result

Scenario 7: After logging in, click on a specific post
Expected results: Jump to the post details page, displaying the specific information of the post, the author of the post, creation time, and number of views
Actual results: Jump to the post details page, display the specific information of the post, author of the post, creation time, number of views, consistent with the expected results

Scenario 8: After logging in, click 'Publish Post',
Expected result: Jump to the post page
Actual result: Jump to Publish post page, consistent with expected results

Scenario 9: After logging in, click the 'bell' in the upper right corner,
Expected result: Jump out of all private messages on the site
Actual result: Jump out of all private messages on the site, Consistent with expected results

Scenario 9: After logging in, click on the user in the upper right corner and click on My Posts
Expected results: Jump to the My Posts page, displaying all posts published by the author, personal introduction, and Number of posts, email address, registration date
Actual result: jump to my post page, display all posts posted by the author, personal introduction is not displayed, number of posts, email address, registration date are incorrect, Not consistent with expected results

Scenario 9: After logging in, click on the user in the upper right corner and click on Personal Center
Expected results: Jump to the Personal Center and display personal information
Actual results : Jump to personal center, display personal information, consistent with expected results

Other tests are not shown one by one.

Compatibility testing

serial number Specific content Test Data step expected outcome actual results
1 Internet Explorer Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Consistent with expected results
2 QQ browser Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Consistent with expected results
3 Chrome browser Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Consistent with expected results
4 Firefox browser Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Consistent with expected results
5 Testing different versions of each browser Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Not tested
6 Mac system Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Not tested
7 Linux system Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Not tested
8 Different computers such as ASUS, Lenovo, Apple, Huawei, etc. Account: test01, password: 123456 Open the browser, enterhttp://175.178.163.221:58080/sign-in.html, enter the account password, and click to log in Can be used normally Not tested

Performance Testing

Log in:

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;
}

1692153075849.png
1692153092150.png

1692154270167.png

1692154468379.png

1692154508190.png

Guess you like

Origin blog.csdn.net/qq_53869058/article/details/132345370