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

news/2024/7/25 20:06:22/文章来源:https://blog.csdn.net/weixin_44985964/article/details/138819911

目录

前言

一、项目背景

二、相关技术及工具

三、数据库设计

四、软件开发

4.1、搭建环境

4.1.1、创建工程

4.1.2、配置application.yml文件

4.1.3、环境测试

创建测试接口

4.1.4、继续配置

4.2、公共组件

4.2.1、创建工程结构

4.2.2、配置数据源

添加相关依赖

配置application.yml

测试

4.2.3、编写类与映射文件

根据数据库编写实体类

编写映射文件

编写Dao类

4.2.4、生成类与映射文件

引入依赖

创建generatorConfig.xml

运行插件生成文件

添加获取主键值的选项

扫描配置

测试

4.2.5、编写公共代码

定义状态码

定义返回结果

自定义异常

全局异常处理

登录拦截器

创建LoginInterceptor

application.yml配置文件

创建AppInterceptorConfigurer

实现API自动生成

引入依赖

编写配置类

application.xml添加配置

API常用注解

访问API列表

创建工具类 

创建MD5加密工具类

创建生成UUID工具类

创建字符串工具类

4.3、实现业务功能

4.3.1、注册(后端实现)

请求

响应

创建扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

注册(前端实现)

4.3.2、登录(后端实现)

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现controller层

测试接口

登录(前端实现)

4.3.3、退出

请求

响应

实现Controller层

测试接口

退出(前端页面) 

4.3.4、个人中心

4.3.4.1、获取用户信息

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现controller层

测试接口

修复返回值 

前端代码

4.3.4.2、修改个人信息

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码 

4.3.4.3、修改密码

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.5、版块信息

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

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

application.yml添加配置

实现Controller层

测试接口

前端代码

4.3.5.2、获取指定版块信息 

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.6、帖子列表

4.3.6.1、版块帖子列表

请求

响应

修改Article实体类

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.6.2、用户帖子列表

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7、帖子操作

4.3.7.1、集成编辑区

编写HTML

编写JS

4.3.7.2、发布帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.3、获取帖子详情

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.4、编辑帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.5、删除帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.7.6、点赞帖子

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.8、帖子回复

4.3.8.1、提交回复内容

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.8.2、帖子回复列表

请求

响应

添加关联对象

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9、站内信

4.3.9.1、发送

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.2、未读数

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.3、列表

请求

响应

扩展Mapper.xml

修改DAO

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.4、更新状态

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

4.3.9.5、回复

请求

响应

创建Service接口

实现Service接口

对Service接口进行单元测试

实现Controller层

测试接口

前端代码

五、发布部署

1、执行SQL脚本

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

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

4、打包程序

5、上传到服务器

6、验证访问


前言

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

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

一、项目背景

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

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

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

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

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

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

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

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

二、相关技术及工具

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

三、数据库设计

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

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

-- ----------------------------
-- 创建数据库,并指定字符集
-- ----------------------------
drop database if exists java_forum;
create database java_forum character set utf8mb4 collate utf8mb4_general_ci;
-- 选择数据库
use java_forum;SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- 创建帖子表 t_article
-- ----------------------------
DROP TABLE IF EXISTS `t_article`;
CREATE TABLE `t_article`  (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '帖子编号,主键,自增',`boardId` bigint(20) NOT NULL COMMENT '关联板块编号,非空',`userId` bigint(20) NOT NULL COMMENT '发帖人,非空,关联用户编号',`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题,非空,最大长度100个字符',`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '帖子正文,非空',`visitCount` int(11) NOT NULL DEFAULT 0 COMMENT '访问量,默认0',`replyCount` int(11) NOT NULL DEFAULT 0 COMMENT '回复数据,默认0',`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0正常 1 禁用,默认0',`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0 否 1 是,默认0',`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',`updateTime` datetime NOT NULL COMMENT '修改时间,精确到秒,非空',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子表' ROW_FORMAT = Dynamic;-- ----------------------------
-- 创建帖子回复表 t_article_reply
-- ----------------------------
DROP TABLE IF EXISTS `t_article_reply`;
CREATE TABLE `t_article_reply`  (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号,主键,自增',`articleId` bigint(20) NOT NULL COMMENT '关联帖子编号,非空',`postUserId` bigint(20) NOT NULL COMMENT '楼主用户,关联用户编号,非空',`replyId` bigint(20) NULL DEFAULT NULL COMMENT '关联回复编号,支持楼中楼',`replyUserId` bigint(20) NULL DEFAULT NULL COMMENT '楼主下的回复用户编号,支持楼中楼',`content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '回贴内容,长度500个字符,非空',`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1禁用,默认0',`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子回复表' ROW_FORMAT = Dynamic;-- ----------------------------
-- 创建版块表 t_board
-- ----------------------------
DROP TABLE IF EXISTS `t_board`;
CREATE TABLE `t_board`  (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '版块编号,主键,自增',`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '版块名,非空',`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '帖子数量,默认0',`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序优先级,升序,默认0,',`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态,0 正常,1禁用,默认0',`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '版块表' ROW_FORMAT = Dynamic;-- ----------------------------
-- 创建站内信表 for t_message
-- ----------------------------
DROP TABLE IF EXISTS `t_message`;
CREATE TABLE `t_message`  (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '站内信编号,主键,自增',`postUserId` bigint(20) NOT NULL COMMENT '发送者,并联用户编号',`receiveUserId` bigint(20) NOT NULL COMMENT '接收者,并联用户编号',`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容,非空,长度255个字符',`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0未读 1已读,默认0',`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '站内信表' ROW_FORMAT = Dynamic;-- ----------------------------
-- 创建用户表 for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户编号,主键,自增',`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名,非空,唯一',`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '加密后的密码',`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称,非空',`phoneNum` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址',`gender` tinyint(4) NOT NULL DEFAULT 2 COMMENT '0女 1男 2保密,非空,默认2',`salt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '为密码加盐,非空',`avatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户头像URL,默认系统图片',`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '发帖数量,非空,默认0',`isAdmin` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否管理员,0否 1是,默认0',`remark` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注,自我介绍',`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1 禁言,默认0',`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒',`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒',PRIMARY KEY (`id`) USING BTREE,UNIQUE INDEX `user_username_index`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT =
Dynamic;SET FOREIGN_KEY_CHECKS = 1;-- 写入版块信息数据
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (1, 'Java', 0, 1, 0, 0, '2023-01-14 19:02:18', '2023-01-14 19:02:18');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (2, 'C++', 0, 2, 0, 0, '2023-01-14 19:02:41', '2023-01-14 19:02:41');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (3, '前端技术', 0, 3, 0, 0, '2023-01-14 19:02:52', '2023-01-14 19:02:52');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (4, 'MySQL', 0, 4, 0, 0, '2023-01-14 19:03:02', '2023-01-14 19:03:02');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (5, '面试宝典', 0, 5, 0, 0, '2023-01-14 19:03:24', '2023-01-14 19:03:24');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (6, '经验分享', 0, 6, 0, 0, '2023-01-14 19:03:48', '2023-01-14 19:03:48');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (7, '招聘信息', 0, 7, 0, 0, '2023-01-25 21:25:33', '2023-01-25 21:25:33');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (8, '福利待遇', 0, 8, 0, 0, '2023-01-25 21:25:58', '2023-01-25 21:25:58');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (9, '灌水区', 0, 9, 0, 0, '2023-01-25 21:26:12', '2023-01-25 21:26:12');insert into t_article values (null, 1, 1, '测试测试111', '测试测试内容内容111', 0,0,0,0,0, '2023-07-19 14:46:00', now());
insert into t_article values (null, 1, 1, '测试测试222', '测试测试内容内容222', 0,0,0,0,0, '2023-07-19 14:46:00', now());
insert into t_article values (null, 2, 1, '测试测试333', '测试测试内容内容333', 0,0,0,0,0, '2023-07-19 14:46:00', now());

四、软件开发

4.1、搭建环境

4.1.1、创建工程

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

添加依赖

修改一下几处:

4.1.2、配置application.yml文件

#spring全局配置
spring:application:name: forum_system  #配置项目名称output:ansi:enabled: always  #控制台输出彩色日志#服务器配置
server:port: 58080  #修改Tomcat的默认端口号#日志配置
logging:pattern:dateformat: yyyy-MM-dd HH:mm:sslevel:root: info  #默认日志级别com.example.forum_system: debug  #指定包的日志级别file:path: E:\idea-project\log\project\forum  #日志保存目录

4.1.3、环境测试

创建测试接口

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

@RestController
@RequestMapping("/test")
public class TestController {@GetMapping("/hello")public String hello(){return "hello,baekhyun...";}
}

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

4.1.4、继续配置

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

4.2、公共组件

4.2.1、创建工程结构

4.2.2、配置数据源

添加相关依赖
        <!-- 管理依赖版块号--><!--mysql-connector 数据库连接驱动包 --><mysql-connector.version>5.1.49</mysql-connector.version><!-- mybatis --><mybatis-starter.version>2.3.0</mybatis-starter.version><!-- 数据源 --><druid-starter.version>1.2.16</druid-starter.version>
        <!-- 数据库驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql-connector.version}</version></dependency><!-- mybatis 依赖其中已经包含了spring-jdbc不再重复引用,此项目中使用spring-jdbc提供的HikariCP做为数据源, 相关配置在yml文件中--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis-starter.version}</version></dependency><!-- 阿里巴巴druid数据源,如果使用SpringBoot默认的数据源,删除或注释这个依赖即可 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid-starter.version}</version></dependency>
配置application.yml
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/java_forum?characterEncoding=utf8&useSSL=false # 数据库连接串username: root # 数据库用户名password: '19930112' # 数据库密码driver-class-name: com.mysql.jdbc.Driver # 数据库驱动类
测试
    @Resourceprivate DataSource dataSource;@Testvoid 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 generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration><!-- 驱动包路径,location中路径替换成自己本地路径 --><classPathEntry location="C:\Users\19756\.m2\repository\mysql\mysql-connector-java\5.1.49\mysql-connector-java-5.1.49.jar"/><context id="DB2Tables" targetRuntime="MyBatis3"><!-- 禁用自动生成的注释 --><commentGenerator><property name="suppressAllComments" value="true"/><property name="suppressDate" value="true"/></commentGenerator><!-- 连接配置 --><jdbcConnection driverClass="com.mysql.jdbc.Driver"connectionURL="jdbc:mysql://127.0.0.1:3306/java_forum?characterEncoding=utf8&amp;useSSL=false"userId="root"password="19930112"></jdbcConnection><javaTypeResolver><!-- 小数统一转为BigDecimal --><property name="forceBigDecimals" value="false"/></javaTypeResolver><!-- 实体类生成位置 --><javaModelGenerator targetPackage="com.example.forum_system.model" targetProject="src/main/java"><property name="enableSubPackages" value="true"/><property name="trimStrings" value="true"/></javaModelGenerator><!-- mapper.xml生成位置 --><sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"><property name="enableSubPackages" value="true"/></sqlMapGenerator><!-- DAO类生成位置 --><javaClientGenerator type="XMLMAPPER" targetPackage="com.example.forum_system.dao"targetProject="src/main/java"><property name="enableSubPackages" value="true"/></javaClientGenerator><!-- 配置生成表与实例, 只需要修改表名tableName, 与对应类名domainObjectName 即可--><table tableName="t_article" domainObjectName="Article" enableSelectByExample="false"enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"enableUpdateByExample="false"><!-- 类的属性用数据库中的真实字段名做为属性名, 不指定这个属性会自动转换 _ 为驼峰命名规则--><property name="useActualColumnNames" value="true"/></table><table tableName="t_article_reply" domainObjectName="ArticleReply" enableSelectByExample="false"enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table><table tableName="t_board" domainObjectName="Board" enableSelectByExample="false" enableDeleteByExample="false"enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table><table tableName="t_message" domainObjectName="Message" enableSelectByExample="false"enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table><table tableName="t_user" domainObjectName="User" enableSelectByExample="false" enableDeleteByExample="false"enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table></context>
</generatorConfiguration>
运行插件生成文件

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

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

添加获取主键值的选项

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

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

application.xml更新配置

#mybatis相关配置
mybatis:mapper-locations: classpath:mapper/**/*.xml  # 指定 xxxMapper.xml的扫描路径
测试
    @Resourceprivate UserMapper userMapper;@Testvoid 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;}@Overridepublic String toString() {return "code="+code+",message="+message+".";}
}
定义返回结果

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

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

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

public class AppResult<T> {//自定义状态码private long code;//描述信息private String message;//结果数据private T data;public long getCode() {return code;}/*** 构造方法*/public AppResult(long code, String message) {this.code = code;this.message = message;}public AppResult(long code, String message, T data) {this.code = code;this.message = message;this.data = data;}/*** 成功方法*/public static <T> AppResult<T> success(String message,T data){return new AppResult<>(ResultCode.SUCCESS.code,message,data);}public static <T> AppResult<T> success(String message){return new AppResult<>(ResultCode.SUCCESS.code,message,null);}public static <T> AppResult<T> success(T data){return new AppResult<>(ResultCode.SUCCESS.code,ResultCode.SUCCESS.getMessage(),data);}public static <T> AppResult<T> success(){return new AppResult<>(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMessage(),null);}/*** 失败方法* @return*/public static AppResult failed(){return new AppResult(ResultCode.FAILED.getCode(),ResultCode.FAILED.getMessage());}public static AppResult failed(String message){return new AppResult(ResultCode.FAILED.getCode(),message);}public static AppResult failed(ResultCode resultCode){return new AppResult(resultCode.getCode(), resultCode.getMessage());}public void setCode(long code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public T getData() {return data;}public void setData(T data) {this.data = data;}
}
自定义异常

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

/*** 自定义异常*/
public class ApplicationException extends RuntimeException{//自定义的异常描述private AppResult errorResult;//指定状态码,异常描述public ApplicationException(AppResult appResult) {//构造异常中的Message属性super(appResult.getMessage());//自定义的错误描述this.errorResult=appResult;}//自定义异常描述public ApplicationException(String message) {super(message);//根据异常描述构建返回对象this.errorResult=new AppResult(ResultCode.FAILED.getCode(),message);}//指定异常public ApplicationException(Throwable cause){super(cause);}//自定义异常描述,异常信息public ApplicationException(String message,Throwable cause){super(message,cause);}public AppResult getErrorResult() {return errorResult;}public void setErrorResult(AppResult errorResult) {this.errorResult = errorResult;}
}
全局异常处理

实现统一异常处理

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

测试异常处理

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

访问以下链接:

http://127.0.0.1:58080/test/testException

http://127.0.0.1:58080/test/applicationException

登录拦截器
创建LoginInterceptor
@Component
public class LoginInterceptor implements HandlerInterceptor {//从配置文件中获取默认登录页的URL@Value("${forum_system.login.url}")private String defaultURL;/*** 请求的前置处理* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic 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 {@Resourceprivate LoginInterceptor loginInterceptor;@Overridepublic 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*/@Beanpublic 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 不兼容的问题* 复制即可**/@Beanpublic WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,ServletEndpointsSupplier servletEndpointsSupplier,ControllerEndpointsSupplier controllerEndpointsSupplier,EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,WebEndpointProperties webEndpointProperties, Environment environment) {List<ExposableEndpoint<?>> allEndpoints = new ArrayList();Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();allEndpoints.addAll(webEndpoints);allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());String basePath = webEndpointProperties.getBasePath();EndpointMapping endpointMapping = new EndpointMapping(basePath);boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment,basePath);return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),shouldRegisterLinksMapping, null);}private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,String basePath) {return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));}}
application.xml添加配置
spring:mvc:pathmatch:matching-strategy: ant_path_matcher  #Springfox-Swagger兼容性配置
API常用注解

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

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

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

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

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

修改接口测试

@Api(tags = "测试接口")
@RestController
@RequestMapping("/test")
public class TestController {@ApiOperation("测试打印")@GetMapping("/hello")public String hello(){return "hello,baekhyun...";}@ApiOperation("测试异常")@GetMapping("/testException")public AppResult testException() throws Exception {throw new Exception("这是一个Exception...");}@ApiOperation("测试自定义异常")@GetMapping("/applicationException")public AppResult testApplicationException() throws Exception {throw new ApplicationException("这是一个自定义的ApplicationException...");}@ApiOperation("测试传参")@GetMapping("/helloByName")public String testHelloByName(@ApiParam("名字") String name){return "hello,"+name;}
}
访问API列表

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

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

将API导入到postman中即可

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

用户传入一个密码明文

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

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

导入依赖

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

<dependency>

<groupId>commons-codec</groupId>

<artifactId>commons-codec</artifactId>

</dependency>

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

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

/***  ⽣成UUID⼯具类*/
public class UUIDUtils {/*** ⽣成32位UUID* @return*/public static String UUID_32(){return UUID.randomUUID().toString().replace("-","");}/*** 生成36位UUID* @return*/public static String UUID_36(){return UUID.randomUUID().toString();}
}
创建字符串工具类

校验字符串是否为空

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

4.3、实现业务功能

4.3.1、注册(后端实现)

请求

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

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

响应

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

创建扩展Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.forum_system.dao.UserMapper"><!--  根据用户名查询用户信息  --><select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">select<include refid="Base_Column_List" />from t_userwhere deleteState=0and 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 {@Resourceprivate UserMapper userMapper;@Overridepublic User selectByUsername(String username) {//非空校验if (StringUtils.isEmpty(username)){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//调用DAOUser user=userMapper.selectByUsername(username);//返回结果return user;}@Overridepublic 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 {@Resourceprivate IUserService userService;@Resourceprivate ObjectMapper objectMapper;@Testvoid selectByUsername() throws JsonProcessingException {User user=userService.selectByUsername("baekhyun");System.out.println(objectMapper.writeValueAsString(user));}@Testvoid 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 {@Resourceprivate 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);  //盐//调用serviceuserService.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" /><pathd="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" /><pathd="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" /></svg></a></span><div class="invalid-feedback">请检查确认密码</div></div></div><div class="mb-3"><label class="form-check"><input type="checkbox" class="form-check-input" id="policy" /><span class="form-check-label">同意 <a href="#" tabindex="-1">BAEKHYUN论坛使用条款和隐私政策</a>.</span></label></div><div class="form-footer"><button type="button" class="btn btn-primary w-100" id="submit">注册</button></div></div></form><div class="text-center text-muted mt-3">我已有一个账户? <a href="./sign-in.html" tabindex="-1">登录</a></div></div></div>
</body>
<!-- 导入JS -->
<script src="./dist/js/tabler.min.js"></script>
<script src="./dist/js/jquery-3.6.3.min.js"></script>
<script src="./dist/js/jquery.toast.js"></script>
<script>$(function () {// 获取表单并校验$('#submit').click(function () {let checkForm = true;// 校验用户名if (!$('#username').val()) {$('#username').addClass('is-invalid');checkForm = false;}// 校验昵称if (!$('#nickname').val()) {$('#nickname').addClass('is-invalid');checkForm = false;}// 校验密码非空if (!$('#password').val()) {$('#password').addClass('is-invalid');checkForm = false;}// 校验确认密码非空, 校验密码与重复密码是否相同if (!$('#passwordRepeat').val() || $('#password').val() != $('#passwordRepeat').val()) {$('#passwordRepeat').addClass('is-invalid');checkForm = false;}// 检验政策是否勾选if (!$('#policy').prop('checked')) {$('#policy').addClass('is-invalid');checkForm = false;}// 根据判断结果提交表单if (!checkForm) {return false;}// 所有校验通过之后构造要发送的数据let postData={username:$('#username').val(),nickname:$('#nickname').val(),password:$('#password').val(),passwordRepeat:$('#passwordRepeat').val()};// 发送AJAX请求 // contentType = application/x-www-form-urlencoded// 成功后跳转到 sign-in.html$.ajax ({type:'POST',url:'user/register',contentType:'application/x-www-form-urlencoded',data:postData,//回调success:function(respData){//根据code的值判断响应是否成功if(respData.code==0){//成功location.assign('sign-in.html');}else{//失败$.toast({heading:'失败',text:respData.message,icon:'warning'})}},error:function(){$.toast({heading:'错误',text:'访问网站出现问题,请与管理员联系',icon:'error'})}});});// 表单元单独检验$('#username, #nickname, #password').on('blur', function () {if ($(this).val()) {$(this).removeClass('is-invalid');$(this).addClass('is-valid');} else {$(this).removeClass('is-valid');$(this).addClass('is-invalid');}})// 检验确认密码$('#passwordRepeat').on('blur', function () {if ($(this).val() && $(this).val() == $('#password').val()) {$(this).removeClass('is-invalid');$(this).addClass('is-valid');} else {$(this).removeClass('is-valid');$(this).addClass('is-invalid');}})// 校验政策是否勾选$('#policy').on('change', function () {if ($(this).prop('checked')) {$(this).removeClass('is-invalid');$(this).addClass('is-valid');} else {$(this).removeClass('is-valid');$(this).addClass('is-invalid');}})// 密码框右侧明文密文切换按钮$('#passwordRepeat_a').click(function () {if($('#passwordRepeat').attr('type') == 'password') {$('#passwordRepeat').attr('type', 'text');} else {$('#passwordRepeat').attr('type', 'password');}});$('#password_a').click(function () {if($('#password').attr('type') == 'password') {$('#password').attr('type', 'text');} else {$('#password').attr('type', 'password');}});
});</script></html>

4.3.2、登录(后端实现)

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

在IUserService新增方法

    /*** 用户登录* @param username* @param password* @return*/User login(String username,String password);
实现Service接口
    @Overridepublic 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接口进行单元测试
    @Testvoid 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){//调用ServiceUser user=userService.login(username,password);if (user==null){//返回错误return AppResult.failed(ResultCode.FAILED_LOGIN);}//获取session对象HttpSession session= request.getSession(true);  //没有的话创建一个//把用户信息设置到session中session.setAttribute(AppConfig.USER_SESSION_KEY,user);//返回结果return AppResult.success();}
测试接口

登录(前端实现)
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/><meta http-equiv="X-UA-Compatible" content="ie=edge"/><link rel="shortcut icon" href="/favicon.ico"><!-- 标题 --><title>BAEKHYUN论坛 - 用户登录</title><!-- 导入CSS --><link href="./dist/css/tabler.min.css?1674944402" rel="stylesheet"/><link rel="stylesheet" href="./dist/css/jquery.toast.css"><!-- 设置字体 --><!-- <style>@import url('https://rsms.me/inter/inter.css');:root {--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;}body {font-feature-settings: "cv03", "cv04", "cv11";}</style> --></head><body class="d-flex flex-column"><!-- 正文 --><div class="page page-center"><div class="container container-normal py-4"><div class="row align-items-center g-4"><div class="col-lg"><div class="container-tight"><div class="text-center mb-4"><img src="./image/bit-forum-logo01.png" height="50" alt=""></div><div class="card card-md"><div class="card-body"><h2 class="text-center mb-4">用户登录</h2><form id="signInForm" method="get" autocomplete="off" novalidate><div class="mb-3"><label class="form-label required">用户名</label><input type="text" class="form-control" placeholder="请输入用户名" autocomplete="off" name="username" id="username"><div class="invalid-feedback">用户名不能为空</div></div><div class="mb-2"><label class="form-label required">密码<!-- <span class="form-label-description"><a href="#">忘记密码</a></span> --></label><div class="input-group input-group-flat"><input type="password" class="form-control"  placeholder="请输入密码"  autocomplete="off" name="password"id="password"><span class="input-group-text"><a href="javascript:void(0);" id="password_a" class="link-secondary" title="显示密码" data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye --><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" /></svg></a></span><div class="invalid-feedback">密码不能为空</div></div></div><!-- <div class="mb-2"><label class="form-check"><input type="checkbox" class="form-check-input"/><span class="form-check-label">记住我</span></label></div> --><div class="form-footer"><button id="submit" type="button" class="btn btn-primary w-100">登录</button></div></form></div></div><div class="text-center text-muted mt-3">还没有注册吗? <a href="./sign-up.html" tabindex="-1">点击注册</a></div></div></div><div class="col-lg d-none d-lg-block"><img src="./dist/illustrations/undraw_joyride_hnno.svg" height="300" class="d-block mx-auto" alt=""></div></div></div></div></body><!-- 导入JS --><script src="./dist/js/jquery-3.6.3.min.js"></script><script src="./dist/js/tabler.min.js"></script><script src="./dist/js/jquery.toast.js"></script><script>$(function () {// 获取控件// 用户名let usernameEl = $('#username');let passwordEl = $('#password');// 登录校验$('#submit').click(function () {let checkForm = true;// 校验用户名if (!usernameEl.val()) {usernameEl.addClass('is-invalid');checkForm = false;}// 校验密码if (!passwordEl.val()) {passwordEl.addClass('is-invalid');checkForm = false;}// 根据判断结果提交表单if (!checkForm) {return false;}// 构造数据let postData={username:usernameEl.val(),password:passwordEl.val(),};// 发送AJAX请求,成功后跳转到index.html$.ajax({type:'POST',url:'user/login',contentType:'application/x-www-form-urlencoded',data:postData,//回调success:function(respData){//根据code的值判断响应是否成功if(respData.code==0){//成功location.assign('index.html');}else{//失败$.toast({heading:'失败',text:respData.message,icon:'warning'})}},error:function(){$.toast({heading:'错误',text:'访问网站出现问题,请与管理员联系',icon:'error'})}});});//// 表单元单独检验$('#username, #password').on('blur', function () {if ($(this).val()) {$(this).removeClass('is-invalid');$(this).addClass('is-valid');} else {$(this).removeClass('is-valid');$(this).addClass('is-invalid');}});// 显示密码$('#password_a').click(function () {if(passwordEl.attr('type') == 'password') {passwordEl.attr('type', 'text');} else {passwordEl.attr('type', 'password');}});});</script>
</html>

4.3.3、退出

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

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

4.3.4、个人中心

4.3.4.1、获取用户信息

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

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

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

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


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

修复返回值 

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

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

    @ApiModelProperty("密码")@JsonIgnore//不参与JSON序列化private String password;//敏感信息不应在网络上传输@JsonIgnore//不参与JSON序列化private String salt;@ApiModelProperty("删除状态 0正常 1删除")@JsonIgnore//不参与JSON序列化private Byte deleteState;

在application.yml添加配置 

spring:#JSON序列化配置jackson:date-format: yyyy-MM-dd HH:mm:ss  #日期格式default-property-inclusion: non_null  #不为null时序列化
前端代码
    //========================= 获取用户信息 =======================// 成功后,手动设置用户信息// $('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');$.ajax({//请求方法type:'GET',//请求路径url:'user/info',//回调success:function(respData){//根据code的值判断响应是否成功if(respData.code==0){//获取用户对象let user=respData.data;//判断头像是否为空if(!user.avatarUrl){//为头像设置默认值user.avatarUrl=avatarUrl;}//成功后处理具体的逻辑//设置头像$('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');//设置昵称$('#index_nav_nickname').html(user.nickname);//设置用户分类let subName=user.isAdmin==1?'管理员':"普通用户";$('#index_nav_name_sub').html(subName);//记录当前登录的用户currentUserId=user.id;}else{//失败$.toast({heading:'失败',text:respData.message,icon:'warning'})}},error:function(){$.toast({heading:'错误',text:'访问网站出现问题,请与管理员联系',icon:'error'})}});
    // 构造查询用户信息的queryStringlet userInfoQueryString = '';if (profileUserId) {userInfoQueryString = '?id=' + profileUserId}// ============= 获取用户信息 =============// 成功时调用initProfileUserInfo()方法,初始化用户数据$.ajax({type:'GET',url:'user/info'+userInfoQueryString,// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {//成功后构建初始化页面上的用户信息initProfileUserInfo(respData.data);} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})  }},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'}) }});// ============= 设置Profile页面用户信息 ================function initProfileUserInfo(user) {console.log(user);// 默认头像路径if (!user.avatarUrl) {user.avatarUrl = avatarUrl;}console.log('currentUserId = '+currentUserId);// 站内信按钮if (user.id != currentUserId) {// 显示站内信按钮$('#div_profile_send_message').show();// 设置站内信目标用户信息$('#btn_profile_send_message').click(function() {setMessageReceiveUserInfo(user.id, user.nickname);});}// 设置用户ID$('#profile_user_id').val(user.id);// 设置头像$('#profile_avatar').css('background-image', 'url(' + user.avatarUrl + ')');// 用户昵称$('#profile_nickname').html(user.nickname);// 发贴数$('#profile_articleCount').html(user.articleCount);// 邮箱if (user.email) {$('#profile_email').html(user.email);}// 注册日期$('#profile_createTime').html(user.createTime);// 个人介绍if (user.remark) {$('#profile_remark').html(user.remark);}}
    // ================= 获取用户详情,初始化页面内容 =================// 发送AJAX请求,成功时 调用initUserInfo方法,完成页面数据初始化$.ajax({type:'get',url:'user/info',//回调// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {// 成功之后调用构建初始化页面上的用户信息的方法initUserInfo(respData.data);} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})}},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'});}});});// ================= 设置用户信息 =================function initUserInfo(user) {// 默认头像路径if (!user.avatarUrl) {user.avatarUrl = avatarUrl;}// 用户Id$('#settings_user_id').val(user.id);// title 昵称$('#settings_nickname').html(user.nickname);// 头像$('#settings_avatar').css('background-image', 'url(' + user.avatarUrl + ')');// 昵称$('#setting_input_nickname').val(user.nickname);// 邮箱$('#setting_input_email').val(user.email);// 电话$('#setting_input_phoneNum').val(user.phoneNum);// 个人简历$('#settings_textarea_remark').html(user.remark);}
4.3.4.2、修改个人信息

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

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

请求
// 请求
POST http: //127.0.0.1:58080/user/modifyInfo HTTP/1.1
Content-Type: application/x-www-form-urlencoded
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : {
"id" : 1 ,
"username" : "xxx" ,
"nickname" : "xxx" ,
"phoneNum" : "xxx" ,
"email" : "xxx" ,
"gender" : 1 ,
"avatarUrl" : null ,
"articleCount" : 1 ,
"isAdmin" : 1 ,
"remark" : " xxx " ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
}
}
创建Service接口
    /*** 修改用户信息* @param user*/void modifyInfo(User user);
实现Service接口
    @Overridepublic void modifyInfo(User user) {//非空校验if (user==null || user.getId()==null || user.getId()<=0){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//查询用户基本信息User existUser=userMapper.selectByPrimaryKey(user.getId());if (existUser==null){//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));}//定义一个标识boolean checkParams=false;//定义一个更新对象User updateUser=new User();//设置IdupdateUser.setId(user.getId());//更新时间updateUser.setUpdateTime(new Date());//处理usernameif (!StringUtils.isEmpty(user.getUsername())&& !user.getUsername().equals(existUser.getUsername())){//如果username不为空,那么则需要更新//1、查询当前数据库中是否存在相同的用户名User checkUser=selectByUsername(user.getUsername());if (checkUser!=null){//用户名已存在,抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));}//设置要更新用户名updateUser.setUsername(user.getUsername());//设置参数检查标识checkParams=true;}//处理昵称if (!StringUtils.isEmpty(user.getNickname())&& !user.getNickname().equals(existUser.getNickname())){//如果nickname不为空,那么则需要更新updateUser.setNickname(user.getNickname());//设置参数检查标识checkParams=true;}//处理电话号码if (!StringUtils.isEmpty(user.getPhoneNum())&& !user.getPhoneNum().equals(existUser.getPhoneNum())){//如果nickname不为空,那么则需要更新updateUser.setPhoneNum(user.getPhoneNum());//设置参数检查标识checkParams=true;}//处理邮箱if (!StringUtils.isEmpty(user.getEmail())&& !user.getEmail().equals(existUser.getEmail())){//如果nickname不为空,那么则需要更新updateUser.setEmail(user.getEmail());//设置参数检查标识checkParams=true;}//处理个人简介if (!StringUtils.isEmpty(user.getRemark())&& !user.getRemark().equals(existUser.getRemark())){//如果nickname不为空,那么则需要更新updateUser.setRemark(user.getRemark());//设置参数检查标识checkParams=true;}//处理性别if (user.getGender()!=null && user.getGender()!=existUser.getGender()){//设置要更新的值updateUser.setGender(user.getGender());//性别是否有效if (updateUser.getGender()<0 || updateUser.getGender()>2){//默认为2(保密)updateUser.setGender((byte) 2);}//设置参数检查标识checkParams=true;}//如果所有的参数都为空if (!checkParams){//打印日志log.warn("用户更新时,所有的参数都为空,user id="+user.getId());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//调用DAOint row=userMapper.updateByPrimaryKeySelective(updateUser);//判断受影响的行数if (row!=1){log.warn("用户更新时,"+ResultCode.ERROR_SERVICES.toString());throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
对Service接口进行单元测试
    @Testvoid modifyInfo() {//创建一个要修改的用户User user=new User();user.setId(3L);user.setUsername("krystal111");user.setNickname("krystal111");user.setPhoneNum("123456781");user.setEmail("111@qq.com");user.setGender((byte) 0);user.setRemark("我是krystal111");//调用ServiceuserService.modifyInfo(user);System.out.println("修改个人信息成功");}

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

前端代码 
  // ================= 封装ajax请求 =================function changeUserInfo(userInfo, type) {// 校验用户信息if (!userInfo) {$.toast({heading: '提示',text: '请检查要修改的内容是否正确或联系管理员',icon: 'info'});return;}// 构造query stringlet searchParams = new URLSearchParams(userInfo);let queryString = '?' + searchParams.toString();console.log(queryString);// 发送请求,提示响应结果$.ajax({type:'POST',url:'user/modifyInfo',contentType : 'application/x-www-form-urlencoded',data:userInfo,// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {let user = respData.data;// 修改页面昵称if (user.nickname) {// 当前页面$('#settings_nickname').html(user.nickname);// 导航栏$('#index_nav_nickname').html(user.nickname);}// 成功之后弹出提示框$.toast({heading: '成功',text: respData.message,icon: 'success'})} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})}},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'});}});}// ================= 修改用户昵称 =================$('#setting_submit_nickname').click(function(){// 获取值let nicknameEl = $('#setting_input_nickname');// 校验if(!nicknameEl.val()) {nicknameEl.focus();return false;}// 构造数据let nicknameObj = {id : $('#settings_user_id').val(),nickname : nicknameEl.val()}// 发送请求changeUserInfo(nicknameObj);});// ================= 修改邮箱 =================$('#setting_submit_email').click(function(){// 获取值let emailEl = $('#setting_input_email');// 校验if(!emailEl.val()) {emailEl.focus();return false;}// 构造数据let emailObj = {id : $('#settings_user_id').val(),email : emailEl.val()}// 发送请求changeUserInfo(emailObj);});// ================= 修改电话 =================$('#setting_submit_phoneNum').click(function(){// 获取值let phoneNumEl = $('#setting_input_phoneNum');// 校验if(!phoneNumEl.val()) {phoneNumEl.focus();return false;}// 构造数据let phoneNumObj = {id : $('#settings_user_id').val(),phoneNum : phoneNumEl.val()}// 发送请求changeUserInfo(phoneNumObj);});// ================= 修改个人介绍 =================$('#settings_submit_remark').click(function(){// 获取值let remarkEl = $('#settings_textarea_remark');// 校验if(!remarkEl.val()) {remarkEl.focus();return false;}// 构造数据let remarkObj = {id : $('#settings_user_id').val(),remark : remarkEl.val()}// 发送请求changeUserInfo(remarkObj);});
4.3.4.3、修改密码

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

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

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

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

前端代码
<!-- Page header -->
<div class="page-header d-print-none"><div class="container-xl"><div class="row g-2 align-items-center"><div class="col"><h2 class="page-title">用户中心</h2></div></div></div>
</div>
<!-- Page body -->
<div class="page-body"><div class="container-xl"><div class="card"><div class="row g-0"><div class="col-3 d-none d-md-block border-end"><div class="card-body"><div class="list-group list-group-transparent"><a href="javascript:void(0);"class="list-group-item list-group-item-action d-flex align-items-center active">我的账户</a><!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">修改密码</a><a href="#" class="list-group-item list-group-item-action d-flex align-items-center">个人简介</a> --></div></div></div><div class="col d-flex flex-column"><div class="card-body"><h2 id="settings_nickname" class="mb-4">比特鑫哥</h2><input type="text" style="display: none;" id="settings_user_id"><div class="row align-items-center"><div class="col-auto"><a id="settings_avatar" class="avatar avatar-xl" style="background-image: url(./image/avatar02.jpeg)"onclick="openFileDialog()"></a><!-- 文件选择 --><input type="file" class="form-control" style="display: none;" id="settings_input_chooiceAvatar"></div><div class="col-auto"><a href="javascript:void(0);" class="btn" onclick="openFileDialog()">修改头像</a></div></div><h3 class="card-title mt-4">昵称</h3><div class="row"><div class="col-9"><input id="setting_input_nickname" type="text" class="form-control"><div class="invalid-feedback">昵称不能为空</div></div><div class="col-3"><a id="setting_submit_nickname" href="javascript:void(0)" class="btn">修 改</a></div></div><hr><h3 class="card-title mt-4">邮箱地址</h3><div><div class="row g-3"><div class="col-9"><input id="setting_input_email" type="text" class="form-control"><div class="invalid-feedback">邮箱地址不能为空</div></div><div class="col-3"><a id="setting_submit_email" href="javascript:void(0)" class="btn">修 改</a></div></div></div><hr><h3 class="card-title mt-4">电话号码</h3><div><div class="row g-3"><div class="col-9"><input id="setting_input_phoneNum" type="text" class="form-control"><div class="invalid-feedback">电话号码不能为空</div></div><div class="col-3"><a id="setting_submit_phoneNum" href="javascript:void(0);" class="btn">修 改</a></div></div></div><hr><h3 class="card-title mt-4">修改密码</h3><div><div class="row g-3"><!-- 表单 --><div class="col-9"><form autocomplete="off" novalidate><div class="mb-2"><label class="form-label required">原密码</label><div class="input-group input-group-flat"><input id="settings_input_oldPassword" type="password" class="form-control" placeholder="请输入密码" autocomplete="off"><span class="input-group-text"><a href="#" class="link-secondary" title="显示密码"data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye --><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><pathd="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" /></svg></a></span><div class="invalid-feedback">原密码不能为空</div></div></div><div class="mb-2"><label class="form-label required">新原密码</label><div class="input-group input-group-flat"><input id="settings_input_newPassword" type="password" class="form-control" placeholder="请输入密码" autocomplete="off"><span class="input-group-text"><a href="#" class="link-secondary" title="显示密码"data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye --><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><pathd="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" /></svg></a></span><div class="invalid-feedback">新密码不能为空</div></div></div><div class="mb-2"><label class="form-label required">确认密码</label><div class="input-group input-group-flat"><input id="settings_input_passwordRepeat" type="password" class="form-control" placeholder="请输入密码" autocomplete="off"><span class="input-group-text"><a href="#" class="link-secondary" title="显示密码"data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye --><svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><pathd="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" /></svg></a></span><div class="invalid-feedback">确认密码不能为空</div></div></div><div class="form-footer"><button id="settings_submit_password" type="button" class="btn btn-outline-danger w-100">提交修改</button></div></form></div></div></div><hr><h3 class="card-title mt-4">个人简介</h3><div class="row"><div class="col-9"><textarea id="settings_textarea_remark" class="form-control" placeholder="写点自我介绍,可以让朋友们了解你..."rows="5"></textarea><div class="invalid-feedback">个人简介不能为空</div></div><div class="col-3"><a href="javascript:void(0);" class="btn" id="settings_submit_remark">修 改</a></div></div></div></div></div></div></div>
</div>
<script>$(function() {// ================= 获取用户详情,初始化页面内容 =================// 发送AJAX请求,成功时 调用initUserInfo方法,完成页面数据初始化$.ajax({type : 'get',url: 'user/info',// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {// 成功之后调用初始化页面上的用户信息initUserInfo(respData.data);} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})}},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'});}});});// ================= 设置用户信息 =================function initUserInfo(user) {// 默认头像路径if (!user.avatarUrl) {user.avatarUrl = avatarUrl;}// 用户Id$('#settings_user_id').val(user.id);// title 昵称$('#settings_nickname').html(user.nickname);// 头像$('#settings_avatar').css('background-image', 'url(' + user.avatarUrl + ')');// 昵称$('#setting_input_nickname').val(user.nickname);// 邮箱$('#setting_input_email').val(user.email);// 电话$('#setting_input_phoneNum').val(user.phoneNum);// 个人简历$('#settings_textarea_remark').html(user.remark);}// ================= 封装ajax请求 =================function changeUserInfo(userInfo, type) {// 校验用户信息if (!userInfo) {$.toast({heading: '提示',text: '请检查要修改的内容是否正确或联系管理员',icon: 'info'});return;}// 定义接口路径let userURL = 'user/modifyInfo';if (type == 1) {userURL = 'user/modifyInfo';} else if (type == 2) {userURL = 'user/modifyPwd';}// 发送请求,提示响应结果$.ajax({type : 'POST',url : userURL,contentType : 'application/x-www-form-urlencoded',data : userInfo, // 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {let user = respData.data;//修改密码之后跳转到登录页面if(type==2){location.assign("sign-in.html");return;}// 修改页面昵称if (user.nickname) {// 当前页面$('#settings_nickname').html(user.nickname);// 导航栏$('#index_nav_nickname').html(user.nickname);}// 成功之后弹出提示框$.toast({heading: '成功',text: respData.message,icon: 'success'})} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})}},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'});}});}// ================= 处理选择头像事件 =================function openFileDialog () {// 触发选择文件按钮的点击事件$('#settings_input_chooiceAvatar').click();}// ================= 修改用户昵称 =================$('#setting_submit_nickname').click(function(){// 获取值let nicknameEl = $('#setting_input_nickname');// 校验if(!nicknameEl.val()) {nicknameEl.focus();return false;}// 构造数据let nicknameObj = {nickname : nicknameEl.val()}// 发送请求changeUserInfo(nicknameObj);});// ================= 修改邮箱 =================$('#setting_submit_email').click(function(){// 获取值let emailEl = $('#setting_input_email');// 校验if(!emailEl.val()) {emailEl.focus();return false;}// 构造数据let emailObj = {email : emailEl.val()}// 发送请求changeUserInfo(emailObj);});// ================= 修改电话 =================$('#setting_submit_phoneNum').click(function(){// 获取值let phoneNumEl = $('#setting_input_phoneNum');// 校验if(!phoneNumEl.val()) {phoneNumEl.focus();return false;}// 构造数据let phoneNumObj = {phoneNum : phoneNumEl.val()}// 发送请求changeUserInfo(phoneNumObj);});// ================= 修改个人介绍 =================$('#settings_submit_remark').click(function(){// 获取值let remarkEl = $('#settings_textarea_remark');// 校验if(!remarkEl.val()) {remarkEl.focus();return false;}// 构造数据let remarkObj = {remark : remarkEl.val()}// 发送请求changeUserInfo(remarkObj);});// ================= 修改密码 =================$('#settings_submit_password').click(function() {// 获取值let oldPasswordEl = $('#settings_input_oldPassword');// 校验if(!oldPasswordEl.val()) {oldPasswordEl.focus();return false;}// 获取值let newPasswordEl = $('#settings_input_newPassword');// 校验if(!newPasswordEl.val()) {newPasswordEl.focus();return false;}// 获取值let passwordRepeatEl = $('#settings_input_passwordRepeat');// 校验if(!passwordRepeatEl.val()) {passwordRepeatEl.focus();return false;}// 两次输入的密码是否相同if (newPasswordEl.val() != passwordRepeatEl.val()) {$.toast({heading: '提示',text: '两次输入的密码不相同',icon: 'warning'});// 获取焦点passwordRepeatEl.focus();return false;}// 构造数据let  passwrodObj = {oldPassword : oldPasswordEl.val(),newPassword : newPasswordEl.val(),passwordRepeat : passwordRepeatEl.val()}// 发送请求changeUserInfo(passwrodObj, 2);// 清空输入框oldPasswordEl.val('');newPasswordEl.val('');passwordRepeatEl.val('');});</script>

4.3.5、版块信息

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

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

请求
// 请求
GET http: //127.0.0.1:58080/board/topList HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : [
{
"id" : 1 ,
"name" : "Java" ,
"articleCount" : 5 ,
"sort" : 1 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 2 ,
"name" : "C++" ,
"articleCount" : 1 ,
"sort" : 2 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 3 ,
"name" : " 前端技术 " ,
"articleCount" : 0 ,
"sort" : 3 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 4 ,
"name" : "MySQL" ,
"articleCount" : 0 ,
"sort" : 4 ,
"state" : 0 ,
"createTime "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 5 ,
"name" : " ⾯试宝典 " ,
"articleCount" : 0 ,
"sort" : 5 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 6 ,
"name" : " 经验分享 " ,
"articleCount" : 0 ,
"sort" : 6 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx"
},
{
"id" : 7 ,
"name" : " 灌⽔区 " ,
"articleCount" : 0 ,
"sort" : 9 ,
"state" : 0 ,
"createTime" : "2023-01-25 13:26:12" ,
"updateTime" : "2023-01-25 13:26:12"
}
]
}
扩展Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.forum_system.dao.BoardMapper"><!-- 查询首页的版块列表 --><select id="selectByNum" resultMap="BaseResultMap" parameterType="java.lang.Integer">select<include refid="Base_Column_List" />from t_boardwhere state=0and deleteState=0order by sort asclimit 0,#{num,jdbcType=INTEGER}</select>
</mapper>
修改DAO
    /*** 查询首页的版块列表* @param num* @return*/List<Board> selectByNum(@Param("num") Integer num);
创建Service接口
public interface IBoardService {/*** 查询首页的版块列表* @param num* @return*/List<Board> selectByNum(Integer num);
}
实现Service接口
@Slf4j
@Service
public class BoardServiceImpl implements IBoardService {@Resourceprivate BoardMapper boardMapper;@Overridepublic List<Board> selectByNum(Integer num) {//非空校验if (num==null || num<=0){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//调用DAOList<Board> boards=boardMapper.selectByNum(num);//返回结果return boards;}
}
对Service接口进行单元测试
@SpringBootTest
class BoardServiceImplTest {@Resourceprivate IBoardService boardService;@Resourceprivate ObjectMapper objectMapper;@Testvoid selectByNum() throws JsonProcessingException {List<Board> boards=boardService.selectByNum(9);System.out.println(objectMapper.writeValueAsString(boards));}
}

application.yml添加配置
#项目自定义配置
forum_system:login:url: sign-in.html  #未登录状态下强制跳转页面index:board-num: 9  #首页中显示的版块个数
实现Controller层
@Api(tags = "版块接口")
@Slf4j
@RestController
@RequestMapping("/board")
public class BoardController {//从配置文件中获取主页中显示的版块个数,默认为9@Value("${forum_system.index.board-num:9}")private Integer indexBoardNum;@Resourceprivate IBoardService boardService;@ApiOperation("获取首页中的版块列表")@GetMapping("/topList")public AppResult<List<Board>> topList(){//直接调用ServiceList<Board> boards=boardService.selectByNum(indexBoardNum);//返回结果return AppResult.success(boards);}
}
测试接口

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

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

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

请求

// 请求

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

响应

// 响应

HTTP/1.1 200

Content-Type: application/json

{

"code": 0,

"message": "成功",

"data": {

"id": 1,

"name": "Java",

"articleCount": 5,

"sort": 1,

"state": 0,

"createTime": "xxx",

"updateTime": "xxx"

}

}

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

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

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

4.3.6、帖子列表

4.3.6.1、版块帖子列表

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

请求
// 请求
// 返回指定版块下的帖⼦列表
GET http: //127.0.0.1:58080/article/getAllByBoardId?boardId=1 HTTP/1.1
// 返回所有的帖⼦列表
GET http: //127.0.0.1:58080/article/getAllByBoardId HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : [
{
"id" : 1 ,
"boardId" : 1 ,
"userId" : 1 ,
"title" : " 测试删除 " ,
"visitCount" : 8 ,
"replyCount" : 1 ,
"likeCount" : 1 ,
"state" : 0 ,
"createTime" : "xxx" ,
"updateTime" : "xxx" ,
"board" : {
"id" : 1 ,
"name" : "Java"
},
"user" : {
"id" : 1 ,
"nickname" : "xxx" ,
"phoneNum" : null ,
"email" : null ,
"gender" : 1 ,
"avatarUrl" : null
},
"own" : false
}
}
修改Article实体类
    //添加关联对象private User user;//添加关联对象private Board board;
扩展Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.forum_system.dao.ArticleMapper"><!--自定义结果集,设置User对象和Board对象的映射关系 --><resultMap id="AllInfoResultMap" type="com.example.forum_system.model.Article" extends="ResultMapWithBLOBs"><!-- 关联的User对象映射关系 --><association property="user" resultMap="com.example.forum_system.dao.UserMapper.BaseResultMap" columnPrefix="u_" /><!-- 关联的Board对象映射关系 --><association property="board" resultMap="com.example.forum_system.dao.BoardMapper.BaseResultMap" columnPrefix="b_" /></resultMap><!-- 查询所有帖子 --><select id="selectAll" resultMap="AllInfoResultMap">selectu.id as u_id,u.nickname as u_nickname,u.avatarUrl as u_avatarUrl,u.gender as u_gender,u.state as u_state,b.id as b_id,b.name as b_name,a.id,a.boardId,a.userId,a.title,a.visitCount,a.replyCount,a.likeCount,a.state,a.deleteState,a.createTime,a.updateTimefrom t_article a, t_board b, t_user uwhere a.boardId = b.idand a.userId = u.idand a.deleteState = 0order by a.createTime desc</select><!--  根据版块Id查询帖子列表  --><select id="selectByBoardId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">selectu.id as u_id,u.nickname as u_nickname,u.avatarUrl as u_avatarUrl,u.gender as u_gender,u.state as u_state,a.id,a.boardId,a.userId,a.title,a.visitCount,a.replyCount,a.likeCount,a.state,a.deleteState,a.createTime,a.updateTimefrom t_article a, t_user uwhere a.userId = u.idand a.deleteState = 0and a.boardId = #{boardId,jdbcType=BIGINT}order by a.createTime desc</select>
</mapper>
修改DAO
    /*** 获取所有的帖子的集合* @return*/List<Article> selectAll();/*** 根据版块Id查询帖子列表* @param boardId* @return*/List<Article> selectByBoardId(@Param("boardId") Long boardId);
创建Service接口
public interface IArticleService {/*** 获取所有的帖子的集合* @return*/List<Article> selectAll();/*** 根据版块Id查询帖子列表* @param boardId* @return*/List<Article> selectByBoardId(Long boardId);
}
实现Service接口
@Service
@Slf4j
public class ArticleServiceImpl implements IArticleService {@Resourceprivate ArticleMapper articleMapper;@Overridepublic List<Article> selectAll() {//直接调用DAOList<Article> articles=articleMapper.selectAll();//返回结果return articles;}@Overridepublic List<Article> selectByBoardId(Long boardId) {//非空校验if (boardId==null || boardId<=0){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//调用DAOList<Article> articles=articleMapper.selectByBoardId(boardId);//返回结果return articles;}
}
对Service接口进行单元测试
@SpringBootTest
class ArticleServiceImplTest {@Resourceprivate IArticleService articleService;@Resourceprivate ObjectMapper objectMapper;@Testvoid selectAll() throws JsonProcessingException {List<Article> articles=articleService.selectAll();System.out.println(objectMapper.writeValueAsString(articles));}@Testvoid selectByBoardId() throws JsonProcessingException {List<Article> articles=articleService.selectByBoardId(1L);System.out.println(objectMapper.writeValueAsString(articles));articles=articleService.selectByBoardId(2L);System.out.println(objectMapper.writeValueAsString(articles));}
}

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

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

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

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

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

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

前端代码
    // 构造查询用户信息的queryStringlet articleListQueryString = '';if (currentUserId) {articleListQueryString = '?userId=' + currentUserId}// ============= 获取当前用户发贴 =============// url: 'article/getAllByUserId' + articleListQueryString// 成功后,调用buildProfileUserArticle()方法,构造帖子列表$.ajax({type:'GET',url:'article/getAllByUserId'+articleListQueryString,// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {//设置用户信息buildProfileUserArticle(respData.data);} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})  }},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'}) }});// ============= 构建用户帖子列表 =============function buildProfileUserArticle(data) {// 没有帖子if(data.length == 0) {$('#profile_article_body').html('还没有帖子');return;}// 构建帖子列表data.forEach(article => {let articleHtmlStr = ' <li class="timeline-event">'+ ' <div class="timeline-event-icon bg-twitter-lt">'+ ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-plus" width="24"'+ ' height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"'+ ' stroke-linecap="round" stroke-linejoin="round">'+ ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'+ ' <path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4"></path>'+ ' <path d="M10 11l4 0"></path>'+ ' <path d="M12 9l0 4"></path>'+ ' </svg>'+ ' </div>'+ ' <div class="card timeline-event-card">'+ ' <div class="card-body">'+ ' <div>'+ ' <div class="row">'+ ' <div class="col">'+ ' <div class="text-truncate">'+ ' <a href="javascript:void(0);"  class="profile_article_list_a_title">'+ ' <strong>' + article.title + '</strong>'+ ' </a>'+ ' </div>'+ ' <div class="text-muted mt-2">'+ ' <div class="row">'+ ' <div class="col">'+ ' <ul class="list-inline list-inline-dots mb-0">'+ ' <li class="list-inline-item">'+ ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clock-edit"'+ ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'+ ' fill="none" stroke-linecap="round" stroke-linejoin="round">'+ ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'+ ' <path d="M21 12a9 9 0 1 0 -9.972 8.948c.32 .034 .644 .052 .972 .052"></path>'+ ' <path d="M12 7v5l2 2"></path>'+ ' <path d="M18.42 15.61a2.1 2.1 0 0 1 2.97 2.97l-3.39 3.42h-3v-3l3.42 -3.39z"></path>'+ ' </svg> '+ article.createTime+ ' </li>'+ ' </ul>'+ ' </div>'+ ' <div class="col-auto d-none d-md-inline">'+ ' <ul class="list-inline list-inline-dots mb-0">'+ ' <li class="list-inline-item">'+ ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-eye"'+ ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'+ ' fill="none" stroke-linecap="round" stroke-linejoin="round">'+ ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'+ ' <path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>'+ ' <path'+ ' d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7">'+ ' </path>'+ ' </svg> '+ article.visitCount+ ' </li>'+ ' <li class="list-inline-item">'+ ' <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-heart"'+ ' width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"'+ ' fill="none" stroke-linecap="round" stroke-linejoin="round">'+ ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'+ ' <path'+ ' d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572">'+ ' </path>'+ ' </svg> '+ article.likeCount+ ' </li>'+ ' <li class="list-inline-item">'+ ' <svg xmlns="http://www.w3.org/2000/svg"'+ ' class="icon icon-tabler icon-tabler-message-circle" width="24" height="24"'+ ' viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"'+ ' stroke-linecap="round" stroke-linejoin="round">'+ ' <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>'+ ' <path d="M3 20l1.3 -3.9a9 8 0 1 1 3.4 2.9l-4.7 1"></path>'+ ' <path d="M12 12l0 .01"></path>'+ ' <path d="M8 12l0 .01"></path>'+ ' <path d="M16 12l0 .01"></path>'+ ' </svg> '+ article.replyCount+ ' </li>'+ ' </ul>'+ ' </div>'+ ' </div>'+ ' </div>'+ ' </div>'+ ' </div>'+ ' </div>'+ ' </div>'+ ' </div>'+ ' </li>';// 追加到父标签let profileArtilceItem = $(articleHtmlStr);// 获取标题的 a 标签let articleTitle = profileArtilceItem.find('.profile_article_list_a_title');// 处理标题点击事件articleTitle.click(function() {// 通过全局变量保存当前访问的帖子信息currentArticle = article;$('#bit-forum-content').load('details.html');});// 追加到父标签$('#profile_article_body').append(profileArtilceItem);});}

 

4.3.7、帖子操作

4.3.7.1、集成编辑区

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

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

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

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

IUserService实现方法

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

IBoardService实现方法

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

IArticleService实现方法

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

UserServiceImpl实现方法

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

BoardServiceImpl实现方法

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

ArticleServiceImpl实现方法

    @Overridepublic void create(Article article) {//非空校验if (article==null || article.getBoardId()==null || article.getBoardId()<=0|| article.getUserId()==null || article.getUserId()<=0|| StringUtils.isEmpty(article.getTitle()) || StringUtils.isEmpty(article.getContent())){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//设置默认值article.setVisitCount(0);  //访问数量article.setReplyCount(0);  //回复数article.setLikeCount(0);  //点赞数article.setState((byte) 0);  //状态article.setDeleteState((byte) 0);  //删除状态Date date=new Date();article.setCreateTime(date);  //发布时间article.setUpdateTime(date);  //更新时间//写入数据库int row=articleMapper.insertSelective(article);if (row!=1){//打印日志log.warn(ResultCode.ERROR_SERVICES.toString()+",帖子写入失败");//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//更新用户发帖数userService.addOneArticleCountById(article.getUserId());//更新版块帖子数boardService.addOneArticleCountById(article.getBoardId());//log.info("发帖成功:userId"+article.getUserId()+",boardId:"+article.getBoardId());}
对Service接口进行单元测试
    @Testvoid addOneArticleCountById() {userService.addOneArticleCountById(2L);System.out.println("更新成功");}

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

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

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

前端代码
    // ================== 处理发贴按钮事件 =======================$('#article_post_submit').click(function () {let boardIdEl = $('#article_post_borad');let titleEl = $('#article_post_title');let contentEl = $('#article_post_content');// 非空校验if (!titleEl.val()) {titleEl.focus();// 提示$.toast({heading: '提示',text: '请输入帖子标题',icon: 'warning'});return;}if (!contentEl.val()) {// 提示$.toast({heading: '提示',text: '请输入帖子内容',icon: 'warning'});return;}// 构造帖子对象let postData={boardId:boardIdEl.val(),title:titleEl.val(),content:contentEl.val()};// 提交, 成功后调用changeNavActive($('#nav_board_index'));回到首页并加载帖子列表// contentType: 'application/x-www-form-urlencoded'$.ajax({type:'POST',url:'article/create',contentType: 'application/x-www-form-urlencoded',data:postData,//回调success:function(respData){//根据code的值判断响应是否成功if(respData.code==0){//成功changeNavActive($('#nav_board_index'));}else{//失败$.toast({heading:'失败',text:respData.message,icon:'warning'})}},error:function(){$.toast({heading:'错误',text:'访问网站出现问题,请与管理员联系',icon:'error'})}});});
4.3.7.3、获取帖子详情

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

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

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

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

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

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

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

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

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

前端代码
    // ========================== 获取帖子详情 ========================== // 成功后,设置ID,版块名,标题,并初始编辑区同时设置正文initEditor(edit_article.content);$.ajax({type:'GET',url:'article/details?id='+currentArticle.id,// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {let edit_article=respData.data;//设置Id$('#edit_article_id').val(edit_article.id);//设置版块名$('#edit_article_board_name').html(edit_article_board_name);//设置标题$('#edit_article_title').html(edit_article_title);//设置内容$('#edit_article_content').html(edit_article_content);// 成功之后调用构建版块列表的方法initEditor(edit_article.content);} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})  }},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'}) }});// ========================== 初始化编辑器 ========================== var editor;function initEditor (md) {console.log('编辑区内容:' + md);editor = editormd("edit_article_content_area", {width: "100%",height: "100%",// theme : "dark",// previewTheme : "dark",// editorTheme : "pastel-on-dark",codeFold: true,markdown : md, // 处理编辑区内容//syncScrolling : false,saveHTMLToTextarea: true,    // 保存 HTML 到 TextareasearchReplace: true,watch : true,                    // 实时预览htmlDecode: "style,script,iframe|on*",            // 开启 HTML 标签解析,为了安全性,默认不开启    // toolbar  : false,             //关闭工具栏// previewCodeHighlight : false, // 关闭预览 HTML 的代码块高亮,默认开启emoji: true,taskList: true,tocm: true,         // Using [TOCM]tex: true,                     // 开启科学公式TeX语言支持,默认关闭// flowChart: true,               // 开启流程图支持,默认关闭// sequenceDiagram: true,         // 开启时序/序列图支持,默认关闭,placeholder: '开始创作...',     // 占位符path: "./dist/editor.md/lib/"});}// ========================== 处理提交修改事件 ========================== $('#edit_article_submit').click(function () {// IDlet articleIdEl = $('#edit_article_id');if (!articleIdEl.val()) {// 提示$.toast({heading: '提示',text: '数据不正确,请刷新后重试',icon: 'warning'});return;}//titlelet articleTitleEl = $('#edit_article_title');if (!articleTitleEl.val()) {// 提示$.toast({heading: '提示',text: '请输入帖子标题',icon: 'warning'});return;}// contentlet articleContentEl = $('#edit_article_content');// 非空校验if (!articleContentEl.val()) {// 提示$.toast({heading: '提示',text: '请输入帖子内容',icon: 'warning'});return;}// 构造修改对象let postData={id:articleIdEl.val(),title:articleTitleEl.val(),content:articleContentEl.val()};// 发送修改请求, 成功后跳转至首页changeNavActive($('#nav_board_index'));$.ajax({type:'POST',url:'article/modify',contentType : 'application/x-www-form-urlencoded',data:postData,// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {//成功后跳转到首页changeNavActive($('#nav_board_index'));} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})  }},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'}) }});});
4.3.7.5、删除帖子
请求
// 请求
GET http: //127.0.0.1:58080/article/delete?id=11 HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口

IUserService实现方法

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

IBoardService实现方法

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

IArticleService实现方法

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

UserServiceImpl实现方法

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

BoardServiceImpl实现方法

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

ArticleServiceImpl实现方法

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

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

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

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

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

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

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

4.3.8、帖子回复

4.3.8.1、提交回复内容

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

请求
// 请求
POST http: //127.0.0.1:58080/reply/create HTTP/1.1
Content-Type: application/x-www-form-urlencoded
articleId= 1 &content=%E5% 9 B% 9 E%E5%A4% 8 D%E6%B2%A1%E8%AF% 95
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
public interface IArticleReplyService {/*** 新增回复* @param articleReply*/@Transactionalvoid create(ArticleReply articleReply);
}
实现Service接口
@Service
@Slf4j
public class ArticleReplyServiceImpl implements IArticleReplyService {@Resourceprivate ArticleReplyMapper articleReplyMapper;@Resourceprivate IArticleService articleService;@Overridepublic void create(ArticleReply articleReply) {//非空校验if (articleReply==null || articleReply.getPostUserId()==null || articleReply.getPostUserId()<=0|| articleReply.getArticleId()==null || articleReply.getArticleId()<=0|| StringUtils.isEmpty(articleReply.getContent())){//打印日志log.info(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//获取帖子详情Article article=articleService.selectDetailById(articleReply.getArticleId());if (article==null || article.getDeleteState()==1){//帖子不存在throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));}if (article.getState()==1){//帖子封帖throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_STATE));}//设置默认值articleReply.setLikeCount(0);  //点赞数articleReply.setState((byte) 0);  //状态articleReply.setDeleteState((byte) 0);  //删除状态Date date=new Date();articleReply.setCreateTime(date);  //创建时间articleReply.setUpdateTime(date);  //更新时间//调用DAOint row=articleReplyMapper.insertSelective(articleReply);if (row!=1){//打印日志log.info(ResultCode.ERROR_SERVICES.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}//更新帖子的回复数Article updateArticle=new Article();updateArticle.setId(article.getId());  //帖子IdupdateArticle.setReplyCount(article.getReplyCount()+1);  //回复数updateArticle.setUpdateTime(new Date());  //更新时间//调用帖子的更新方法articleService.updateById(updateArticle);//打印日志log.info("回复成功,article id="+article.getId());}
}
对Service接口进行单元测试
@SpringBootTest
class ArticleReplyServiceImplTest {@Resourceprivate IArticleReplyService articleReplyService;@Resourceprivate ObjectMapper objectMapper;@Testvoid create() {ArticleReply reply=new ArticleReply();reply.setPostUserId(1L);reply.setArticleId(1L);reply.setContent("测试内容回复");articleReplyService.create(reply);System.out.println("回复成功");}
}

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

前端代码
    // ====================== 回复帖子 ======================$('#details_btn_article_reply').click(function () {let articleIdEl = $('#details_article_id');let replyContentEl = $('#details_article_reply_content');// 非空校验if (!replyContentEl.val()) {// 提示$.toast({heading: '提示',text: '请输入回复内容',icon: 'warning'});return;}// 构造帖子对象let postData={articleId:articleIdEl.val(),content:replyContentEl.val()};// 发送请求,成功后 // 1. 清空回复区域// 2. 更新回贴数 currentArticle.replyCount = currentArticle.replyCount + 1;// 3. 调用loadArticleDetailsReply()方法,重新构建回贴列表$.ajax({type:'POST',url:'reply/create',contentType : 'application/x-www-form-urlencoded',data:postData,// 回调success : function (respData) {// 根据code的值判断响应是否成功if (respData.code == 0) {//清空输入区editor.setValue('');//更新全局变量中replyCount的值currentArticle.replyCount = currentArticle.replyCount + 1;//设置页面中回复数的值$('#details_article_replyCount').html(currentArticle.replyCount);//加载回复列表loadArticleDetailsReply();$.toast({heading: '提⽰',text: respData.message,icon: 'success'});} else {// 失败$.toast({heading: '失败',text: respData.message,icon: 'warning'})  }},error : function () {$.toast({heading: '错误',text: '访问网站出现问题,请与管理员联系',icon: 'error'}) }});});
4.3.8.2、帖子回复列表

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

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

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

    //关联用户对象private User user;
扩展Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.forum_system.dao.ArticleReplyMapper"><!-- 自定义表关联的结查集 --><resultMap id="AllInfoResultMap" type="com.example.forum_system.model.ArticleReply" extends="BaseResultMap"><!-- 关联对象的映射 --><association property="user" resultMap="com.example.forum_system.dao.UserMapper.BaseResultMap" columnPrefix="u_" /></resultMap><!--  根据主帖Id查询所有回复  --><select id="selectByArticleId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">selectu.id as u_id,u.nickname as u_nickname,u.gender as u_gender,u.avatarUrl as u_avatarUrl,u.phoneNum as u_phoneNum,u.email as u_email,ar.id,ar.articleId,ar.postUserId,ar.replyId,ar.replyUserId,ar.content,ar.likeCount,ar.state,ar.createTime,ar.updateTimefrom t_article_reply ar, t_user uwhere ar.postUserId = u.idand ar.deleteState = 0and ar.articleId = #{articleId,jdbcType=BIGINT}order by ar.createTime desc</select>
</mapper>
修改DAO
    /*** 根据主帖Id查询回复列表* @param articleId* @return*/List<ArticleReply> selectByArticleId(@Param("articleId") Long articleId);
创建Service接口
/*** 根据主帖Id查询回复列表* @param articleId* @return*/List<ArticleReply> selectByArticleId(Long articleId);
实现Service接口
    @Overridepublic List<ArticleReply> selectByArticleId(Long articleId) {//非空校验if (articleId==null || articleId<=0){//打印日志log.info(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//校验主帖是否存在Article article=articleService.selectDetailById(articleId);if (article==null || article.getDeleteState()==1){//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));}//调用DAOList<ArticleReply> articleReplies=articleReplyMapper.selectByArticleId(articleId);//返回结果集return articleReplies;}
对Service接口进行单元测试
    @Testvoid selectByArticleId() throws JsonProcessingException {List<ArticleReply> articleReplies=articleReplyService.selectByArticleId(1L);System.out.println("回复数为:"+articleReplies.size());System.out.println(objectMapper.writeValueAsString(articleReplies));articleReplies=articleReplyService.selectByArticleId(6L);System.out.println("回复数为:"+articleReplies.size());System.out.println(objectMapper.writeValueAsString(articleReplies));}

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

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

4.3.9、站内信

4.3.9.1、发送

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

请求
// 请求
POST http: //127.0.0.1:58080/message/send HTTP/1.1
Content-Type: application/x-www-form-urlencoded
receiveUserId= 2 &content=%E4%BD%A0%E5%A5%BD%E5% 95 % 8 A
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
public interface IMessageService {/*** 发送站内信息* @param message*/void create(Message message);
}
实现Service接口
@Slf4j
@Service
public class MessageServiceImpl implements IMessageService {@Resourceprivate MessageMapper messageMapper;@Resourceprivate IUserService userService;@Overridepublic void create(Message message) {//非空校验if (message==null || message.getPostUserId()==null || message.getReceiveUserId()==null|| StringUtils.isEmpty(message.getContent())){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//校验接收者是否存在User user=userService.selectById(message.getReceiveUserId());if (user==null || user.getDeleteState()==1){//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//设置默认值message.setState((byte) 0);  //表示未读状态message.setDeleteState((byte) 0);//设置创建于更新时间Date date=new Date();message.setCreateTime(date);message.setUpdateTime(date);//调用DAOint row=messageMapper.insertSelective(message);if (row!=1){//打印日志log.warn(ResultCode.FAILED_CREATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));}}
}
对Service接口进行单元测试
@SpringBootTest
class MessageServiceImplTest {@Resourceprivate IMessageService messageService;@Resourceprivate ObjectMapper objectMapper;@Testvoid create() {Message message=new Message();message.setPostUserId(3L);message.setReceiveUserId(1L);message.setContent("你好,我是krystal");messageService.create(message);System.out.println("新增站内信成功");}
}

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

前端代码
    // ============ 发送站内信 ==============$('#btn_index_send_message').click(function() {// 获取输入内容let receiveUserIdEl = $('#index_message_receive_user_id');let messageContentEl = $('#index_message_receive_content');// 校验if (!receiveUserIdEl.val()) {$.toast({heading: '警告',text: '出错了,请联系管理员',icon: 'warning'});return;}if (!messageContentEl.val()) {$.toast({heading: '警告',text: '请输入要发送的内容',icon: 'warning'});// 输入框messageContentEl.focus();retrun;}// 构造发送数据let postData = {receiveUserId : receiveUserIdEl.val(),content : messageContentEl.val()};// 发送站内信请求 url = message/send, 成功与失败都调用cleanMessageForm()方法,清空输入框$.ajax({type:'POST',url:'message/send',contentType : 'application/x-www-form-urlencoded',data:postData,//回调success:function(respData){//根据code的值判断响应是否成功if(respData.code==0){//成功cleanMessageForm();//提示信息$.toast({heading:'成功',text:respData.message,icon:'success'});}else{//失败$.toast({heading:'失败',text:respData.message,icon:'warning'})}},error:function(){$.toast({heading:'错误',text:'访问网站出现问题,请与管理员联系',icon:'error'})}});});
4.3.9.2、未读数

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

请求
// 请求
GET http: //127.0.0.1.41:58080/message/getUnreadCount HTTP/1.1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{ "code" : 0 , "message" : " 成功 " , "data" : 1 }
扩展Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 与源配置⽂件使⽤相同命名空间-->
<mapper namespace="com.example.forum_system.dao.MessageMapper"><select id="selectUnreadCount" parameterType="java.lang.Long"resultType="java.lang.Integer">select COUNT(*) from t_messageWHERE state = 0and deleteState = 0and receiveUserId = #{receiveUserId,jdbcType=BIGINT}</select>
</mapper>
修改DAO
    /*** 查询当前登录用户的未读站内信数量* @param userId* @return*/Integer selectUnreadCount(Long userId);
创建Service接口
    /*** 查询当前登录用户的未读站内信数量* @param userId* @return*/Integer selectUnreadCount(Long userId);
实现Service接口
    @Overridepublic Integer selectUnreadCount(Long userId) {//非空校验if (userId==null || userId<=0){//记录日志log.info(ResultCode.ERROR_IS_NULL.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_IS_NULL));}//调用DAOInteger result= messageMapper.selectUnreadCount(userId);//返回结果return result;}
对Service接口进行单元测试
    @Testvoid selectUnreadCount() {Integer result=messageService.selectUnreadCount(1L);System.out.println(result);System.out.println("查询成功");}

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

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

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

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

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

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

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

请求
// 请求
POST http: //127.0.0.1:58080/message/markRead HTTP/1.1
Content-Type: application/x-www-form-urlencoAded
id= 1
响应
// 响应
HTTP/ 1.1 200
Content-Type: application/json
{
"code" : 0 ,
"message" : " 成功 " ,
"data" : null
}
创建Service接口
    /*** 读取站内信* @param id* @return*/Message selectById(Long id);/*** 根据Id更新* @param id* @param state*/void updateStateById(Long id,Byte state);
实现Service接口
    @Overridepublic Message selectById(Long id) {//非空校验if (id==null || id<=0){//记录日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//调用DAOMessage message=messageMapper.selectByPrimaryKey(id);//返回结果return message;}@Overridepublic void updateStateById(Long id, Byte state) {//非空校验if (id==null || id<=0 || state<0 || state>2){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//构造更新对象Message updateMessage=new Message();updateMessage.setId(id);  //用户IdupdateMessage.setState(state);  //站内信状态Date date=new Date();updateMessage.setUpdateTime(date);  //更新时间//调用DAOint row=messageMapper.updateByPrimaryKeySelective(updateMessage);if (row!=1){//打印日志log.warn(ResultCode.ERROR_SERVICES.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
对Service接口进行单元测试
    @Testvoid selectById() throws JsonProcessingException {Message message=messageService.selectById(1L);System.out.println(objectMapper.writeValueAsString(message));System.out.println("读取站内信成功");}@Testvoid updateStateById() {messageService.updateStateById(1L, (byte) 1);System.out.println("更新状态成功");}

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

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

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

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

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

前端代码
    // ============ 绑定发送按钮事件 ============$('#btn_index_send_message_reply').click(function () {// 校验用户输入let replyReceiveContentEl = $('#index_message_reply_receive_content');if (!replyReceiveContentEl.val()) {$.toast({heading: '警告',text: '请输入要回复的内容',icon: 'warning'});// 输入框replyReceiveContentEl.focus();retrun;}// 构造请求数据let postData = {repliedId: $('#index_message_detail_id').val(),receiveUserId : $('#index_message_reply_receive_user_id').val(),content: replyReceiveContentEl.val()};// 发送请求 message/reply// 回复成功后刷新未读标识和站内信列表// requestMessageUnreadCount();// requestMessageList();// // 清空输入区// cleanMessageReplyForm ();$.ajax ({type:'POST',url:'message/reply',contentType : 'application/x-www-form-urlencoded',data:postData,//回调success:function(respData){//根据code的值判断响应是否成功if(respData.code==0){//回复成功后刷新未读标识和站内信列表requestMessageUnreadCount();requestMessageList();// 清空输入区cleanMessageReplyForm ();//提示信息$.toast({heading:'成功',text:respData.message,icon:'success'})}else{//失败$.toast({heading:'失败',text:respData.message,icon:'warning'})}},error:function(){$.toast({heading:'错误',text:'访问网站出现问题,请与管理员联系',icon:'error'})}});

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

五、发布部署

1、执行SQL脚本

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

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

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

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

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

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

4、打包程序

5、上传到服务器

6、验证访问

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1052266.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何使用 Re-Ranking 改进大模型 RAG 检索

基于大型语言模型&#xff08;LLMs&#xff09;的聊天机器人可以通过检索增强生成&#xff08;RAG&#xff09;提供外部知识来改进。 这种外部知识可以减少错误答案&#xff08;幻觉&#xff09;&#xff0c;并且使模型能够访问其训练数据中未包含的信息。 通过RAG&#xff0…

科技产业园3D探秘:未来科技之城的奇幻之旅

在数字时代的浪潮中&#xff0c;科技产业园区成为了推动城市经济发展、科技创新的重要引擎。 当我们打开科技产业园的3D可视化模型&#xff0c;仿佛穿越时空&#xff0c;来到了一个充满奇幻色彩的科技世界。在这里&#xff0c;高楼大厦鳞次栉比&#xff0c;绿色植被点缀其间&am…

java图书电子商务网站的设计与实现源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的图书电子商务网站的设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 图书电子商…

堆结构知识点复习——玩转堆结构

前言:堆算是一种相对简单的数据结构&#xff0c; 本篇文章将详细的讲解堆中的知识点&#xff0c; 包括那些我们第一次学习堆的时候容易忽略的内容&#xff0c; 本篇文章会作为重点详细提到。 本篇内容适合已经学完C语言数组和函数部分的友友们观看。 目录 什么是堆 建堆算法…

Qt | QGridLayout 类(网格布局)

01、上节回顾 Qt | QBoxLayout 及其子类(盒式布局)02、QGridLayout 简介 1、网格布局原理(见下图): 基本原理是把窗口划分为若干个单元格,每个子部件被放置于一个或多个单元格之中,各 单元格的大小可由拉伸因子和一行或列中单元格的数量来确定,若子部件的大小(由 sizeH…

区别Scanner 类的 nextLine() 和 next() 方法

Scanner 类的 nextLine() 和 next() 方法区别有以下几点&#xff1a; 1、读取内容 nextLine() 方法读取输入直到遇到换行符&#xff08;\n 或 \r\n&#xff09;&#xff0c;包括空格和制表符在内的所有字符都将被读取。它返回的字符串是包含所有这些字符的一行。next() 方法读…

CLIP源码详解:clip.py 文件

前言 这是关于 CLIP 源码中的 clip.py 文件中的代码带注释版本。 clip.py 文件的作用&#xff1a;封装了 clip 项目的相关 API&#xff0c;通过这些 API &#xff0c;我们可以轻松使用 CLIP 项目预训练好的模型进行自己项目的应用。 另外不太容易懂的地方都使用了二级标题强…

必示科技参与智能运维国家标准预研线下编写会议并做主题分享

近日&#xff0c;《信息技术服务 智能运维 第3部分&#xff1a;算法治理》&#xff08;拟定名&#xff09;国家标准预研阶段第一次编写工作会议在杭州举行。本次会议由浙商证券承办。 此次编写有来自银行、证券、保险、通信、高校研究机构、互联网以及技术方等29家单位&#xf…

使用 Android Jetpack 的 Room 部分将数据保存到本地数据库

处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的使用场景是缓存相关的数据&#xff0c;这样一来&#xff0c;当设备无法访问网络时&#xff0c;用户仍然可以在离线状态下浏览该内容。 Room 持久性库在 SQLite 上提供了一个抽象层&#xff0c;以便在充分利…

Linux操作系统最著名的两大系列Red Hat和Debian

Linux操作系统可以根据其背后的项目或社区分为不同的系列&#xff0c;其中最著名的两大系列是Red Hat系列和Debian系列。 1.著名的两大系列是Red Hat和Debian Red Hat系列&#xff1a; Red Hat Enterprise Linux (RHEL)&#xff1a;这是Red Hat公司推出的企业级操作系统&#…

【CSharp】将ushort数组保存为1通道位深16bit的Tiff图片

【CSharp】将ushort数组保存为1通道位深16bit的Tiff图片 1.背景2.接口 1.背景 System.Drawing.Common 是一个用于图像处理和图形操作的库&#xff0c;它是 System.Drawing 命名空间的一部分。由于 .NET Core 和 .NET 5 的跨平台特性&#xff0c;许多以前内置于 .NET Framework…

产品经理-产品设计规范(六)

1. 设计规范 2. 七大定律 2.1 菲茨定律 2.1.1 概念 2.1.2 理解 2.1.3 启示 按钮等可点击对象需要合理的大小尺寸根据用户使用习惯合理设计按钮的相对和绝对位置屏幕的边和角很适合放置像菜单栏和按钮这样的元素 2.1.4 参考使用手机习惯 2.1.5 案例 2.2 席克定律 2.2.1 概念 …

数据仓库和数据挖掘基础

文章目录 1. 数据仓库基础知识1.1 数据仓库的基本特性1.2 数据仓库的数据模式1.3 数据仓库的体系结构 2. 数据挖掘基础知识2.1 数据挖掘的分类2.2 数据挖掘技术2.3 数据挖掘的应用过程 传统数据库在联机事务处理(OLTP)中获得了较大的成功&#xff0c;但是对管理人员的决策分析要…

微软中国区AI团队“打包赴美”?

大家好&#xff0c;我是程序员小灰。 最近两天&#xff0c;互联网上在疯传一个消息&#xff1a;微软公司突然发布邮件&#xff0c;让中国区 Azure 人工智能团队的部分员工集体打包去美国&#xff0c;涉及的项目组包括Azure ML和Azure Core等团队&#xff0c;总共涉及上百名员工…

30.哀家要长脑子了!---栈与队列

1.388. 文件的最长绝对路径 - 力扣&#xff08;LeetCode&#xff09; 其实看懂了就还好 用一个栈来保存所遍历过最大的文件的绝对路径的长度&#xff0c;栈顶元素是文件的长度&#xff0c;栈中元素的个数是该文件目录的深度&#xff0c;非栈顶元素就是当时目录的长度 检查此…

Leetcode260

260. 只出现一次的数字 III - 力扣&#xff08;LeetCode&#xff09; class Solution {public int[] singleNumber(int[] nums) {//通过异或操作,使得最终结果为两个只出现一次的元素的异或值int filterResult 0;for(int num:nums){filterResult^num;}//计算首个1(从右侧开始)…

单元测试(了解)

单元测试定义 针对最小功能单元&#xff08;方法&#xff09;&#xff0c;编写测试代码对其进行正确性测试 之前如何进行单元测试&#xff1f;有什么问题&#xff1f; main中编写测试代码&#xff0c;调用方法测试 问题&#xff1a; 无法自动化测试 每个方法的测试可能不是…

如何在go项目中实现发送邮箱验证码、邮箱+验证码登录

前期准备 GoLand &#xff1a;2024.1.1 下载官网&#xff1a;https://www.jetbrains.com/zh-cn/go/download/other.html Postman&#xff1a; 下载官网&#xff1a;https://www.postman.com/downloads/ 效果图(使用Postman) Google&#xff1a; QQ&#xff1a; And …

部署LAMP平台

目录 一、LAMP简介与概述 1.1 各组件作用 1.2 LAMP平台搭建时各组件安装顺序 1.3 httpd服务的目录结构 1.4 httpd.conf配置文件 二、编译安装Apache httpd服务 2.1 关闭防火墙&#xff0c;将安装Apache所需软件包传到/opt目录下 2.2 安装环境依赖包 ​2.3 配置软件模块…

01Python相关基础学习

Python基础 模块相关导入模块sys模块 模块相关 导入模块 1. import 模块名 2. import 模块名 as 别名 3. from 模块名 import 成员名 as 别名sys模块 1. sys.argv 介绍: 实现从程序的外部想程序传递参数返回的是一个列表,第一个元素是程序文件名,第二个元素是程序外部传入的…