目录
前言
个人论坛系统是一种在线社交平台,为用户提供了丰富的功能,让他们能够轻松地创建帖子、分享信息、讨论话题以及互动交流。
整个项目在开发过程中直接进行了单元测试,具体的自动化测试(功能、界面)在此处进行。
一、项目背景
以下是该系统的主要业务功能:
-
用户注册:允许用户创建自己的账户,填写个人信息并进行注册,以便能够使用论坛系统的各项功能。
-
用户登录:已注册用户可以通过输入用户名和密码登录到论坛系统,以便访问其个人信息和发表帖子。
-
论坛主界面:提供了一个主页面,用户可以在这里浏览各个板块的帖子列表,切换不同的板块浏览不同的内容,同时显示当前用户的信息,如用户名、头像等,还可以在此页面发布新帖子。
-
帖子详情页:用户可以点击帖子标题或摘要进入帖子详情页面,可以查看帖子的详细内容,并进行点赞、编辑、删除等操作。
-
用户中心页:用户可以在个人中心页查看和修改自己的个人信息,包括用户名、头像、个人简介等,还可以查看收到的站内信(私信),并回复他人的信息。
-
站内信功能:用户可以在论坛系统内部发送和接收私信,进行一对一的交流和沟通,也可以在帖子下面回复其他用户的信息,进行公开的讨论和互动。
以上功能使得个人论坛系统成为一个活跃的在线社交平台,为用户提供了丰富的交流和互动机会。
二、相关技术及工具
构架 | 基于MVC实现前后端分离 |
服务器端技术 | SpringBoot、SpringMVC、MyBatis |
浏览器端技术 | HTML、CSS、JavaScript、jQuery、Bootstrap |
数据库 | MySQL |
项目构建工具 | Maven |
版本控制工具 | git+gitee |
开发工具 | IntelliJ IDEA 2022.3.3 |
API文档生成工具 | Swagger、Springfox |
前后端交互数据格式 | JSON |
三、数据库设计
根据数据库设计的方案,利用数据库客户端工具建立数据库及数据表,并生成相应的SQL脚本,具体步骤如下所示。
无特殊要求的情况下,每张表必须有长整型的自增主键,删除状态、创建时间、更新时间
-- ----------------------------
-- 创建数据库,并指定字符集
-- ----------------------------
drop database if exists java_forum;
create database java_forum character set utf8mb4 collate utf8mb4_general_ci;
-- 选择数据库
use java_forum;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 创建帖子表 t_article
-- ----------------------------
DROP TABLE IF EXISTS `t_article`;
CREATE TABLE `t_article` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '帖子编号,主键,自增',
`boardId` bigint(20) NOT NULL COMMENT '关联板块编号,非空',
`userId` bigint(20) NOT NULL COMMENT '发帖人,非空,关联用户编号',
`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题,非空,最大长度100个字符',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '帖子正文,非空',
`visitCount` int(11) NOT NULL DEFAULT 0 COMMENT '访问量,默认0',
`replyCount` int(11) NOT NULL DEFAULT 0 COMMENT '回复数据,默认0',
`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0正常 1 禁用,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0 否 1 是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '修改时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建帖子回复表 t_article_reply
-- ----------------------------
DROP TABLE IF EXISTS `t_article_reply`;
CREATE TABLE `t_article_reply` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号,主键,自增',
`articleId` bigint(20) NOT NULL COMMENT '关联帖子编号,非空',
`postUserId` bigint(20) NOT NULL COMMENT '楼主用户,关联用户编号,非空',
`replyId` bigint(20) NULL DEFAULT NULL COMMENT '关联回复编号,支持楼中楼',
`replyUserId` bigint(20) NULL DEFAULT NULL COMMENT '楼主下的回复用户编号,支持楼中楼',
`content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '回贴内容,长度500个字符,非空',
`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1禁用,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子回复表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建版块表 t_board
-- ----------------------------
DROP TABLE IF EXISTS `t_board`;
CREATE TABLE `t_board` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '版块编号,主键,自增',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '版块名,非空',
`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '帖子数量,默认0',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序优先级,升序,默认0,',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态,0 正常,1禁用,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '版块表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建站内信表 for t_message
-- ----------------------------
DROP TABLE IF EXISTS `t_message`;
CREATE TABLE `t_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '站内信编号,主键,自增',
`postUserId` bigint(20) NOT NULL COMMENT '发送者,并联用户编号',
`receiveUserId` bigint(20) NOT NULL COMMENT '接收者,并联用户编号',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容,非空,长度255个字符',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0未读 1已读,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '站内信表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建用户表 for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户编号,主键,自增',
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名,非空,唯一',
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '加密后的密码',
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称,非空',
`phoneNum` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址',
`gender` tinyint(4) NOT NULL DEFAULT 2 COMMENT '0女 1男 2保密,非空,默认2',
`salt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '为密码加盐,非空',
`avatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户头像URL,默认系统图片',
`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '发帖数量,非空,默认0',
`isAdmin` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否管理员,0否 1是,默认0',
`remark` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注,自我介绍',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1 禁言,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `user_username_index`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT =
Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
-- 写入版块信息数据
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (1, 'Java', 0, 1, 0, 0, '2023-01-14 19:02:18', '2023-01-14 19:02:18');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (2, 'C++', 0, 2, 0, 0, '2023-01-14 19:02:41', '2023-01-14 19:02:41');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (3, '前端技术', 0, 3, 0, 0, '2023-01-14 19:02:52', '2023-01-14 19:02:52');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (4, 'MySQL', 0, 4, 0, 0, '2023-01-14 19:03:02', '2023-01-14 19:03:02');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (5, '面试宝典', 0, 5, 0, 0, '2023-01-14 19:03:24', '2023-01-14 19:03:24');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (6, '经验分享', 0, 6, 0, 0, '2023-01-14 19:03:48', '2023-01-14 19:03:48');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (7, '招聘信息', 0, 7, 0, 0, '2023-01-25 21:25:33', '2023-01-25 21:25:33');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (8, '福利待遇', 0, 8, 0, 0, '2023-01-25 21:25:58', '2023-01-25 21:25:58');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (9, '灌水区', 0, 9, 0, 0, '2023-01-25 21:26:12', '2023-01-25 21:26:12');
insert into t_article values (null, 1, 1, '测试测试111', '测试测试内容内容111', 0,0,0,0,0, '2023-07-19 14:46:00', now());
insert into t_article values (null, 1, 1, '测试测试222', '测试测试内容内容222', 0,0,0,0,0, '2023-07-19 14:46:00', now());
insert into t_article values (null, 2, 1, '测试测试333', '测试测试内容内容333', 0,0,0,0,0, '2023-07-19 14:46:00', now());
四、软件开发
4.1、搭建环境
4.1.1、创建工程
由于目前IDEA不支持JDK1.8版本,所以在搭建环境后需要手动修改
添加依赖
修改一下几处:
4.1.2、配置application.yml文件
#spring全局配置 spring: application: name: forum_system #配置项目名称 output: ansi: enabled: always #控制台输出彩色日志 #服务器配置 server: port: 58080 #修改Tomcat的默认端口号 #日志配置 logging: pattern: dateformat: yyyy-MM-dd HH:mm:ss level: root: info #默认日志级别 com.example.forum_system: debug #指定包的日志级别 file: path: E:\idea-project\log\project\forum #日志保存目录
4.1.3、环境测试
创建测试接口
创建controller包,包下创建TestController.java
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello,baekhyun...";
}
}
出现以下页面证明测试成功
4.1.4、继续配置
<!-- 编译环境JDK版本 -->
<maven.compiler.source>${java.version}</maven.compiler.source>
<!-- 运行环境JVM版本 -->
<maven.compiler.target>${java.version}</maven.compiler.target>
<!-- 构建项目指定编码集 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4.2、公共组件
4.2.1、创建工程结构
4.2.2、配置数据源
添加相关依赖
<!-- 管理依赖版块号-->
<!--mysql-connector 数据库连接驱动包 -->
<mysql-connector.version>5.1.49</mysql-connector.version>
<!-- mybatis -->
<mybatis-starter.version>2.3.0</mybatis-starter.version>
<!-- 数据源 -->
<druid-starter.version>1.2.16</druid-starter.version>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!-- mybatis 依赖
其中已经包含了spring-jdbc不再重复引用,
此项目中使用spring-jdbc提供的HikariCP做为数据源, 相关配置在yml文件中
-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-starter.version}</version>
</dependency>
<!-- 阿里巴巴druid数据源,如果使用SpringBoot默认的数据源,删除或注释这个依赖即可 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-starter.version}</version>
</dependency>
配置application.yml
spring: datasource: url: jdbc:mysql://127.0.0.1:3306/java_forum?characterEncoding=utf8&useSSL=false # 数据库连接串 username: root # 数据库用户名 password: '19930112' # 数据库密码 driver-class-name: com.mysql.jdbc.Driver # 数据库驱动类
测试
@Resource
private DataSource dataSource;
@Test
void testDBConnection() throws SQLException {
System.out.println("dataSource="+dataSource.getClass());
Connection connection= dataSource.getConnection();
System.out.println("connection="+connection.getClass());
System.out.println(connection);
}
4.2.3、编写类与映射文件
根据数据库编写实体类
编写映射文件
编写Dao类
由于很多项目中数据库比较庞大,一个一个写类文件里的属性比较费时,所以在此处使用mybatis生成器插件
4.2.4、生成类与映射文件
引入依赖
<!-- mybatis生成器 --> <mybatis-generator-plugin-version>1.4.1</mybatis-generator-plugin-version>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>${mybatis-generator-plugin-version}</version>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<!-- 指定Maven中的执行阶段 -->
<phase>deploy</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<!-- 相关配置 -->
<configuration>
<!-- 打开日志 -->
<verbose>true</verbose>
<!-- 允许覆盖 -->
<overwrite>true</overwrite>
<!-- 配置文件路径 -->
<configurationFile>
src/main/resources/mybatis/generatorConfig.xml
</configurationFile>
</configuration>
</plugin>
创建generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 驱动包路径,location中路径替换成自己本地路径 -->
<classPathEntry location="C:\Users\19756\.m2\repository\mysql\mysql-connector-java\5.1.49\mysql-connector-java-5.1.49.jar"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 禁用自动生成的注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
<property name="suppressDate" value="true"/>
</commentGenerator>
<!-- 连接配置 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/java_forum?characterEncoding=utf8&useSSL=false"
userId="root"
password="19930112">
</jdbcConnection>
<javaTypeResolver>
<!-- 小数统一转为BigDecimal -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 实体类生成位置 -->
<javaModelGenerator targetPackage="com.example.forum_system.model" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- mapper.xml生成位置 -->
<sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- DAO类生成位置 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.example.forum_system.dao"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 配置生成表与实例, 只需要修改表名tableName, 与对应类名domainObjectName 即可-->
<table tableName="t_article" domainObjectName="Article" enableSelectByExample="false"
enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
enableUpdateByExample="false">
<!-- 类的属性用数据库中的真实字段名做为属性名, 不指定这个属性会自动转换 _ 为驼峰命名规则-->
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_article_reply" domainObjectName="ArticleReply" enableSelectByExample="false"
enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_board" domainObjectName="Board" enableSelectByExample="false" enableDeleteByExample="false"
enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_message" domainObjectName="Message" enableSelectByExample="false"
enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"
enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
<table tableName="t_user" domainObjectName="User" enableSelectByExample="false" enableDeleteByExample="false"
enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false">
<property name="useActualColumnNames" value="true"/>
</table>
</context>
</generatorConfiguration>
运行插件生成文件
点击mybatis.generator即生成相应的文件
接下来给实体类添加@Data注解,删除实体类的get、set方法,是代码更加简洁
添加获取主键值的选项
在mapper下的所有文件中的insert标签中添加
<insert id="insert" parameterType="com.bitejiuyeke.forum.model.User"
useGeneratedKeys="true" keyProperty="id" >
扫描配置
//配置类
@Configuration
//指定Mybatis的扫描路径
@MapperScan("com.example.forum_system.dao")
public class MybatisConfig {
}
application.xml更新配置
#mybatis相关配置 mybatis: mapper-locations: classpath:mapper/**/*.xml # 指定 xxxMapper.xml的扫描路径
测试
@Resource
private UserMapper userMapper;
@Test
void createUser(){
User user=new User();
user.setUsername("边伯贤");
user.setPassword("1992");
user.setNickname("啵啵虎");
user.setGender((byte) 2);
user.setSalt("123456");
user.setArticleCount(0);
user.setState((byte) 0);
user.setDeleteState((byte) 0);
Date date = new Date();
user.setCreateTime(date);
user.setUpdateTime(date);
userMapper.insertSelective(user);
System.out.println("写入成功");
User user1 = userMapper.selectByPrimaryKey(user.getId());
System.out.println(user1);
}
4.2.5、编写公共代码
定义状态码
在执行业务处理逻辑的过程中,会涉及到各种不同的状态,包括成功和失败。为了更好地管理和处理这些状态,可以使用枚举来定义状态码。
这些状态码可以根据具体业务需求进行扩展和调整。当业务中遇到新的问题时,可以根据需要添加新的状态码,并为其提供适当的描述,以便更好地反映业务逻辑和处理状态。
/**
* 系统状态码
*/
public enum ResultCode {
//定义状态码
SUCCESS (0, "操作成功"),
FAILED (1000, "操作失败"),
FAILED_UNAUTHORIZED (1001, "未授权"),
FAILED_PARAMS_VALIDATE (1002, "参数校验失败"),
FAILED_FORBIDDEN (1003, "禁止访问"),
FAILED_CREATE (1004, "新增失败"),
FAILED_NOT_EXISTS (1005, "资源不存在"),
FAILED_USER_EXISTS (1101, "用户已存在"),
FAILED_USER_NOT_EXISTS (1102, "用户不存在"),
FAILED_LOGIN (1103, "用户名或密码错误"),
FAILED_USER_BANNED (1104, "您已被禁言, 请联系管理员, 并重新登录."),
FAILED_TWO_PWD_NOT_SAME (1105, "两次输入的密码不一致"),
FAILED_BOARD_NOT_EXISTS (1201, "版块不存在"),
FAILED_ARTICLE_NOT_EXISTS (1301, "帖子不存在"),
FAILED_ARTICLE_STATE (1302, "帖子状态异常"),
MESSAGE_NOT_EXISTS (1401,"站内信不存在"),
ERROR_SERVICES (2000, "服务器内部错误"),
ERROR_IS_NULL (2001, "IS NULL.");
//状态码
int code;
//错误描述
String message;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
ResultCode(int code, String message){
this.code=code;
this.message=message;
}
@Override
public String toString() {
return "code="+code+",message="+message+".";
}
}
定义返回结果
在设计前后端分离的系统,并且希望统一返回JSON格式的字符串时,你可以定义一个类来封装返回的数据。这个类可以包含以下属性:
- 状态码(status code):表示请求的处理状态,比如成功、失败等。
- 描述信息(message):对处理状态的简要描述,用于提示用户或开发者。
- 返回结果数据(data):实际的返回数据,可能是查询结果、操作结果等。
通过这个类,你可以方便地组织和传递请求处理的结果,使得前后端之间的通信更加清晰和规范。
public class AppResult<T> {
//自定义状态码
private long code;
//描述信息
private String message;
//结果数据
private T data;
public long getCode() {
return code;
}
/**
* 构造方法
*/
public AppResult(long code, String message) {
this.code = code;
this.message = message;
}
public AppResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功方法
*/
public static <T> AppResult<T> success(String message,T data){
return new AppResult<>(ResultCode.SUCCESS.code,message,data);
}
public static <T> AppResult<T> success(String message){
return new AppResult<>(ResultCode.SUCCESS.code,message,null);
}
public static <T> AppResult<T> success(T data){
return new AppResult<>(ResultCode.SUCCESS.code,ResultCode.SUCCESS.getMessage(),data);
}
public static <T> AppResult<T> success(){
return new AppResult<>(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMessage(),null);
}
/**
* 失败方法
* @return
*/
public static AppResult failed(){
return new AppResult(ResultCode.FAILED.getCode(),ResultCode.FAILED.getMessage());
}
public static AppResult failed(String message){
return new AppResult(ResultCode.FAILED.getCode(),message);
}
public static AppResult failed(ResultCode resultCode){
return new AppResult(resultCode.getCode(), resultCode.getMessage());
}
public void setCode(long code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
自定义异常
加入状态码与状态描述属性
/**
* 自定义异常
*/
public class ApplicationException extends RuntimeException{
//自定义的异常描述
private AppResult errorResult;
//指定状态码,异常描述
public ApplicationException(AppResult appResult) {
//构造异常中的Message属性
super(appResult.getMessage());
//自定义的错误描述
this.errorResult=appResult;
}
//自定义异常描述
public ApplicationException(String message) {
super(message);
//根据异常描述构建返回对象
this.errorResult=new AppResult(ResultCode.FAILED.getCode(),message);
}
//指定异常
public ApplicationException(Throwable cause){
super(cause);
}
//自定义异常描述,异常信息
public ApplicationException(String message,Throwable cause){
super(message,cause);
}
public AppResult getErrorResult() {
return errorResult;
}
public void setErrorResult(AppResult errorResult) {
this.errorResult = errorResult;
}
}
全局异常处理
实现统一异常处理
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理自定义的已知异常
* @param e ApplicationException
* @return AppResult
*/
// 以body形式返回
@ResponseBody
// 指定要处理的异常
@ExceptionHandler(ApplicationException.class)
public AppResult handleApplicationException (ApplicationException e) {
// 打印异常
e.printStackTrace(); // 在上生产之前一定要记得把这行代码注释掉
// 记录日志
log.error(e.getMessage());
// 获取异常信息
if (e.getErrorResult() != null) {
// 返回异常类中记录的状态
return e.getErrorResult();
}
// 默认返回异常信息
return AppResult.failed(e.getMessage());
}
/**
* 处理全未捕获的其他异常
* @param e Exception
* @return AppResult
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public AppResult handleException (Exception e) {
// 打印异常
e.printStackTrace(); // 在上生产之前一定要记得把这行代码注释掉
// 记录日志
log.error(e.getMessage());
if (e.getMessage() == null) {
return AppResult.failed(ResultCode.ERROR_SERVICES);
}
// 默认返回异常信息
return AppResult.failed(e.getMessage());
}
}
测试异常处理
@GetMapping("testException")
public AppResult testException() throws Exception {
throw new Exception("这是一个Exception...");
}
@GetMapping("applicationException")
public AppResult testApplicationException() throws Exception {
throw new ApplicationException("这是一个自定义的ApplicationException...");
}
访问以下链接:
http://127.0.0.1:58080/test/testException
http://127.0.0.1:58080/test/applicationException
登录拦截器
创建LoginInterceptor
@Component
public class LoginInterceptor implements HandlerInterceptor {
//从配置文件中获取默认登录页的URL
@Value("${forum_system.login.url}")
private String defaultURL;
/**
* 请求的前置处理
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取session并做已登录用户信息校验
HttpSession session= request.getSession(false);
if (session!=null && session.getAttribute(AppConfig.USER_SESSION_KEY)!=null){
//校验通过
return true;
}
//保证跳转页面的路正确性
if (!defaultURL.startsWith("/")){
defaultURL="/"+defaultURL;
}
//校验未通过,跳转到登录页面
response.sendRedirect(defaultURL);
//中止请求
return false;
}
}
application.yml配置文件
#项目自定义配置 forum_system: login: url: sign-in.html #未登录状态下强制跳转页面
创建AppInterceptorConfigurer
@Configuration
public class AppInterceptorConfigurer implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor) //添加用户登录拦截器
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/sign-in.html") //排除登录HTML
.excludePathPatterns("/sign-up.html") //排除注册HTML
.excludePathPatterns("/user/login") //排除登录api接口
.excludePathPatterns("/user/register") //排除注册api接口
.excludePathPatterns("/user/logout") //排除退出api接口
.excludePathPatterns("/swagger*/**") //排除登录swagger下所有
.excludePathPatterns("/v3*/**") //排除登录v3下所有,与swagger相关
.excludePathPatterns("/dist/**") //排除所有静态文件
.excludePathPatterns("/image/**")
.excludePathPatterns("/**.ico")
.addPathPatterns("/js/**");
}
}
实现API自动生成
使用Springfox Swagger生成API,并导入Postman,完成API单元测试
Swagger是⼀套API定义的规范,按照这套规范的要求去定义接⼝及接⼝相关信息, 再通过可以解析这套规范⼯具,就可以⽣成各种格式的接⼝⽂档,以及在线接⼝调试⻚⾯,通过⾃动 ⽂档的⽅式,解决了接⼝⽂档更新不及时的问题。
引入依赖
<!-- springfox - Swagger --> <springfox-boot-starter.version>3.0.0</springfox-boot-starter.version>
<!-- API文档生成,基于swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox-boot-starter.version}</version>
</dependency>
<!-- SpringBoot健康监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
编写配置类
解决SpringBoot 2.6.0以上与Springfox3.0.0 不兼容的问题,涉及SpringBoot 版本升级过程中的⼀
些内部实现变化,具体说明在修改配置文件部分
/**
* Swagger配置类
* @Author baekhyun
**/
// 配置类
@Configuration
// 开启Springfox-Swagger
@EnableOpenApi
public class SwaggerConfig {
/**
* Springfox-Swagger基本配置
* @return
*/
@Bean
public Docket createApi() {
Docket docket = new Docket(DocumentationType.OAS_30)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.forum_system.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
// 配置API基本信息
private ApiInfo apiInfo() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("论坛系统API")
.description("论坛系统前后端分离API测试")
.contact(new Contact("BAEKHYUN Tech", "https://edu.forumsystem.com", "1975688561@qq.com"))
.version("1.0")
.build();
return apiInfo;
}
/**
* 解决SpringBoot 6.0以上与Swagger 3.0.0 不兼容的问题
* 复制即可
**/
@Bean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties, Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment,
basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
shouldRegisterLinksMapping, null);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,
String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
}
application.xml添加配置
spring: mvc: pathmatch: matching-strategy: ant_path_matcher #Springfox-Swagger兼容性配置
API常用注解
@Api: 作用在Controller上,对控制器类的说明
@ApiModel: 作用在响应的类上,对返回响应数据的说明
@ApiModelProerty:作用在类的属性上,对属性的说明
@ApiOperation: 作用在具体方法上,对API接口的说明
@ApiParam: 作用在方法中的每⼀个参数上,对参数的属性进行说明
修改接口测试
@Api(tags = "测试接口")
@RestController
@RequestMapping("/test")
public class TestController {
@ApiOperation("测试打印")
@GetMapping("/hello")
public String hello(){
return "hello,baekhyun...";
}
@ApiOperation("测试异常")
@GetMapping("/testException")
public AppResult testException() throws Exception {
throw new Exception("这是一个Exception...");
}
@ApiOperation("测试自定义异常")
@GetMapping("/applicationException")
public AppResult testApplicationException() throws Exception {
throw new ApplicationException("这是一个自定义的ApplicationException...");
}
@ApiOperation("测试传参")
@GetMapping("/helloByName")
public String testHelloByName(@ApiParam("名字") String name){
return "hello,"+name;
}
}
访问API列表
通过访问 http://127.0.0.1:58080/swagger-ui/index.html,可以正常显示接口信息,针对每个接口进行测试
在此处还可以使用postman进行测试,首先获取API地址
将API导入到postman中即可
创建工具类
创建MD5加密工具类
用户传入一个密码明文
服务器生成一个扰动字符串(盐)
最终密文=MD5(MD5(密码明文)+盐)
导入依赖
<!-- 编码解码加密工具包-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
public class MD5Utils {
/**
* 返回一个用MD5加密后的字符串
* @param str
* @return
*/
public static String md5(String str){
return DigestUtils.md5Hex(str);
}
/**
* 明文加盐生成最终的密文
* @param str
* @param salt
* @return
*/
public static String md5Salt(String str,String salt){
//先对铭文进行MD5加密
String s=DigestUtils.md5Hex(str);
//加密后的原文与盐拼接在一起之后在进行一次MD5加密
String ciphertext=DigestUtils.md5Hex(s+salt);
//返回密文
return ciphertext;
}
}
创建生成UUID工具类
服务器生成一个扰动字符串(盐)
/**
* ⽣成UUID⼯具类
*/
public class UUIDUtils {
/**
* ⽣成32位UUID
* @return
*/
public static String UUID_32(){
return UUID.randomUUID().toString().replace("-","");
}
/**
* 生成36位UUID
* @return
*/
public static String UUID_36(){
return UUID.randomUUID().toString();
}
}
创建字符串工具类
校验字符串是否为空
/**
* 字符串相关的工具类
*/
public class StringUtils {
/**
* 校验字符串是否为空
* @param value
* @return
*/
public static boolean isEmpty(String value){
if (value==null || value.isEmpty()){
return true;
}
return false;
}
}
4.3、实现业务功能
4.3.1、注册(后端实现)
请求
// 请求
POST /user/register HTTP/1.1Content-Type: application/x-www-form-urlencoded
username=xxx&nickname=xxx&password=xxx&passwordRepeat=xxx
响应
// 响应
HTTP/1.1 200
Content-Type: application/json
{"code":0,"message":"成功","data":null}
创建扩展Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.forum_system.dao.UserMapper">
<!-- 根据用户名查询用户信息 -->
<select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user
where deleteState=0
and username=#{username,jdbcType=VARCHAR}
</select>
</mapper>
修改DAO
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
User selectByUsername(@Param("username") String username);
创建Service接口
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
User selectByUsername(String username);
/**
* 创建普通用户
* @param user
*/
void createNormalUser(User user);
实现Service接口
@Slf4j
@Service
public class UserServiceImpl implements IUserService {
@Resource
private UserMapper userMapper;
@Override
public User selectByUsername(String username) {
//非空校验
if (StringUtils.isEmpty(username)){
//打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
//调用DAO
User user=userMapper.selectByUsername(username);
//返回结果
return user;
}
@Override
public void createNormalUser(User user) {
//1、非空校验
if (user==null || StringUtils.isEmpty(user.getUsername())
|| StringUtils.isEmpty(user.getNickname()) || StringUtils.isEmpty(user.getPassword())
|| StringUtils.isEmpty(user.getSalt())){
//打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
//2、校验用户是否存在
User existUser=selectByUsername(user.getUsername());
if (existUser!=null){
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));
}
//3、设置默认值
if (user.getGender()==null || user.getGender()<0 || user.getGender()>2){
//性别保密
user.setGender((byte) 2);
}
user.setArticleCount(0); //发布的文章数量
user.setIsAdmin((byte) 0); //是否管理员
user.setState((byte) 0); //状态
user.setDeleteState((byte) 0); //是否删除
//时间
Date date=new Date();
user.setCreateTime(date); //创建时间
user.setUpdateTime(date); //更新时间
//4、写入用户数据,返回结果
int row=userMapper.insertSelective(user);
//判断受影响的行数
if (row!=1){
log.warn("用户注册时,"+ResultCode.FAILED_CREATE.toString());
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
}
}
}
对Service接口进行单元测试
@SpringBootTest
class UserServiceImplTest {
@Resource
private IUserService userService;
@Resource
private ObjectMapper objectMapper;
@Test
void selectByUsername() throws JsonProcessingException {
User user=userService.selectByUsername("baekhyun");
System.out.println(objectMapper.writeValueAsString(user));
}
@Test
void createNormalUser() {
//创建一个用户对象
User user=new User();
user.setUsername("baekhyun");
user.setNickname("baekhyun");
//处理密码
String password="1992"; //明文密码
String salt= UUIDUtils.UUID_32(); //盐
String ciphertext=MD5Utils.md5Salt(password,salt);
//设置密码
user.setPassword(ciphertext);
//设置盐
user.setSalt(salt);
userService.createNormalUser(user);
System.out.println("写入用户成功 "+user.getId());
}
}
实现Controller层
@Slf4j
@Api(tags = "用户接口")
@RequestMapping("/user")
@RestController
public class UserController {
@Resource
private IUserService userService;
@ApiOperation("用户注册")
@PostMapping("/register")
public AppResult register(@ApiParam("用户名") @RequestParam("username") @NonNull String username,
@ApiParam("昵称") @RequestParam("nickname") @NonNull String nickname,
@ApiParam("密码") @RequestParam("password") @NonNull String password,
@ApiParam("确认密码") @RequestParam("passwordRepeat") @NonNull String passwordRepeat){
//1、判断密码与确认密码是否相同
if (!password.equals(passwordRepeat)){
//返回错误信息
return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
}
//2、判断用户是否存在
User existUser=userService.selectByUsername(username);
if (existUser!=null){
//用户已存在
return AppResult.failed(ResultCode.FAILED_USER_EXISTS);
}
//3、生成密码的密文
//生成盐
String salt= UUIDUtils.UUID_32();
//生成密文
String ciphertext= MD5Utils.md5Salt(password,salt);
//构造user对象
User user=new User();
user.setUsername(username); //用户名
user.setNickname(nickname); //昵称
user.setPassword(ciphertext); //密码(密文)
user.setSalt(salt); //盐
//调用service
userService.createNormalUser(user);
//返回正常响应
return AppResult.success();
}
}
测试接口
在controller层加入注解之后,我们不用再手动校验参数是否为空,只做必要的业务校验就行,而且生成的API也有了相应的文字和是否必传的标注。
注册(前端实现)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="shortcut icon" href="/favicon.ico">
<title>BAEKHYUN论坛 - 用户注册</title>
<!-- 导入CSS -->
<link href="./dist/css/tabler.min.css?1674944402" rel="stylesheet" />
<link rel="stylesheet" href="./dist/css/jquery.toast.css">
<!-- 设置字体 -->
<!-- <style>
@import url('https://rsms.me/inter/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style> -->
</head>
<body class="d-flex flex-column">
<!-- 正文 -->
<div class="page page-center">
<div class="container container-tight py-4">
<div class="text-center mb-4">
<img src="./image/bit-forum-logo01.png" height="50" alt="">
</div>
<form id="signUpForm" class="card card-md" autocomplete="off" novalidate>
<div class="card-body">
<h2 class="text-center mb-4">用户注册</h2>
<!-- 用户名 -->
<div class="mb-3">
<label class="form-label required">用户名</label>
<input type="text" class="form-control " placeholder="请输入用户名" name="username" id="username">
<div class="invalid-feedback">用户名不能为空</div>
</div>
<!-- 昵称 -->
<div class="mb-3">
<label class="form-label required">昵称</label>
<input type="text" class="form-control" placeholder="请输入昵称" name="nickname" id="nickname">
<div class="invalid-feedback">昵称不能为空</div>
</div>
<!-- 密码 -->
<div class="mb-3">
<label class="form-label required">密码</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" placeholder="请输入密码" autocomplete="off" name="password"
id="password">
<span class="input-group-text">
<a href="javascript:void(0);" class="link-secondary" id="password_a" title="显示密码"
data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path
d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
</svg>
</a>
</span>
<div class="invalid-feedback">密码不能为空</div>
</div>
</div>
<!-- 确认密码 -->
<div class="mb-3">
<label class="form-label required">确认密码</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" placeholder="再次输入密码" autocomplete="off" name="passwordRepeat"
id="passwordRepeat">
<span class="input-group-text">
<a href="javascript:void(0);" class="link-secondary" id="passwordRepeat_a" title="显示密码"
data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path
d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
</svg>
</a>
</span>
<div class="invalid-feedback">请检查确认密码</div>
</div>
</div>
<div class="mb-3">
<label class="form-check">
<input type="checkbox" class="form-check-input" id="policy" />
<span class="form-check-label">同意 <a href="#" tabindex="-1">BAEKHYUN论坛使用条款和隐私政策</a>.</span>
</label>
</div>
<div class="form-footer">
<button type="button" class="btn btn-primary w-100" id="submit">注册</button>
</div>
</div>
</form>
<div class="text-center text-muted mt-3">
我已有一个账户? <a href="./sign-in.html" tabindex="-1">登录</a>
</div>
</div>
</div>
</body>
<!-- 导入JS -->
<script src="./dist/js/tabler.min.js"></script>
<script src="./dist/js/jquery-3.6.3.min.js"></script>
<script src="./dist/js/jquery.toast.js"></script>
<script>
$(function () {
// 获取表单并校验
$('#submit').click(function () {
let checkForm = true;
// 校验用户名
if (!$('#username').val()) {
$('#username').addClass('is-invalid');
checkForm = false;
}
// 校验昵称
if (!$('#nickname').val()) {
$('#nickname').addClass('is-invalid');
checkForm = false;
}
// 校验密码非空
if (!$('#password').val()) {
$('#password').addClass('is-invalid');
checkForm = false;
}
// 校验确认密码非空, 校验密码与重复密码是否相同
if (!$('#passwordRepeat').val() || $('#password').val() != $('#passwordRepeat').val()) {
$('#passwordRepeat').addClass('is-invalid');
checkForm = false;
}
// 检验政策是否勾选
if (!$('#policy').prop('checked')) {
$('#policy').addClass('is-invalid');
checkForm = false;
}
// 根据判断结果提交表单
if (!checkForm) {
return false;
}
// 所有校验通过之后构造要发送的数据
let postData={
username:$('#username').val(),
nickname:$('#nickname').val(),
password:$('#password').val(),
passwordRepeat:$('#passwordRepeat').val()
};
// 发送AJAX请求
// contentType = application/x-www-form-urlencoded
// 成功后跳转到 sign-in.html
$.ajax ({
type:'POST',
url:'user/register',
contentType:'application/x-www-form-urlencoded',
data:postData,
//回调
success:function(respData){
//根据code的值判断响应是否成功
if(respData.code==0){
//成功
location.assign('sign-in.html');
}else{
//失败
$.toast({
heading:'失败',
text:respData.message,
icon:'warning'
})
}
},
error:function(){
$.toast({
heading:'错误',
text:'访问网站出现问题,请与管理员联系',
icon:'error'
})
}
});
});
// 表单元单独检验
$('#username, #nickname, #password').on('blur', function () {
if ($(this).val()) {
$(this).removeClass('is-invalid');
$(this).addClass('is-valid');
} else {
$(this).removeClass('is-valid');
$(this).addClass('is-invalid');
}
})
// 检验确认密码
$('#passwordRepeat').on('blur', function () {
if ($(this).val() && $(this).val() == $('#password').val()) {
$(this).removeClass('is-invalid');
$(this).addClass('is-valid');
} else {
$(this).removeClass('is-valid');
$(this).addClass('is-invalid');
}
})
// 校验政策是否勾选
$('#policy').on('change', function () {
if ($(this).prop('checked')) {
$(this).removeClass('is-invalid');
$(this).addClass('is-valid');
} else {
$(this).removeClass('is-valid');
$(this).addClass('is-invalid');
}
})
// 密码框右侧明文密文切换按钮
$('#passwordRepeat_a').click(function () {
if($('#passwordRepeat').attr('type') == 'password') {
$('#passwordRepeat').attr('type', 'text');
} else {
$('#passwordRepeat').attr('type', 'password');
}
});
$('#password_a').click(function () {
if($('#password').attr('type') == 'password') {
$('#password').attr('type', 'text');
} else {
$('#password').attr('type', 'password');
}
});
});
</script>
</html>
4.3.2、登录(后端实现)
请求
// 请求POST /user/login HTTP/ 1.1Content-Type: application/x-www-form-urlencodedusername=xxx&password= xxx
响应
// 响应HTTP/ 1.1 200Content-Type: application/json{ "code" : 0 , "message" : " 成功 " , "data" : null }
创建Service接口
在IUserService新增方法
/**
* 用户登录
* @param username
* @param password
* @return
*/
User login(String username,String password);
实现Service接口
@Override
public User login(String username, String password) {
//1、非空校验
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
//打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
//2、查询用户是否存在
User user=selectByUsername(username);
//校验用户
if (user==null){
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN.getMessage()));
}
//校验密码
//获取用户的盐
String salt=user.getSalt();
//生成密文
String ciphertext= MD5Utils.md5Salt(password,salt);
//比较密文是否一致
if (!ciphertext.toLowerCase().equals(user.getPassword().toLowerCase())){
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN.getMessage()));
}
//校验通过,返回user对象
return user;
}
对Service接口进行单元测试
@Test
void login() throws JsonProcessingException {
User user=userService.login("baekhyun","1992");
System.out.println(objectMapper.writeValueAsString(user));
}
实现controller层
@ApiOperation("用户登录")
@PostMapping("/login")
public AppResult<User> login(HttpServletRequest request,
@ApiParam("用户名") @RequestParam("username") @NonNull String username,
@ApiParam("密码") @RequestParam("password") @NonNull String password){
//调用Service
User user=userService.login(username,password);
if (user==null){
//返回错误
return AppResult.failed(ResultCode.FAILED_LOGIN);
}
//获取session对象
HttpSession session= request.getSession(true); //没有的话创建一个
//把用户信息设置到session中
session.setAttribute(AppConfig.USER_SESSION_KEY,user);
//返回结果
return AppResult.success();
}
测试接口
登录(前端实现)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<link rel="shortcut icon" href="/favicon.ico">
<!-- 标题 -->
<title>BAEKHYUN论坛 - 用户登录</title>
<!-- 导入CSS -->
<link href="./dist/css/tabler.min.css?1674944402" rel="stylesheet"/>
<link rel="stylesheet" href="./dist/css/jquery.toast.css">
<!-- 设置字体 -->
<!-- <style>
@import url('https://rsms.me/inter/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style> -->
</head>
<body class="d-flex flex-column">
<!-- 正文 -->
<div class="page page-center">
<div class="container container-normal py-4">
<div class="row align-items-center g-4">
<div class="col-lg">
<div class="container-tight">
<div class="text-center mb-4">
<img src="./image/bit-forum-logo01.png" height="50" alt="">
</div>
<div class="card card-md">
<div class="card-body">
<h2 class="text-center mb-4">用户登录</h2>
<form id="signInForm" method="get" autocomplete="off" novalidate>
<div class="mb-3">
<label class="form-label required">用户名</label>
<input type="text" class="form-control" placeholder="请输入用户名" autocomplete="off" name="username" id="username">
<div class="invalid-feedback">用户名不能为空</div>
</div>
<div class="mb-2">
<label class="form-label required">
密码
<!-- <span class="form-label-description">
<a href="#">忘记密码</a>
</span> -->
</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" placeholder="请输入密码" autocomplete="off" name="password"
id="password">
<span class="input-group-text">
<a href="javascript:void(0);" id="password_a" class="link-secondary" title="显示密码" data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" /></svg>
</a>
</span>
<div class="invalid-feedback">密码不能为空</div>
</div>
</div>
<!-- <div class="mb-2">
<label class="form-check">
<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、退出
- 用户访问退出接口。
- 服务器注销用户的会话(Session)。
- 返回成功或失败的消息。
- 如果返回成功,浏览器将跳转到相应的页面。
- 结束操作。
请求
// 请求GET http: //127.0.0.1:58080/user/logout HTTP/1.1
响应
// 响应HTTP/ 1.1 200Content-Type: application/json{ "code" : 0 , "message" : " 成功 " , "data" : null }
实现Controller层
@ApiOperation("用户注销")
@GetMapping("/logout")
public AppResult logout(HttpServletRequest request){
//1、获取session
HttpSession session=request.getSession(false);
//2、注销session
if (session!=null){
//销毁session
session.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参数来确定返回哪个用户的详细信息:
- 如果没有传入用户Id,服务器会返回当前登录用户的详细信息。
- 如果传入了用户Id,服务器会返回指定Id的用户详细信息。
请求
// 请求GET /user/info HTTP/ 1.1GET /user/info?id= 1 HTTP/ 1.1
响应
// 响应HTTP/ 1.1 200Content-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接口
@Override
public 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、调用DAO
User user=userMapper.selectByPrimaryKey(id);
//3、返回结果
return user;
}
对Service接口进行单元测试
@Test
void 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'
})
}
});
// 构造查询用户信息的queryString
let 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.1Content-Type: application/x-www-form-urlencoded
响应
// 响应HTTP/ 1.1 200Content-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接口
@Override
public 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();
//设置Id
updateUser.setId(user.getId());
//更新时间
updateUser.setUpdateTime(new Date());
//处理username
if (!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));
}
//调用DAO
int row=userMapper.updateByPrimaryKeySelective(updateUser);
//判断受影响的行数
if (row!=1){
log.warn("用户更新时,"+ResultCode.ERROR_SERVICES.toString());
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
}
对Service接口进行单元测试
@Test
void 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");
//调用Service
userService.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()); //id
updateUser.setUsername(username); //用户名
updateUser.setNickname(nickname); //昵称
updateUser.setGender(gender); //性别
updateUser.setPhoneNum(phoneNum); //电话号
updateUser.setEmail(email); //邮箱
updateUser.setRemark(remark); //个人简介
//调用Service
userService.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 string
let 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.1Content-Type: application/x-www-form-urlencodedid= 1 &oldPassword= 123456 &newPassword= 123456 &passwordRepeat= 123456
响应
// 响应HTTP/ 1.1 200Content-Type: application/json{"code" : 0 ,"message" : " 成功 " ,"data" : null}
创建Service接口
/**
* 修改用户密码
* @param id
* @param newPassword
* @param oldPassword
*/
void modifyPassword(Long id,String newPassword,String oldPassword);
实现Service接口
@Override
public 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()); //用户Id
updateUser.setSalt(salt); //盐
updateUser.setPassword(encryptPassword); //新密码的密文
updateUser.setUpdateTime(new Date()); //更新时间
//8、调用DAO
int row=userMapper.updateByPrimaryKeySelective(updateUser);
//判断受影响的行数
if (row!=1){
log.warn("用户更新密码时,"+ResultCode.ERROR_SERVICES.toString());
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
}
对Service接口进行单元测试
@Test
void 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、调用Service
userService.modifyPassword(user.getId(), newPassword,oldPassword);
//4、销毁session
if (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" />
<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-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" />
<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-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" />
<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="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 200Content-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_board
where state=0
and deleteState=0
order by sort asc
limit 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 {
@Resource
private BoardMapper boardMapper;
@Override
public 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));
}
//调用DAO
List<Board> boards=boardMapper.selectByNum(num);
//返回结果
return boards;
}
}
对Service接口进行单元测试
@SpringBootTest
class BoardServiceImplTest {
@Resource
private IBoardService boardService;
@Resource
private ObjectMapper objectMapper;
@Test
void 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;
@Resource
private IBoardService boardService;
@ApiOperation("获取首页中的版块列表")
@GetMapping("/topList")
public AppResult<List<Board>> topList(){
//直接调用Service
List<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接口
@Override
public 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));
}
//调用DAO
Board board=boardMapper.selectByPrimaryKey(id);
//返回结果
return board;
}
对Service接口进行单元测试
@Test
void 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){
//调用Service
Board 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 200Content-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">
select
u.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
from t_article a, t_board b, t_user u
where a.boardId = b.id
and a.userId = u.id
and a.deleteState = 0
order by a.createTime desc
</select>
<!-- 根据版块Id查询帖子列表 -->
<select id="selectByBoardId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">
select
u.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.updateTime
from t_article a, t_user u
where a.userId = u.id
and a.deleteState = 0
and 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 {
@Resource
private ArticleMapper articleMapper;
@Override
public List<Article> selectAll() {
//直接调用DAO
List<Article> articles=articleMapper.selectAll();
//返回结果
return articles;
}
@Override
public 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));
}
//调用DAO
List<Article> articles=articleMapper.selectByBoardId(boardId);
//返回结果
return articles;
}
}
对Service接口进行单元测试
@SpringBootTest
class ArticleServiceImplTest {
@Resource
private IArticleService articleService;
@Resource
private ObjectMapper objectMapper;
@Test
void selectAll() throws JsonProcessingException {
List<Article> articles=articleService.selectAll();
System.out.println(objectMapper.writeValueAsString(articles));
}
@Test
void 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 {
@Resource
private 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);
}
//防止返回的结果是null
if (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;
}
// 构造HTML
let 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.1GET 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">
select
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
from
t_board as b,
t_article as a
where
a.userId=#{userId,jdbcType=BIGINT} and
b.id=a.boardId and
a.deleteState=0
order 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接口
@Override
public 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接口进行单元测试
@Test
void 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);
}
测试接口
前端代码
// 构造查询用户信息的queryString
let 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 到 Textarea
searchReplace: 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.1Content-Type: application/x-www-form-urlencodedboardId= 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 200Content-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
*/
//事务管理
@Transactional
void create(Article article);
实现Service接口
UserServiceImpl实现方法
@Override
public 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());
//调用DAO
int row=userMapper.updateByPrimaryKeySelective(updateUser);
if (row!=1){
//打印日志
log.warn(ResultCode.ERROR_SERVICES.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
}
BoardServiceImpl实现方法
@Override
public 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());
//调用DAO
int row=boardMapper.updateByPrimaryKeySelective(updateBoard);
if (row!=1){
//打印日志
log.warn(ResultCode.ERROR_SERVICES.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
}
ArticleServiceImpl实现方法
@Override
public 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接口进行单元测试
@Test
void addOneArticleCountById() {
userService.addOneArticleCountById(2L);
System.out.println("更新成功");
}
@Test
void addOneArticleCountById() {
boardService.addOneArticleCountById(9L);
System.out.println("更新成功");
}
@Test
void 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()); //作者Id
article.setBoardId(boardId); //版块Id
article.setTitle(title); //标题
article.setContent(content); //内容
//调用Service
articleService.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">
select
u.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.content
from t_article a, t_board b, t_user u
where a.boardId = b.id
and a.userId = u.id
and a.deleteState = 0
and 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接口
@Override
public 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));
}
//帖子询问数量加1
article.setVisitCount(article.getVisitCount()+1);
//创建用于更新的对象
Article updateArticle=new Article();
updateArticle.setId(article.getId()); //设置Id
updateArticle.setVisitCount(article.getVisitCount()); //访问数
updateArticle.setUpdateTime(new Date()); //更新时间
//调用更新方法
articleMapper.updateByPrimaryKeySelective(updateArticle);
//返回成功
return article;
}
对Service接口进行单元测试
@Test
void 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);
//调用Service
Article article=articleService.selectDetailById(id);
//判断当前登录用户是不是帖子的作者
if (user.getId()== article.getUserId()){
//设置所有者标识为true
article.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、编辑帖子
用户只能编辑自己发的帖子,点击编辑接钮,提交编辑后的帖子时需要进行校验
//当作者是自己的时候,设置为true
private Boolean own;
当帖子的发帖人是当前用户时,显示编辑按钮。用户点击编辑按钮后,进入编辑页面;在编辑页面,获取帖子的信息,并在对应的位置显示标题和内容;用户可以修改帖子的标题和内容;用户提交修改后,将修改内容发送到服务器。服务器验证当前用户是否是帖子的发帖人,并更新数据库中的信息;返回更新结果给用户。
请求
// 请求POST http: //127.0.0.1:58080/article/modify HTTP/1.1Content-Type: application/x-www-form-urlencodedid= 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 200Content-Type: application/json{"code" : 0 ,"message" : " 成功 " ,"data" : null}
创建Service接口
/**
* 更新
* @param id
* @param title
* @param content
*/
void modify(Long id,String title,String content);
实现Service接口
@Override
public 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); //id
updateArticle.setTitle(title); //标题
updateArticle.setContent(content); //正文
updateArticle.setUpdateTime(new Date()); //更新时间
//调用DAO
int 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接口进行单元测试
@Test
void 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);
}
//调用Service
articleService.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 到 Textarea
searchReplace: 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 () {
// ID
let articleIdEl = $('#edit_article_id');
if (!articleIdEl.val()) {
// 提示
$.toast({
heading: '提示',
text: '数据不正确,请刷新后重试',
icon: 'warning'
});
return;
}
//title
let articleTitleEl = $('#edit_article_title');
if (!articleTitleEl.val()) {
// 提示
$.toast({
heading: '提示',
text: '请输入帖子标题',
icon: 'warning'
});
return;
}
// content
let 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 200Content-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
*/
@Transactional
void deleteById(Long id);
实现Service接口
UserServiceImpl实现方法
@Override
public 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之后,用户的发帖数是否小于0
if (updateUser.getArticleCount()<0){
//如果小于0,则设置为0
updateUser.setArticleCount(0);
}
//调用DAO
int row=userMapper.updateByPrimaryKeySelective(updateUser);
if (row!=1){
//打印日志
log.warn(ResultCode.ERROR_SERVICES.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
BoardServiceImpl实现方法
@Override
public 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之后是否小于0
if (updateBoard.getArticleCount()<0){
//如果小于0那么设置为0
updateBoard.setArticleCount(0);
}
//调用DAO
int row=boardMapper.updateByPrimaryKeySelective(updateBoard);
if (row!=1){
//打印日志
log.warn(ResultCode.ERROR_SERVICES.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
}
ArticleServiceImpl实现方法
@Override
public 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);
//调用DAO
int 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接口进行单元测试
@Test
void 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.1Content-Type: application/x-www-form-urlencodedid= 1
响应
// 响应HTTP/ 1.1 200Content-Type: application/json{"code" : 0 ,"message" : " 成功 " ,"data" : null}
创建Service接口
/**
* 点赞
* @param id
*/
void thumbsUpById(Long id);
实现Service接口
@Override
public 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);
//调用DAO
int 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接口进行单元测试
@Test
void 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.1Content-Type: application/x-www-form-urlencodedarticleId= 1 &content=%E5% 9 B% 9 E%E5%A4% 8 D%E6%B2%A1%E8%AF% 95
响应
// 响应HTTP/ 1.1 200Content-Type: application/json{"code" : 0 ,"message" : " 成功 " ,"data" : null}
创建Service接口
public interface IArticleReplyService {
/**
* 新增回复
* @param articleReply
*/
@Transactional
void create(ArticleReply articleReply);
}
实现Service接口
@Service
@Slf4j
public class ArticleReplyServiceImpl implements IArticleReplyService {
@Resource
private ArticleReplyMapper articleReplyMapper;
@Resource
private IArticleService articleService;
@Override
public 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); //更新时间
//调用DAO
int 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()); //帖子Id
updateArticle.setReplyCount(article.getReplyCount()+1); //回复数
updateArticle.setUpdateTime(new Date()); //更新时间
//调用帖子的更新方法
articleService.updateById(updateArticle);
//打印日志
log.info("回复成功,article id="+article.getId());
}
}
对Service接口进行单元测试
@SpringBootTest
class ArticleReplyServiceImplTest {
@Resource
private IArticleReplyService articleReplyService;
@Resource
private ObjectMapper objectMapper;
@Test
void 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 {
@Resource
private IArticleReplyService articleReplyService;
@Resource
private 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); //主帖Id
articleReply.setContent(content); //回复内容
//5、调用Service
articleReplyService.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 200Content-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">
select
u.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.updateTime
from t_article_reply ar, t_user u
where ar.postUserId = u.id
and ar.deleteState = 0
and 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接口
@Override
public 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));
}
//调用DAO
List<ArticleReply> articleReplies=articleReplyMapper.selectByArticleId(articleId);
//返回结果集
return articleReplies;
}
对Service接口进行单元测试
@Test
void 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);
//校验结果是否为空,为空的话则创建一个空集合并返回([]),防止返回一个字符串形式的null
if (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 () {
// 设置要查看用户的Id
profileUserId = 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.1Content-Type: application/x-www-form-urlencodedreceiveUserId= 2 &content=%E4%BD%A0%E5%A5%BD%E5% 95 % 8 A
响应
// 响应HTTP/ 1.1 200Content-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 {
@Resource
private MessageMapper messageMapper;
@Resource
private IUserService userService;
@Override
public 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);
//调用DAO
int 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 {
@Resource
private IMessageService messageService;
@Resource
private ObjectMapper objectMapper;
@Test
void 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 {
@Resource
private IMessageService messageService;
@Resource
IUserService 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);
//调用Service
messageService.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 200Content-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_message
WHERE state = 0
and deleteState = 0
and receiveUserId = #{receiveUserId,jdbcType=BIGINT}
</select>
</mapper>
修改DAO
/**
* 查询当前登录用户的未读站内信数量
* @param userId
* @return
*/
Integer selectUnreadCount(Long userId);
创建Service接口
/**
* 查询当前登录用户的未读站内信数量
* @param userId
* @return
*/
Integer selectUnreadCount(Long userId);
实现Service接口
@Override
public 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));
}
//调用DAO
Integer result= messageMapper.selectUnreadCount(userId);
//返回结果
return result;
}
对Service接口进行单元测试
@Test
void 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_badge
function 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">
select
u.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.updateTime
FROM
t_message AS m,
t_user AS u
WHERE
m.postUserId = u.id AND
m.deleteState = 0 AND
m.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接口
@Override
public 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));
}
//调用DAO
List<Message> messages=messageMapper.selectByReceiveUserId(receiveUserId);
//返回结果
return messages;
}
对Service接口进行单元测试
@Test
void 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> '
+ ' <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-animated
let 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.1Content-Type: application/x-www-form-urlencoAdedid= 1
响应
// 响应HTTP/ 1.1 200Content-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接口
@Override
public 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));
}
//调用DAO
Message message=messageMapper.selectByPrimaryKey(id);
//返回结果
return message;
}
@Override
public 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); //用户Id
updateMessage.setState(state); //站内信状态
Date date=new Date();
updateMessage.setUpdateTime(date); //更新时间
//调用DAO
int row=messageMapper.updateByPrimaryKeySelective(updateMessage);
if (row!=1){
//打印日志
log.warn(ResultCode.ERROR_SERVICES.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
}
对Service接口进行单元测试
@Test
void selectById() throws JsonProcessingException {
Message message=messageService.selectById(1L);
System.out.println(objectMapper.writeValueAsString(message));
System.out.println("读取站内信成功");
}
@Test
void 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> '
+ ' <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-animated
let 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.state
statusDotEl.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.1Content-Type: application/x-www-form-urlencodedrepliedId= 1 &receiveUserId= 2 &content=%E4%BD%A0%E5%A5%BD%E5% 95 % 8 A
响应
// 响应HTTP/ 1.1 200Content-Type: application/json{"code" : 0 ,"message" : " 成功 " ,"data" : null}
创建Service接口
/**
* 回复站内信
* @param repliedId 被回复的站内信Id
* @param message
*/
@Transactional
void reply(Long repliedId,Message message);
实现Service接口
@Override
public 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接口进行单元测试
@Test
void 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); //内容
//调用Service
messageService.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:ss level: root: info #默认日志级别 com.example.forum_system: debug #指定包的日志级别 file: path: /log/forum #日志保存的路径
4、打包程序
5、上传到服务器
6、验证访问
如需完整代码,可私信博主!!!