访问地址:http://8.130.142.126:18080/sign-in.html
代码获取:基于 Spring 前后端分离版本的论坛系统: 基于 Spring 前后端分离版本的论坛系统
一.前置知识
1.软件生命周期
- a. 可行性研究:通过分析软件开发要求,确定软件项目的性质、目标和规模,得出可行性研究报告,如果可行性研究报告是可行的,就要制订详细的项目开发计划。此阶段交付成果为可行性研究报告;
- b. 需求分析:是软件生命周期中重要的也是决定性的一步,通过需求分析才能把软件功能和性能的总体概念描述为具体的软件需求规格说明书,为软件开发奠定基础,此阶段交付成果为软件需求规格说明书;
- c. 概要设计:根据软件需求规格说明建立软件系统的总体结构和模块间的关系,定义各功能模块接口,设计全局数据库或数据结构,规定设计约束,制定组装(集成)测试计划;
- d. 详细设计:将各模块要实现的功能用相应的设计工具详细的描述出来。
- e. 实现:写出正确的、易理解的和易维护的程序模块。程序员根据详细设计文档将详细设计转化为程序,完成单元测试;
- f. 组装测试(集成测试):将经过单元测试的模块逐步进行组装和测试;
- g. 确认测试:测试系统是否达到了系统要求,按照规格说明书的规定,由用户(或在用户积极参与下)对系统进行验收。必要时,还可以再通过现场测试或并行运行等⽅法对系统进行进一步测试;
- h. 使用:将软件安装在用户确定的运行环境中,测试通过后移交用户使用。在软件使用过程中,客户和维护人员必须认真收集发现的软件错误,定期或阶段性的撰写软件问题报告和软件修改报告;
- i. 维护:通过各种必要的维护活动使系统持久的满足用户需要;
- j. 退役:终止对软件产品的支持,软件停止使用
2.面向对象
3.架构
C/S架构:即客户端 / 服务器架构模式
B/S架构:即浏览器 / 服务器架构模式
4.相关技术和工具
服务器端技术
- Spring
- Spring Boot
- Spring MVC
- MyBatis
浏览器端技术
- HTML, CSS, JavaScript
- jQuery
- Bootstrap
数据库
- MySQL
项目构建工具
- Maven
版本控制工具
- Git + GITEE
二.数据库的设计
1.创建数据库
-- ----------------------------
-- 创建数据库,并指定字符集
-- ----------------------------
drop database if exists forum_db;
create database forum_db character set utf8mb4 collate utf8mb4_general_ci;
2.创建表
-- 选择数据库
use forum_db;set names utf8mb4;
set foreign_key_checks = 0;
1.用户表
-- ----------------------------
-- 创建用户表 for t_user
-- ----------------------------
drop table if exists `t_user`;
create table `t_user` (`id` bigint(20) not null auto_increment comment '用户编号,主键,自增',`username` varchar(20) character set utf8mb4 collate utf8mb4_general_ci not null comment '用户名,非空,唯一',`password` varchar(32) character set utf8mb4 collate utf8mb4_general_ci not null comment '加密后的密码',`nickname` varchar(50) character set utf8mb4 collate utf8mb4_general_ci not null comment '昵称,非空',`phonenum` varchar(20) character set utf8mb4 collate utf8mb4_general_ci null default null comment '手机号',`email` varchar(50) character set utf8mb4 collate utf8mb4_general_ci null default null comment '邮箱地址',`gender` tinyint(4) not null default 2 comment '0女 1男 2保密,非空,默认2',`salt` varchar(32) character set utf8mb4 collate utf8mb4_general_ci not null comment '为密码加盐,非空',`avatarurl` varchar(255) character set utf8mb4 collate utf8mb4_general_ci null default null comment '用户头像url,默认系统图片',`articlecount` int(11) not null default 0 comment '发帖数量,非空,默认0',`isadmin` tinyint(4) not null default 0 comment '是否管理员,0否 1是,默认0',`remark` varchar(1000) character set utf8mb4 collate utf8mb4_general_ci null default null comment '备注,自我介绍',`state` tinyint(4) not null default 0 comment '状态 0 正常,1 禁言,默认0',`deletestate` tinyint(4) not null default 0 comment '是否删除 0否 1是,默认0',`createtime` datetime not null comment '创建时间,精确到秒',`updatetime` datetime not null comment '更新时间,精确到秒',primary key (`id`) using btree,unique index `user_username_uindex`(`username`) using btree
) engine = innodb auto_increment = 2 character set = utf8mb4 collate = utf8mb4_general_ci comment = '用户表' row_format = dynamic;set foreign_key_checks = 1;
2.文章表
-- ----------------------------
-- 创建帖子表 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;
3.文章回复表
-- ----------------------------
-- 创建帖子回复表 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;
4.版块表
-- ----------------------------
-- 创建版块表 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;
5.站内信表
-- ----------------------------
-- 创建站内信表 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;
三.环境的搭建
1.创建Spring项目
项目创建完成如下:
2.开启热部署
<!-- 热部署--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency>
3.pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.14</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.javastudy</groupId><artifactId>forum</artifactId><version>0.0.1-SNAPSHOT</version><name>forum</name><description>forum</description><properties><java.version>1.8</java.version><mybatis-starter.version>2.3.0</mybatis-starter.version><druid-starter.version>1.2.16</druid-starter.version><mysql-connector.version>5.1.49</mysql-connector.version></properties><dependencies><!-- spring web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis-starter.version}</version></dependency><!-- 热部署--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><!-- lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- spring boot test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- mysql连接--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql-connector.version}</version><scope>runtime</scope></dependency><!-- 阿里巴巴druid数据源,如果使用SpringBoot默认的数据源,删除或注释这个依赖即可 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid-starter.version}</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
4.application.yml文件
#配置数据源
spring:application:name: "forum" # 项目名output:ansi:enabled: always # 控制台输出彩色日志datasource:url: jdbc:mysql://127.0.0.1:13306/forum_db?characterEncoding=utf8&useSSL=false # 数据库连接串username: root # 数据库用户名字\password: woaini520 # 数据库密码driver-class-name: com.mysql.jdbc.Driver # 数据库连接驱动
# 服务器配置
server:port: 8082 # 指定端口号# mybatis 相关配置,单独配置,顶格写
mybatis:mapper-locations: classpath:mapper/**/*.xml # 指定 xxxMapper.xml的扫描路径configuration: # 配置打印 MyBatis 执行的 SQLlog-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 日志信息
logging:pattern:dateformat: yyyy-MM-dd HH:mm:ss # 日期格式file:path: logs/level:root: info
四.工程搭建
1.项目结构
2.生成类的映射文件
版本统一管理
<mybatis-generator-plugin-version>1.4.1</mybatis-generator-plugin-version>
在 build --> plugins 标签中加入如下配置
<build><plugins><!-- mybatis 生成器插件 --><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>${mybatis-generator-plugin-version}</version><executions><execution><id>Generate MyBatis Artifacts</id><phase>deploy</phase><goals><goal>generate</goal></goals></execution></executions><!-- 相关配置 --><configuration><!-- 打开日志 --><verbose>true</verbose><!-- 允许覆盖 --><overwrite>true</overwrite><!-- 配置文件路径 --><configurationFile>src/main/resources/mybatis/generatorConfig.xml</configurationFile></configuration></plugin></plugins></build>
generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfigurationPUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN""http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration><!-- 驱动包路径,location中路径替换成自己本地路径 --><classPathEntry location="D:\java cave\Maven\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:13306/forum_db?characterEncoding=utf8&useSSL=false"userId="root"password="woaini520"></jdbcConnection><javaTypeResolver><!-- 小数统一转为BigDecimal --><property name="forceBigDecimals" value="false"/></javaTypeResolver><!-- 实体类生成位置 --><javaModelGenerator targetPackage="com.javastudy.forum.model" targetProject="src/main/java"><property name="enableSubPackages" value="true"/><property name="trimStrings" value="true"/></javaModelGenerator><!-- mapper.xml生成位置 --><sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"><property name="enableSubPackages" value="true"/></sqlMapGenerator><!-- DAO类生成位置 --><javaClientGenerator type="XMLMAPPER" targetPackage="com.javastudy.forum.dao" targetProject="src/main/java"><property name="enableSubPackages" value="true"/></javaClientGenerator><!-- 配置生成表与实例, 只需要修改表名tableName, 与对应类名domainObjectName 即可--><table tableName="t_article" domainObjectName="Article" enableSelectByExample="false"enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"enableUpdateByExample="false"><!-- 类的属性用数据库中的真实字段名做为属性名, 不指定这个属性会自动转换 _ 为驼峰命名规则--><property name="useActualColumnNames" value="true"/></table><table tableName="t_article_reply" domainObjectName="ArticleReply" enableSelectByExample="false"enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table><table tableName="t_board" domainObjectName="Board" enableSelectByExample="false" enableDeleteByExample="false"enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table><table tableName="t_message" domainObjectName="Message" enableSelectByExample="false"enableDeleteByExample="false" enableDeleteByPrimaryKey="false" enableCountByExample="false"enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table><table tableName="t_user" domainObjectName="User" enableSelectByExample="false" enableDeleteByExample="false"enableDeleteByPrimaryKey="false" enableCountByExample="false" enableUpdateByExample="false"><property name="useActualColumnNames" value="true"/></table></context>
</generatorConfiguration>
下面就是创建成功的样子
之后不要忘记给所有的Mapper加上@Mapper注解
3. 配置扫描配置
@Configuration
//指定mybatis的扫描路径
@MapperScan("com.javastudy.forum.dao")
public class MybatisConfig {
}
4.测试
插入一条数据
INSERT INTO `forum_db`.`t_user` (`id`, `username`, `password`, `nickname`,`gender`, `salt`, `avatarurl`, `articlecount`, `isadmin`, `state`,`deletestate`, `createtime`, `updatetime`) VALUES (1, 'joyboy', '123456', '路飞', 2, '123', 'avatar.png',0, 1, 0, 0, '2022-12-13 22:30:10', '2022-12-13 22:30:13');
@SpringBootTest
@Slf4j
class UserMapperTest {@ResourceUserMapper userMapper;@ResourceObjectMapper objectMapper;@Testvoid insert() {}@Testvoid insertSelective() {}@Testvoid selectByPrimaryKey() throws JsonProcessingException {User user = userMapper.selectByPrimaryKey(1L);log.info(objectMapper.writeValueAsString(user));}@Testvoid updateByPrimaryKeySelective() {}@Testvoid updateByPrimaryKey() {}
}
五.准备公共代码编写
1.编写公共代码
1.定义状态码
定义的状态码如下:
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, "两次输入的密码不一致"),ERROR_SERVICES(2000, "服务器内部错误"),ERROR_IS_NULL(2001, "IS NULL.");long code;String message;ResultCode(long code, String message) {this.code = code;this.message = message;}@Overridepublic String toString() {return "ResultCode{" +"code=" + code +", message='" + message + '\'' +'}';}public long getCode() {return code;}public String getMessage() {return message;}
}
2.定义返回结果
public class AppResult<T> {private Long code;private String message;private T data;public AppResult() {}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 AppResult success () {return new AppResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());}public static AppResult success (String message) {return new AppResult(ResultCode.SUCCESS.getCode(), message);}public static <T> AppResult<T> success (String message, T data) {return new AppResult<>(ResultCode.SUCCESS.getCode(), message, data);}public static <T> AppResult<T> success (T data) {return new AppResult<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);}/*** 失败*/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 long getCode() {return code;}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;}}
3.自定义异常
public class ApplicationException extends RuntimeException {private AppResult errorResult;public ApplicationException(AppResult errorResult) {// 加入if 报错 ,待查super(errorResult.getMessage());this.errorResult = errorResult;}public ApplicationException(String message) {super(message);}public ApplicationException(String message, Throwable cause) {super(message, cause);}public ApplicationException(Throwable cause) {super(cause);}public AppResult getErrorResult() {return errorResult;}
}
4.全局异常处理
@Slf4j // 日志
@ControllerAdvice
public class GlobalExceptionHandler {// 以JSON的形式返回BOYD中的数据@ResponseBody// 指定要处理的异常@ExceptionHandler(ApplicationException.class)public AppResult handleApplicationException(ApplicationException e) {// 打印异常信息, 上线生产之前要删除这个打印方式e.printStackTrace();// 打印日志log.error(e.getMessage());// 判断自定义的异常信息是否为空if (e.getErrorResult() != null) {// 返回异常类中记录的状态return e.getErrorResult();}// 根据异常信息,封装AppResultreturn AppResult.failed(e.getMessage());}@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.getMessage());}// 根据异常信息,封装AppResultreturn AppResult.failed(e.getMessage());}
}
2.Swagger自动生成
1.引入版本号
<springfox-boot-starter.version>3.0.0</springfox-boot-starter.version>
2.引入相关依赖
<!-- 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>
3.编写配置类
package com.javastudy.forum.config;import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;/*** @author Chooker* @create 2023-08-09 12:45*/
// 配置类
@Configuration
// 开启Springfox-Swagger
@EnableOpenApi
public class SwaggerConfig {/*** Springfox-Swagger基本配置** @return*/@Beanpublic Docket createApi() {Docket docket = new Docket(DocumentationType.OAS_30).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.javastudy.forum.controller")).paths(PathSelectors.any()).build();return docket;}// 配置API基本信息private ApiInfo apiInfo() {ApiInfo apiInfo = new ApiInfoBuilder().title("论坛系统API").description("论坛系统前后端分离API测试").contact(new Contact("Chooker", "https://blog.csdn.net/qq_64580912", "ak1474502128@gmail.com")).version("1.0").build();return apiInfo;}/*** 解决SpringBoot 6.0以上与Swagger 3.0.0 不兼容的问题* 复制即可**/@Beanpublic WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,ServletEndpointsSupplier servletEndpointsSupplier,ControllerEndpointsSupplier controllerEndpointsSupplier,EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,WebEndpointProperties webEndpointProperties, Environment environment) {List<ExposableEndpoint<?>> allEndpoints = new ArrayList();Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();allEndpoints.addAll(webEndpoints);allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());String basePath = webEndpointProperties.getBasePath();EndpointMapping endpointMapping = new EndpointMapping(basePath);boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment,basePath);return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),shouldRegisterLinksMapping, null);}private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,String basePath) {return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));}
}
4.application.yml 中添加配置
spring: mvc:pathmatch:matching-strategy: ANT_PATH_MATCHER #Springfox-Swagger兼容性配置
5.具体API
API常用注解
- @Api: 作用在Controller上,对控制器类的说明
- tags="说明该类的作用,可以在前台界面上看到的注解
- @ApiModel: 作用在响应的类上,对返回响应数据的说明
- @ApiModelProerty:作用在类的属性上,对属性的说明
- @ApiOperation: 作用在具体方法上,对API接口的说明
- @ApiParam: 作用在方法中的每⼀个参数上,对参数的属性进行说明
访问地址:http://127.0.0.1:18080/swagger-ui/index.html 出现以下页面
导入postman
这样就可以在postman中进行测试
六.注册功能的实现
1.在Mapper.xml中编写SQL语句
1.写入操作(注册部分的代码)
已经自动生成这部分的代码,在UserMapper.xml文件中
<insert id="insertSelective" parameterType="com.javastudy.forum.model.User">insert into t_user<trim prefix="(" suffix=")" suffixOverrides=","><if test="id != null">id,</if><if test="username != null">username,</if><if test="password != null">password,</if><if test="nickname != null">nickname,</if><if test="phonenum != null">phonenum,</if><if test="email != null">email,</if><if test="gender != null">gender,</if><if test="salt != null">salt,</if><if test="avatarurl != null">avatarurl,</if><if test="articlecount != null">articlecount,</if><if test="isadmin != null">isadmin,</if><if test="remark != null">remark,</if><if test="state != null">state,</if><if test="deletestate != null">deletestate,</if><if test="createtime != null">createtime,</if><if test="updatetime != null">updatetime,</if></trim><trim prefix="values (" suffix=")" suffixOverrides=","><if test="id != null">#{id,jdbcType=BIGINT},</if><if test="username != null">#{username,jdbcType=VARCHAR},</if><if test="password != null">#{password,jdbcType=VARCHAR},</if><if test="nickname != null">#{nickname,jdbcType=VARCHAR},</if><if test="phonenum != null">#{phonenum,jdbcType=VARCHAR},</if><if test="email != null">#{email,jdbcType=VARCHAR},</if><if test="gender != null">#{gender,jdbcType=TINYINT},</if><if test="salt != null">#{salt,jdbcType=VARCHAR},</if><if test="avatarurl != null">#{avatarurl,jdbcType=VARCHAR},</if><if test="articlecount != null">#{articlecount,jdbcType=INTEGER},</if><if test="isadmin != null">#{isadmin,jdbcType=TINYINT},</if><if test="remark != null">#{remark,jdbcType=VARCHAR},</if><if test="state != null">#{state,jdbcType=TINYINT},</if><if test="deletestate != null">#{deletestate,jdbcType=TINYINT},</if><if test="createtime != null">#{createtime,jdbcType=TIMESTAMP},</if><if test="updatetime != null">#{updatetime,jdbcType=TIMESTAMP},</if></trim></insert>
2.根据用户名查询用户信息
创建如图的目录结构,并在extension目录下创建UserExtMapper.xml文件,里面编写自动生成以外的代码.
可以看出 UserExtMapper.xml与UserMapper.xml是共用resultMap和Base_Column_List的
<?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.javastudy.forum.dao.UserMapper"><!--1. 注意namespace表示命名空间,要与 UserMapper.xml中的namespace相同2. 统一用com.javastudy.forum.dao.UserMapper, 也就是UserMapper的完全限定名(包名+类名)3. 不同的映射文件指定了相同的namespace后,定义的所有用id或name标识的结果集映射都可以在不同的文件中共享
--><!-- 根据用户名查询用户信息--><select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">select<include refid="Base_Column_List" />from t_user where username=#{username,jdbcType=VARCHAR};</select></mapper>
2.在Mapper.java中定义方法
@Mapper
public interface UserMapper {int insert(User row);int insertSelective(User row);User selectByPrimaryKey(Long id);int updateByPrimaryKeySelective(User row);int updateByPrimaryKey(User row);/*** 根据用户名查询用户信息* @param username* @return*/User selectByUsername(String username);
}
3.定义Service接口
public interface IUserService {User selectByUsername(String username);int createNormalUser(User user);
}
4.实现Serivce接口
@Slf4j //日志
@Service
public class UserServiceImpl implements IUserService {@ResourceUserMapper userMapper;@Overridepublic User selectByUsername(String username) {//非空校验if (StringUtils.isEmpty(username)) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//根据用户名查询用户信息User user = userMapper.selectByUsername(username);return user;}@Overridepublic int createNormalUser(User user) {//非空校验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));}//校验用户名是否存在User exitsUser = userMapper.selectByUsername(user.getUsername());if (exitsUser != null) {//打印日志log.warn(ResultCode.FAILED_USER_EXISTS.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));}//为性別设置默认值if (user.getGender() != null) {if (user.getGender() < 0 || user.getGender() > 2) {user.setGender((byte) 2);}} else {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);//写入数据库int row = userMapper.insertSelective(user);if (row != 1) {//打印日志log.warn(ResultCode.FAILED_CREATE.toString() + " 注册用户失败,username:" + user.getUsername());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));}return row;}
}
5.单元测试
加入SpringBootTest注解
1.测试selectByUsername接口
@SpringBootTest
@Slf4j
class UserServiceImplTest {@Resourceprivate IUserService userService;@Resourceprivate ObjectMapper objectMapper;@Testvoid selectByUsername() throws JsonProcessingException {User user = userService.selectByUsername("joyboy");log.info(objectMapper.writeValueAsString(user));log.info("============================================");User joyboy222 = userService.selectByUsername("");log.info(objectMapper.writeValueAsString(joyboy222));}@Testvoid createNormalUser() {}
}
符合预期,校验完成
2.测试createNormalUser接口
@Testvoid createNormalUser() {User user = new User();user.setUsername("testuser");user.setPassword("123456");user.setNickname("测试用户");user.setSalt("123456");userService.createNormalUser(user);log.info("注册成功");log.info("=================================");user.setUsername("joyboy");userService.createNormalUser(user);log.info("注册成功");}
6.Controller实现方法外提供API接口
@RestController
@Slf4j
@RequestMapping("/user")
@Api(tags = "用户接口")
public class UserController {@ResourceIUserService 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) {if (!password.equals(passwordRepeat)) {return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);}User user = new User();user.setUsername(username);user.setNickname(nickname);//对密码进行处理String salt = UUIDUtils.UUID_32();password = MD5Utils.md5Salt(password, salt);user.setPassword(password);user.setSalt(salt);userService.createNormalUser(user);return AppResult.success("注册成功");}}
7.测试API接口
打开swagger进行测试
8.实现前端逻辑,完成前后端交互
可以在以下地址下载: forum_static.zip
// 构造数据var 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) {if (respData.code == 0) {location.assign("sign-in.html");} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},//http请求的失败error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});});
七.登录功能实现
1.在Mapper.xml中编写SQL语句
之前已经定义过了selectByName
2.在Mapper.java中定义方法
之前已经定义过了selectByName
3.定义Service接口
User login(String username,String password);
4.实现Serivce接口
@Overridepublic User login(String username, String password) {User user = selectByUsername(username);if (user == null) {//打印日志log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());//抛出异常 ---防止恶意猜测throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));}//密码校验boolean flag = MD5Utils.verifyOriginalAndCiphertext(password, user.getSalt(), user.getPassword());if (!flag) {//打印日志log.warn("密码输入错误: username: "+username+ " password: " + password);//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));}return user;}
5.单元测试
@Testvoid login() {User user = userService.login("joyboy", "123456");log.info(user.toString());User user2 = userService.login("user", "123456");log.info(user2.toString());}
符合预期
6.Controller实现方法外提供API接口
@ApiOperation("用户登录")@PostMapping("/login")public AppResult register(HttpServletRequest httpServletRequest,@ApiParam("用户名") @RequestParam("username") @NonNull String username,@ApiParam("密码") @RequestParam("password") @NonNull String password) {User user = userService.login(username, password);//1.获取session对象HttpSession session = httpServletRequest.getSession(true);//2.用户信息保存到session中session.setAttribute(AppConfig.SESSION_USER_KEY, user);return AppResult.success("登陆成功");}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// 构造数据var postData = {username: $("#username").val(),password: $("#password").val(),}// 发送AJAX请求,成功后跳转到index.html$.ajax({type: "post",url: "/user/login",//数据类型contentType: "application/x-www-form-urlencoded",data: postData,success: function (respData) {if (respData.code == 0) {location.assign("/index.html");} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
八.退出功能实现
退出功能直接在session中删除用户信息即可,因此不需要前面的五步
1.Controller实现方法外提供API接口
@ApiOperation("用户注销")@GetMapping("/logout")public AppResult logout(HttpServletRequest httpServletRequest) {//获取Session对象HttpSession session = httpServletRequest.getSession(false);if (session != null) {session.invalidate();}//返回结果return AppResult.success("注销成功");}
2.测试API接口
需要先登录,才能退出
3.实现前端逻辑,完成前后端交互
// 成功后,跳转到sign-in.html$('#index_user_logout').click(function () {$.ajax({type: "get",url: "/user/logout",success: function (respData) {if (respData.code == 0) {location.assign("/sign-in.html");} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});});
九.获取用户信息实现
1.Controller实现方法外提供API接口
@ApiOperation("获取用户信息")@GetMapping("/info")public AppResult<User> getInfo(HttpServletRequest httpServletRequest) {//获取Session对象HttpSession session = httpServletRequest.getSession(false);if (session == null || session.getAttribute(AppConfig.SESSION_USER_KEY) == null) {return AppResult.failed("用户未登录");}User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);return AppResult.success(user);}
2.测试API接口
此时我们进行测试可以得到以上的数据,但是此时还是有一定的弊端的
- 密码和盐不能在网络上进行传输,会有安全隐患
- 有些为空的数据没有必要传输
- 日期信息的传输格式不是常规的
接下来一一来解决以上的问题.
1. 解决密码和盐传输的问题
在实体类属性上面加入@JsonIgnore注解
@ApiModelProperty("密码")@JsonIgnoreprivate String password;@ApiModelProperty("性别")private Byte gender;@ApiModelProperty("盐")@JsonIgnoreprivate String salt;@ApiModelProperty("删除状态")@JsonIgnoreprivate Byte deletestate;
2.解决空数据传输的问题和解决日期格式的问题
# JSON序列化配置jackson:date-format: yyyy-MM-dd HH:mm:ss # 日期格式default-property-inclusion: NON_NULL # 不为null时序列化
此时符合条件
登录的时候,发现data信息不见了,因为data信息为null,所以没有进行序列化,但是实际上code,message,data信息就算是null也是要进行传输的.
加入以下注解可以解决这个问题.
@ApiModelProperty("状态码")@JsonInclude(JsonInclude.Include.ALWAYS) // 任何情况下都参与JSON序列化private Long code;@ApiModelProperty("错误信息")@JsonInclude(JsonInclude.Include.ALWAYS) // 任何情况下都参与JSON序列化private String message;@ApiModelProperty("返回的数据")@JsonInclude(JsonInclude.Include.ALWAYS) // 任何情况下都参与JSON序列化private T data;
头像同样也是需要的
@ApiModelProperty("头像地址")@JsonInclude(JsonInclude.Include.ALWAYS)private String avatarurl;
3.实现前端逻辑,完成前后端交互
//========================= 获取用户信息 =======================// 成功后,手动设置用户信息// $('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');$.ajax({type: "get",url: "/user/info",success: function (respData) {if (respData.code == 0) {//成功var user = respData.data;if (!user.avatarurl) {//设默认的头像user.avatarurl = 'image/avatar01.jpeg';}$('#index_nav_nickname').html(user.nickname);$('#index_nav_avatar').css('background-image', 'url(' + user.avatarurl + ')');let subName = user.isAdmin == 1 ? '管理员' : '普通用户';$('#index_nav_name_sub').html(subName);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
十.拦截器的实现
论坛中的大部分接口都需要在登录的情况下进行访问,因此我们需要一个拦截器对访问页面的时候对未登录进行校验
1.LoginInterceptor
未登录跳转的页面我们在application.yml中定义,这样为了以后的维护更加方便.
forum:login:url: sign-in.html
@Component
public class LoginInterceptor implements HandlerInterceptor {@Value("${forum.login.url}")private String defaultUrl;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute(AppConfig.SESSION_USER_KEY) != null) {return true;}//采用重定向response.sendRedirect(defaultUrl);return true;}
}
2.AppInterceptorConfigurer
@Configuration // 把当前配置类加入到Spring中
public class AppInterceptorConfigurer implements WebMvcConfigurer {@Resourceprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/sign-in.html") // 排除登录HTML.excludePathPatterns("/sign-up.html") // 排除注册HTML.excludePathPatterns("/user/login") // 排除登录api接口.excludePathPatterns("/user/register") // 排除注册api接口.excludePathPatterns("/user/logout") // 排除退出api接口.excludePathPatterns("/swagger*/**") // 排除登录swagger下所有.excludePathPatterns("/v3*/**") // 排除登录v3下所有,与swagger相关.excludePathPatterns("/dist/**") // 排除所有静态文件.excludePathPatterns("/image/**").excludePathPatterns("/**.ico").excludePathPatterns("/js/**");}
}
十一.获取用户信息实现2
当我们进入一个帖子的时候,我们点击发帖的用户,可以看到用户的信息,此时前端给我们一个id,我们需要从数据库中根据id插叙到用户的信息.
1.在Mapper.xml中编写SQL语句
已经自动生成了
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">select <include refid="Base_Column_List" />from t_userwhere id = #{id,jdbcType=BIGINT}</select>
2.在Mapper.java中定义方法
User selectByPrimaryKey(Long id);
3.定义Service接口
/*** 根据id查询用户的信息* @param id* @return*/User selectById(Long id);
4.实现Serivce接口
public User selectById(Long id) {if(id<0||id==null){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}User user = userMapper.selectByPrimaryKey(id);return user;}
5.单元测试
@Testvoid selectById() {User user = userService.selectById(1L);log.info(user.toString());User user2 = userService.selectById(2L);log.info(user2.toString());}
6.Controller实现方法外提供API接口
@ApiOperation("获取用户信息")@GetMapping("/info")public AppResult<User> getInfo(HttpServletRequest httpServletRequest,@ApiParam("用户id") @RequestParam(value = "id", required = false) Long id) {User user;if (id == null) {//获取Session对象HttpSession session = httpServletRequest.getSession(false);user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);} else {user = userService.selectById(id);}return AppResult.success(user);}
7.测试API接口
测试成功.
十二.获取版块信息实现
先向数据库中插入一些板块的信息.
-- 写入版块信息数据
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');
1.在Mapper.xml中编写SQL语句
<?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.javastudy.forum.dao.BoardMapper"><select id="selectByNum" parameterType="java.lang.Integer" resultMap="BaseResultMap">SELECT<include refid="Base_Column_List"/>FROM t_boardWHERE state=0 AND deleteState=0ORDER BY sort ASCLIMIT 0,#{num,jdbcType=INTEGER};</select></mapper>
2.在Mapper.java中定义方法
List<Board> selectByNum(@Param("num") Integer num);
3.定义Service接口
public interface IBoardService {List<Board> selectByNum(Integer num);
}
4.实现Serivce接口
@Service
@Slf4j
public class BoardServiceImpl implements IBoardService {@Resourceprivate BoardMapper boardMapper;@Overridepublic List<Board> selectByNum(Integer num) {if (num < 0 || num == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}List<Board> boards = boardMapper.selectByNum(num);return boards;}
}
5.单元测试
@SpringBootTest
@Slf4j
class BoardServiceImplTest {@ResourceBoardServiceImpl boardService;@Testvoid selectByNum() {List<Board> boards = boardService.selectByNum(9);log.info(boards.toString());}
}
6.Controller实现方法外提供API接口
@RestController
@Slf4j
@RequestMapping("/board")
@Api(tags = "板块接口")
public class BoardController {@Value("${forum.index.board-num}")Integer num;@Resourceprivate IBoardService boardService;@GetMapping("/topList")@ApiOperation("获取板块列表")public AppResult<List<Board>> topList() {List<Board> boards = boardService.selectByNum(num);return AppResult.success(boards);}}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// ========================= 获取版块信息 =======================// 成功后,调用buildTopBoard()方法,构建版块列表$.ajax({type: "get",url: "/board/topList",success: function (respData) {if (respData.code == 0) {buildTopBoard(respData.data);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
十三.获得帖子信息实现
下面是获得所有帖子信息的实现
1.在Mapper.xml中编写SQL语句
创建ArticleExtMapper.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.javastudy.forum.dao.ArticleMapper"><resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.Article" extends="ResultMapWithBLOBs"><association property="user" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_"/></resultMap><!-- 查询所有的帖子集合--><select id="selectAll" resultMap="AllInfoResultMap">select u.id as u_id,u.nickname as u_nickname,u.gender as u_gender,u.avatarUrl as u_avatarUrl,a.id,a.boardId,a.userId,a.title,a.visitCount,a.replyCount,a.likeCount,a.state,a.deleteState,a.createTime,a.updateTimefrom t_article as a,t_user as uwhere a.userId = u.idand a.deleteState = 0order by a.createTime DESC</select></mapper>
2.在Mapper.java中定义方法
List<Article> selectAll();
3.定义Service接口
public interface IArticleService {List<Article> selectAll();
}
4.实现Serivce接口
@Slf4j
@Service
public class ArticleService implements IArticleService {@ResourceArticleMapper articleMapper;@Overridepublic List<Article> selectAll() {List<Article> articles = articleMapper.selectAll();return articles;}
}
5.单元测试
@SpringBootTest
@Slf4j
class ArticleServiceImplTest {@ResourceIArticleService articleService;@Testvoid selectAll() {List<Article> articles = articleService.selectAll();log.info(articles.toString());}
}
6.Controller实现方法外提供API接口
@RestController
@Slf4j
@RequestMapping("/article")
@Api(tags = "帖子接口")
public class ArticleController {@Resourceprivate IArticleService articleService;@GetMapping("/getAllByBoardId")@ApiOperation("获得所有的帖子")public AppResult<List<Article>> getAllByBoardId() {List<Article> articles = articleService.selectAll();if (articles == null) {articles = new ArrayList<>();}return AppResult.success(articles);}
}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// ========================= 获取帖子列表 =======================// 成功后,调用listBuildArticleList()方法,构建帖子列表$.ajax({type: "get",url: "/article/getAllByBoardId",success: function (respData) {if (respData.code == 0) {listBuildArticleList(respData.data);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
十四.获得指定帖子信息实现
1.在Mapper.xml中编写SQL语句
<!-- 查询所有的帖子集合--><select id="selectByBoardId" resultMap="AllInfoResultMap">selectu.id as u_id,u.nickname as u_nickname,u.gender as u_gender,u.avatarUrl as u_avatarUrl,a.id,a.boardId,a.userId,a.title,a.visitCount,a.replyCount,a.likeCount,a.state,a.deleteState,a.createTime,a.updateTimefrom t_article as a, t_user as uwhere a.userId = u.idand a.deleteState = 0and a.boardId = #{boardId,jdbcType=BIGINT}order by a.createTime DESC;</select>
2.在Mapper.java中定义方法
List<Article> selectByBoardId();
3.定义Service接口
List<Article> selectByBoardId(Long boardId);
4.实现Serivce接口
@Overridepublic List<Article> selectByBoardId(Long boardId) {if(boardId==null){//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}List<Article> articles = articleMapper.selectByBoardId(boardId);return articles;}
5.单元测试
@Testvoid selectByBoardId() {List<Article> articles = articleService.selectByBoardId(1L);log.info(articles.toString());}
6.Controller实现方法外提供API接口
@GetMapping("/getAllByBoardId")@ApiOperation("获得所有的帖子")public AppResult<List<Article>> getAllByBoardId(@ApiParam("板块id")@RequestParam(value = "boardId", required = false) Long boardId) {List<Article> articles;if (boardId == null) {articles = articleService.selectAll();} else {articles = articleService.selectByBoardId(boardId);}if (articles == null) {articles = new ArrayList<>();}return AppResult.success(articles);}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// ========================= 获取帖子列表 =======================// 成功后,调用listBuildArticleList()方法,构建帖子列表$.ajax({type: "get",url: "/article/getAllByBoardId"+queryString,success: function (respData) {if (respData.code == 0) {listBuildArticleList(respData.data);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
十五.根据Id获取板块信息
1.在Mapper.xml中编写SQL语句
已经自动生成
<select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">select <include refid="Base_Column_List" />from t_boardwhere id = #{id,jdbcType=BIGINT}</select>
2.在Mapper.java中定义方法
已经自动生成
Board selectByPrimaryKey(Long id);
3.定义Service接口
Board selectById(Long id);
4.实现Serivce接口
@Overridepublic Board selectById(Long id) {if (id < 0 || id == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Board board = boardMapper.selectByPrimaryKey(id);return board;}
5.单元测试
@Testvoid selectById() {Board board = boardService.selectById(1L);log.info(board.toString());}
6.Controller实现方法外提供API接口
@GetMapping("/getById")@ApiOperation("根据id获取板块信息")public AppResult<Board> getById(@ApiParam("板块id") @RequestParam("id") Long id) {Board board = boardService.selectById(id);return AppResult.success(board);}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// ========================= 获取版块信息 =======================// function getBoardInfo(boardId) {if (!boardId) {return;}// 发送请求, 成功后,显示版块相关信息$.ajax({type: "get",url: "/board/getById?id=" + boardId,success: function (respData) {if (respData.code == 0) {$('#article_list_board_title').html(respData.data.name);$('#article_list_count_board').html('帖子数量: ' + respData.data.articleCount);$('#article_list_count_board').show();} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});}
十六.发布新帖操作
思考一下发布帖子会经历什么样的操作.首先最直观的就是新增了一篇文章,涉及到的是文章表(article),其次需要更新用户的文章数,涉及用户表(user),最后需要更新板块的帖子数,涉及到板块表(board),并且这三个操作要么都成功,要么都失败,因此我们需要给这三个操作加上事务.
1.在Mapper.xml中编写SQL语句
更新文章数也有两种方式
第一种:重新定义一个update的语句将articleCount字段加一.
<update id="updateArticleCount" parameterType="java.lang.Long">UPDATE t_userSET articleCount=articleCount + 1WHERE id = #{id,jdbcType=BIGINT};</update>
第二种:使用生成的updateByPrimaryKeySelective的语句,在java程序查询到对应的用户信息,并将文章数加一之后调用更新的sql.(具体下面演示,下面采用这种方式)
1.用户表更新文章数
使用生成的updateByPrimaryKeySelective
2.板块表更新文章数
同样的使用生成的updateByPrimaryKeySelective
3.文章表添加文章
使用生成的insertSelective
2.在Mapper.java中定义方法
已经定义过了
3.定义Service接口
1.用户表
void addOneArticleCountById(Long id);
2.板块表
void addOneArticleCountById(Long id);
3.文章表
@Transactionalvoid create (Article article);
4.实现Serivce接口
1.用户表
@Overridepublic void addOneArticleCountById(Long id) {if (id < 0 || id == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}// 查询现有的用户信息User user = selectById(id);// 校验用户是否为空if (user == null) {// 打印日志log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));}user.setArticlecount(user.getArticlecount() + 1);user.setUpdatetime(new Date());int row = userMapper.updateByPrimaryKeySelective(user);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
2.板块表
@Overridepublic void addOneArticleCountById(Long id) {if (id < 0 || id == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Board board = selectById(id);if (board == null) {//打印日志log.warn(ResultCode.FAILED_BOARD_EXISTS.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_EXISTS));}board.setArticleCount(board.getArticleCount() + 1);board.setUpdateTime(new Date());int row = boardMapper.updateByPrimaryKeySelective(board);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
3.文章表
@Overridepublic void create(Article article) {if (article == null || article.getBoardId() == null || article.getUserId() == null ||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.FAILED_CREATE.toString() + " 新增帖子失败");// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));}userService.addOneArticleCountById(article.getUserId());boardService.addOneArticleCountById(article.getBoardId());// 打印日志 ---如何获取文章的id,可以在mapper.xml进行设置log.info(ResultCode.SUCCESS.toString() + " 帖子发布成功, articleId = " + article.getId());}
为了插入文章之后获取到文章的id,我们可以进行如下的操作.
5.单元测试
1.用户表
@Testvoid addOneArticleCountById() {userService.addOneArticleCountById(1L);log.info("更新成功");}
更新前
更新后
2.板块表
@Testvoid addOneArticleCountById() {boardService.addOneArticleCountById(9L);log.info("更新成功");}
更新前
更新后
3.文章表
@Testvoid create() {Article article = new Article();article.setTitle("8.18测试的文章标题");article.setContent("8.18测试的文章内容");article.setBoardId(9L);article.setUserId(2L);articleService.create(article);log.info("创建成功");}
相应的文章数目也发生了改变
6.Controller实现方法外提供API接口
@ApiOperation("发布帖子")@PostMapping("/create")public AppResult create(@ApiParam("文章标题") @RequestParam("title") @NonNull String title,@ApiParam("文章内容") @RequestParam("content") @NonNull String content,@ApiParam("板块id") @RequestParam("boardId") @NonNull Long boardId,HttpServletRequest httpServletRequest) {//获取用户idHttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);// 校验用户状态if (user.getState() == 1) {// 用户已禁言, 返回提示return AppResult.failed(ResultCode.FAILED_USER_BANNED);}//设置文章信息Article article = new Article();article.setUserId(user.getId());article.setTitle(title);article.setBoardId(boardId);article.setContent(content);articleService.create(article);return AppResult.success("新增文章成功").}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// 构造帖子对象var postData = {title: titleEl.val(),content: contentEl.val(),boardId: boardIdEl.val(),}// 提交, 成功后调用changeNavActive($('#nav_board_index'));回到首页并加载帖子列表// contentType: 'application/x-www-form-urlencoded'$.ajax({type: "post",url: "/article/create",data: postData,contentType: 'application/x-www-form-urlencoded',success: function (respData) {if (respData.code == 0) {// 提示$.toast({heading: '成功',text: respData.message,icon: 'success'});// 成功后跳转到首页changeNavActive($('#nav_board_index'));} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
十七.获取文章详情
获取文章详情的时候也需要获取相关用户的信息,板块的信息
1.在Mapper.xml中编写SQL语句
<mapper namespace="com.javastudy.forum.dao.ArticleMapper"><resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.Article" extends="ResultMapWithBLOBs"><!-- 关联User对象 --><association property="user" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_"/><!-- 关联Board对象--><association property="board" resultMap="com.javastudy.forum.dao.BoardMapper.BaseResultMap" columnPrefix="b_"/></resultMap><!-- 根据帖子Id查询帖子详情 --><select id="selectById" 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,b.id as b_id,b.name as b_name,a.id,a.boardId,a.userId,a.title,a.visitCount,a.replyCount,a.likeCount,a.state,a.deleteState,a.createTime,a.updateTime,a.contentfrom t_article as a,t_user as u,t_board bwhere a.userId = u.idand a.boardId = b.idand a.id = #{id,jdbcType=BIGINT}and a.deleteState = 0</select></mapper>
2.在Mapper.java中定义方法
Article selectById(Long id);
3.定义Service接口
Article selectById(Long id);
4.实现Serivce接口
@Overridepublic Article selectById(Long id) {if (id == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Article article = articleMapper.selectById(id);return article;}
5.单元测试
@Testvoid selectById() {Article article = articleService.selectById(1L);log.info(article.toString());}
6.Controller实现方法外提供API接口
@ApiOperation("通过文章id获取文章的详细信息")@GetMapping("/getById")public AppResult getById(@ApiParam("文章id") @RequestParam("id") @NonNull Long id) {Article article = articleService.selectById(id);if (article == null) {// 返回提示return AppResult.failed("帖子不存在");}return AppResult.success(article);}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// ===================== 请求帖子详情 ===================== $.ajax({type: "get",url: "/article/getById?id=" + currentArticle.id,success: function (respData) {if (respData.code == 0) {initArticleDetails(respData.data);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
十八.文章访问数量的增加
1.在Mapper.xml中编写SQL语句
<update id="updateVisitCountById" parameterType="java.lang.Long">update t_articleset visitCount=visitCount + 1where id = #{id,jdbcType=BIGINT}</update>
2.在Mapper.java中定义方法
int updateVisitCountById(Long id);
3.定义Service接口
Integer updateVisitCountById(Long id);
4.实现Serivce接口
public Integer updateVisitCountById(Long id) {if (id == null || id < 0) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}int row = articleMapper.updateVisitCountById(id);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}return null;}
5.单元测试
@Testvoid updateVisitCountById() {articleService.updateVisitCountById(1L);log.info("更新成功");}
更新前
更新后
6.Controller实现方法外提供API接口
更新查询操作即可
@ApiOperation("通过文章id获取文章的详细信息")@GetMapping("/getById")public AppResult getById(@ApiParam("文章id") @RequestParam("id") @NonNull Long id) {Article article = articleService.selectById(id);if (article == null) {// 返回提示return AppResult.failed("帖子不存在");}articleService.updateVisitCountById(id);article.setVisitCount(article.getVisitCount() + 1);return AppResult.success(article);}
十九.编辑文章操作
在编辑文章之前,我们需要在文章中增加一个属性,来表示这篇文章是不是属于作者的,前端进行判断来选择展示修改和删除文章的按钮
@ApiOperation("通过文章id获取文章的详细信息")@GetMapping("/getById")public AppResult getById(@ApiParam("文章id") @RequestParam("id") @NonNull Long id,HttpServletRequest httpServletRequest) {Article article = articleService.selectById(id);if (article == null) {// 返回提示return AppResult.failed("帖子不存在");}articleService.updateVisitCountById(id);article.setVisitCount(article.getVisitCount() + 1);HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);if (user.getId() == article.getUserId()) {article.setOwn(true);}return AppResult.success(article);}
编辑文章的时候,我们需要先查询到需要修改文章的信息,然后再上一次内容的基础上进行修改,可以直接调用getById接口
article_edit.html页面
// ========================== 获取帖子详情 ========================== // 成功后,设置ID,版块名,标题,并初始编辑区同时设置正文initEditor(edit_article.content);$.ajax({type: "get",url: "/article/getById?id=" + currentArticle.id,success: function (respData) {var article = respData.data;if (respData.code == 0) {$("#edit_article_id").val(article.id);$("#edit_article_title").val(article.title);$('#edit_article_board_name').html(article.board.name);initEditor(article.content);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
1.在Mapper.xml中编写SQL语句
之前已经自动生成了
2.在Mapper.java中定义方法
之前已经自动生成了
3.定义Service接口
void modify(Long id, String title, String content);
4.实现Serivce接口
@Overridepublic void modify(Long id, String title, String content) {if (id == null || id < 0 || StringUtils.isEmpty(title) || StringUtils.isEmpty(content)) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Article article = new Article();article.setTitle(title);article.setContent(content);article.setId(id);article.setUpdateTime(new Date()); //设置更新时间int row = articleMapper.updateByPrimaryKeySelective(article);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
5.单元测试
@Testvoid modify() {articleService.modify(1L, "测试数据-标题1.1","测试数据-内容2.0");log.info("更新成功");}
更新前
更新后
6.Controller实现方法外提供API接口
@ApiOperation("更新帖子")@PostMapping("/modify")public AppResult modify(@ApiParam("文章标题") @RequestParam("title") @NonNull String title,@ApiParam("文章内容") @RequestParam("content") @NonNull String content,@ApiParam("文章id") @RequestParam("id") @NonNull Long id,HttpServletRequest httpServletRequest) {// 从session中获取当前登录的用户HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);// 校验用户信息if (user.getState() == 1) {// 返回提示信息return AppResult.failed(ResultCode.FAILED_USER_BANNED);}// 查询帖子信息Article article = articleService.selectById(id);// 校验帖子是否存在if (article == null) {// 返回提示信息return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);}// 判断当前登录用户是不是作者if (article.getUserId() != user.getId()) {// 返回提示信息return AppResult.failed("您不是帖子的作者,无权修改.");}// 校验帖子状态if (article.getState() == 1) {// 返回提示信息return AppResult.failed("帖子状态异常, 请联系管理员");}articleService.modify(id, title, content);return AppResult.success("更新成功");}
7.测试API接口
更新前
更新后
8.实现前端逻辑,完成前后端交互
// 构造修改对象var 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) {if (respData.code == 0) {// 成功后提示消息$.toast({heading: '成功',text: respData.message,icon: 'success'});// 跳转到首面changeNavActive($('#nav_board_index'));} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
二十.删除文章操作
删除文章的时候需要做以下三个操作
1.需要将删除文章的deleteState置为1(article表)
2.需要将相应板块的文章数减一(board表)
3.需要将相应用户的文章数减一(user表)
1.在Mapper.xml中编写SQL语句
1.user表
直接使用生成的updateByPrimaryKeySelective
2.board表
直接使用生成的updateByPrimaryKeySelective
3.article表
直接使用生成的updateByPrimaryKeySelective
2.在Mapper.java中定义方法
1.user表
直接使用生成的updateByPrimaryKeySelective
2.board表
直接使用生成的updateByPrimaryKeySelective
3.article表
直接使用生成的updateByPrimaryKeySelective
3.定义Service接口
1.user表
void subOneArticleCountById(Long id);
2.board表
void subOneArticleCountById(Long id);
3.article表
@Transactionalvoid delete(Long id);
4.实现Serivce接口
1.user表
@Overridepublic void subOneArticleCountById(Long id) {if (id < 0 || id == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}// 查询现有的用户信息User user = selectById(id);// 校验用户是否为空if (user == null) {// 打印日志log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));}user.setArticlecount(user.getArticlecount() - 1);// 判断减1之后,用户的发帖数是否小于0if (user.getArticlecount() < 0) {// 如果小于0,则设置为0user.setArticlecount(0);}user.setUpdatetime(new Date());int row = userMapper.updateByPrimaryKeySelective(user);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
2.board表
@Overridepublic void subOneArticleCountById(Long id) {if (id < 0 || id == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Board board = selectById(id);if (board == null) {//打印日志log.warn(ResultCode.FAILED_BOARD_EXISTS.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_EXISTS));}board.setArticleCount(board.getArticleCount() - 1);if (board.getArticleCount() < 0) {board.setArticleCount(0);}board.setUpdateTime(new Date());int row = boardMapper.updateByPrimaryKeySelective(board);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
3.article表
public void delete(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 = selectById(id);if (article == null || article.getUserId() == null || article.getBoardId() == null) {//打印日志log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));}article.setDeleteState((byte) 1);article.setUpdateTime(new Date());int row = articleMapper.updateByPrimaryKeySelective(article);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}userService.subOneArticleCountById(article.getUserId());boardService.subOneArticleCountById(article.getBoardId());log.info("删除帖子成功, article id = " + article.getId() + ", user id = " + article.getUserId() + ".");}
5.单元测试
1.user表
@Testvoid subOneArticleCountById() {userService.subOneArticleCountById(1L);log.info("更新成功");}
2.board表
@Testvoid subOneArticleCountById() {boardService.subOneArticleCountById(9L);log.info("更新成功"); }
3.article表
@Testvoid delete() {articleService.delete(6L);log.info("更新成功");}
更新前
t_article
t_board
t_user
t_article
t_board
t_user
6.Controller实现方法外提供API接口
@ApiOperation("删除帖子")@PostMapping("/delete")public AppResult delete(@ApiParam("文章id") @RequestParam("id") @NonNull Long id,HttpServletRequest httpServletRequest) {// 从session中获取当前登录的用户HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);// 校验用户信息if (user.getState() == 1) {// 返回提示信息return AppResult.failed(ResultCode.FAILED_USER_BANNED);}// 查询帖子信息Article article = articleService.selectById(id);// 校验帖子是否存在if (article == null) {// 返回提示信息return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);}// 判断当前登录用户是不是作者if (article.getUserId() != user.getId()) {// 返回提示信息return AppResult.failed("您不是帖子的作者,无权修改.");}// 校验帖子状态if (article.getState() == 1) {// 返回提示信息return AppResult.failed("帖子状态异常, 请联系管理员");}articleService.delete(id);return AppResult.success("删除成功");}
7.测试API接口
8.实现前端逻辑,完成前后端交互
// ====================== 处理删除事件 ======================$('#details_artile_delete').click(function () {$.ajax({type: "post",url: "/article/delete?id=" + $('#details_article_id').val(),success: function (respData) {if (respData.code == 0) {// 成功提示$.toast({heading: '成功',text: '删除成功',icon: 'success'});// 跳转到首页帖子列表changeNavActive($('#nav-link-title'));} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});});
二十一.点赞文章操作
1.在Mapper.xml中编写SQL语句
直接使用生成的updateByPrimaryKeySelective
2.在Mapper.java中定义方法
直接使用生成的updateByPrimaryKeySelective
3.定义Service接口
void thumbsUpById(Long id);
4.实现Serivce接口
@Overridepublic void thumbsUpById(Long id) {if (id == null || id < 0) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Article article = selectById(id);if (article == null || article.getLikeCount() == null) {//打印日志log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));}article.setLikeCount(article.getLikeCount() + 1);article.setUpdateTime(new Date());int row = articleMapper.updateByPrimaryKeySelective(article);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
5.单元测试
@Testvoid thumbsUpById() {articleService.thumbsUpById(1L);log.info("更新成功");}
更新前
更新后
6.Controller实现方法外提供API接口
@ApiOperation("点赞帖子")@PostMapping("/thumbsUp")public AppResult thumbsUp(@ApiParam("文章id") @RequestParam("id") @NonNull Long id,HttpServletRequest httpServletRequest) {// 从session中获取当前登录的用户HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);// 校验用户信息if (user.getState() == 1) {// 返回提示信息return AppResult.failed(ResultCode.FAILED_USER_BANNED);}// 查询帖子信息Article article = articleService.selectById(id);// 校验帖子是否存在if (article == null) {// 返回提示信息return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);}// 判断当前登录用户是不是作者if (article.getUserId() != user.getId()) {// 返回提示信息return AppResult.failed("您不是帖子的作者,无权修改.");}// 校验帖子状态if (article.getState() == 1) {// 返回提示信息return AppResult.failed("帖子状态异常, 请联系管理员");}articleService.thumbsUpById(id);return AppResult.success("点赞成功");}
7.测试API接口
8.实现前端逻辑,完成前后端
// ====================== 处理点赞 ======================$('#details_btn_like_count').click(function () {$.ajax({type: "post",url: "/article/thumbsUp?id=" + currentArticle.id,success: function (respData) {if (respData.code == 0) {currentArticle.likeCount = currentArticle.likeCount + 1;// 设置页面的值 $('#details_article_likeCount').html(currentArticle.likeCount);// 成功提示$.toast({heading: '成功',text: '点赞成功',icon: 'success'});} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});});
二十二.根据文章Id查询回复列表
先来进行插入数据
-- 写入回复表数据
INSERT INTO t_article_reply VALUES (NULL, 1, 1, NULL, NULL, '回复内容111', 0, 0, 0, '2023-08-14 16:52:00', '2023-08-14 16:52:00');
INSERT INTO t_article_reply VALUES (NULL, 1, 1, NULL, NULL, '回复内容222', 0, 0, 0, '2023-08-14 16:53:00', '2023-08-14 16:53:00');
INSERT INTO t_article_reply VALUES (NULL, 1, 1, NULL, NULL, '回复内容333', 0, 0, 0, '2023-08-14 16:54:00', '2023-08-14 16:54:00');
INSERT INTO t_article_reply VALUES (NULL, 1, 2, NULL, NULL, '回复内容444', 0, 0, 0, '2023-08-14 16:55:00', '2023-08-14 16:55:00');
INSERT INTO t_article_reply VALUES (NULL, 1, 2, NULL, NULL, '回复内容555', 0, 0, 0, '2023-08-14 16:56:00', '2023-08-14 16:56:00');
1.在Mapper.xml中编写SQL语句
<?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.javastudy.forum.dao.ArticleReplyMapper"><!-- 自定义表关联的结果集映射 --><resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.ArticleReply" extends="BaseResultMap"><!-- 关联用户表 --><association property="user" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_" /></resultMap><!-- 根据帖子Id查询回复列表 --><select id="selectByArticleId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">selectu.id as u_id,u.nickname as u_nickname,u.gender as u_gender,u.avatarUrl as u_avatarUrl,u.phoneNum as u_phoneNum,u.email as u_email,ar.id,ar.articleId,ar.postUserId,ar.replyId,ar.replyUserId,ar.content,ar.likeCount,ar.state,ar.deleteState,ar.createTime,ar.updateTimefrom t_article_reply ar, t_user uwhere ar.postUserId = u.idand ar.articleId = #{articleId,jdbcType=BIGINT}and ar.deleteState = 0order by ar.createTime desc</select>
</mapper>
2.在Mapper.java中定义方法
List<ArticleReply> selectByArticleId(Long articleId);
3.定义Service接口
public interface IArticleReplyService {List<ArticleReply> selectByArticleId(Long articleId);
}
4.实现Serivce接口
@Slf4j
@Service
public class ArticleReplyService implements IArticleReplyService {@Resourceprivate ArticleReplyMapper articleReplyMapper;@Overridepublic List<ArticleReply> selectByArticleId(Long articleId) {if (articleId == null || articleId < 0) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}List<ArticleReply> articleReplies = articleReplyMapper.selectByArticleId(articleId);return articleReplies;}
}
5.单元测试
@Slf4j
@SpringBootTest
class ArticleReplyServiceTest {@Resourceprivate IArticleReplyService articleReplyService;@Testvoid selectByArticleId() {List<ArticleReply> articleReplies = articleReplyService.selectByArticleId(1L);log.info(articleReplies.toString());}
}
6.Controller实现方法外提供API接口
@RestController
@Slf4j
@RequestMapping("/reply")
@Api(tags = "文章回复接口")
public class ArticleReplyController {@Resourceprivate IArticleReplyService articleReplyService;@Resourceprivate IArticleService articleService;@GetMapping("/getReplies")@ApiOperation("获取回复列表")public AppResult<List<ArticleReply>> getAllByBoardId(@ApiParam("板块id") @RequestParam("articleId") @NonNull Long articleId) {Article article = articleService.selectById(articleId);//判断文章是否删除if (article == null || article.getDeleteState() == 1) {// 返回错误提示return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);}//判断文章状态if (article.getState() == 1) {// 返回错误提示return AppResult.failed("帖子已封帖");}List<ArticleReply> articleReplies = articleReplyService.selectByArticleId(articleId);if (articleReplies == null) {articleReplies = new ArrayList<>();}return AppResult.success(articleReplies);}}
7.测试API接口
8.实现前端逻辑,完成前后端
$.ajax({type: "get",url: "/reply/getReplies?articleId=" + currentArticle.id,success: function (respData) {if (respData.code == 0) {buildArticleReply(respData.data);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
二十三.回复文章
回复文章需要进行如下两个操作
1.插入一个回复(ArticleReply表)
2.增加文章的回复数(Article表)
因为涉及到多次更新的操作,因此也需要事务操作
1.在Mapper.xml中编写SQL语句
1.Article表
已经自动生成updateByPrimaryKeySelective
2.ArticleReply表
已经自动生成insertSelective
2.在Mapper.java中定义方法
1.Article表
已经自动生成updateByPrimaryKeySelective
2.ArticleReply表
已经自动生成insertSelective
3.定义Service接口
1.Article表
void updateArticleCountById(Long id);
2.ArticleReply表
@Transactionalvoid create(ArticleReply articleReply);
4.实现Serivce接口
1.Article表
@Overridepublic void updateArticleCountById(Long id) {if (id == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Article article = selectById(id);if (article == null) {//打印日志log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));}article.setReplyCount(article.getReplyCount() + 1);article.setUpdateTime(new Date());int row = articleMapper.updateByPrimaryKeySelective(article);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
2.ArticleReply表
@Overridepublic void create(ArticleReply articleReply) {if (articleReply == null || StringUtils.isEmpty(articleReply.getContent()) ||articleReply.getPostUserId() == null || articleReply.getPostUserId() < 0 ||articleReply.getArticleId() == null || articleReply.getArticleId() < 0) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}//设置默认值articleReply.setLikeCount(0); // 点赞数articleReply.setState((byte) 0); // 状态articleReply.setDeleteState((byte) 0); // 是否删除Date date = new Date();articleReply.setCreateTime(date); // 创建时间articleReply.setUpdateTime(date); // 更新时间int row = articleReplyMapper.insertSelective(articleReply);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}//更新文章数articleService.updateArticleCountById(articleReply.getArticleId());// 打印日志log.info(ResultCode.SUCCESS.toString() + " 回复发布成功, articleId = " + articleReply.getArticleId());}
5.单元测试
1.Article表
@Testvoid updateArticleCountById() {articleService.updateArticleCountById(1L);log.info("更新成功");}
2.ArticleReply表
@Testvoid create() {ArticleReply articleReply = new ArticleReply();articleReply.setArticleId(3L);articleReply.setPostUserId(2L);articleReply.setContent("测试评论内容");articleReplyService.create(articleReply);}
更新前
更新后
6.Controller实现方法外提供API接口
@PostMapping("/create")@ApiOperation("发布回复")public AppResult<List<ArticleReply>> create(@ApiParam("文章id") @RequestParam("articleId") @NonNull Long articleId,@ApiParam("文章内容") @RequestParam("content") @NonNull String content,HttpServletRequest httpServletRequest) {HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);if (user.getState() == 1) {// 返回提示return AppResult.failed(ResultCode.FAILED_USER_BANNED);}Article article = articleService.selectById(articleId);//判断文章是否删除if (article == null || article.getDeleteState() == 1) {// 返回错误提示return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);}//判断文章状态if (article.getState() == 1) {// 返回错误提示return AppResult.failed("文章已封帖");}ArticleReply articleReply=new ArticleReply();articleReply.setContent(content);articleReply.setPostUserId(user.getId());articleReply.setArticleId(articleId);articleReplyService.create(articleReply);return AppResult.success("发布回复成功");}
7.测试API接口
8.实现前端逻辑,完成前后端
// 构造帖子对象var 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) {if (respData.code == 0) {// 提示$.toast({heading: '成功',text: respData.message,icon: 'success'});// 清空回复区域editor.setValue('');// 更新回贴数// 1. 更新全局变量中的回贴数currentArticle.replyCount = currentArticle.replyCount + 1;// 2. 更新页面的值$('#details_article_replyCount').html(currentArticle.replyCount);// 3. 重新加载回复列表loadArticleDetailsReply();} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});});
二十四.用户个人信息和文章展示
已经有根据id获取用户信息的方法,现在我们只需要根据userId获取所有文章信息的接口即可
1.在Mapper.xml中编写SQL语句
<!-- 根据用户Id查询帖子列表--><select id="selectByUserId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">selectb.id as b_id,b.name as b_name,a.id,a.boardId,a.userId,a.title,a.visitCount,a.replyCount,a.likeCount,a.state,a.deleteState,a.createTime,a.updateTimefrom t_article as a, t_board bwhere a.boardId = b.idand a.userId = #{userId,jdbcType=BIGINT}and a.deleteState = 0order by a.createTime desc</select>
2.在Mapper.java中定义方法
List<Article> selectByUserId(Long userId);
3.定义Service接口
List<Article> selectByUserId(Long userId);
4.实现Serivce接口
@Overridepublic List<Article> selectByUserId(Long userId) {if (userId == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}List<Article> articles = articleMapper.selectByUserId(userId);return articles;}
5.单元测试
@Testvoid selectByUserId() {List<Article> articles = articleService.selectByUserId(1L);log.info(articles.toString());}
6.Controller实现方法外提供API接口
@ApiOperation("获取用户文章列表")@GetMapping("/getAllByUserId")public AppResult<List<Article>> getAllByUserId(@ApiParam("用户id") @RequestParam(value = "userId", required = false) Long userId,HttpServletRequest httpServletRequest) {if (userId == null) {HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);userId = user.getId();}List<Article> articles = articleService.selectByUserId(userId);if (articles == null) {articles = new ArrayList<>();}return AppResult.success(articles);}
7.测试API接口
8.实现前端逻辑,完成前后端
profile.html页面
1.获取用户信息
$.ajax({type: 'get',url: '/user/info' + userInfoQueryString,// 成功回调success: function (respData) {if (respData.code == 0) {// 把查询到的数据设置到页面上initProfileUserInfo(respData.data);} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
2.获取用户发布的文章
$.ajax({type: "get",url: "/article/getAllByUserId" + articleListQueryString,success: function (respData) {if (respData.code == 0) {buildProfileUserArticle(respData.data);} else {// 失败(服务器处理业务逻辑失败), 提示用户错误信息$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
二十五.修改用户信息
修改用户信息,首先需要获取用户的信息,前面的接口已经实现,我们只需要在前端根据接口获取信息展示到页面上即可
1.在Mapper.xml中编写SQL语句
已经自动生成了updateByPrimaryKeySelective
2.在Mapper.java中定义方法
已经自动生成了updateByPrimaryKeySelective
3.定义Service接口
void modifyInfo(User user);
4.实现Serivce接口
@Overridepublic void modifyInfo(User user) {if (user == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}// 定义一个标识boolean checkParam = false;//更新用户User updateUser = new User();updateUser.setId(user.getId());//用户名if (!StringUtils.isEmpty(user.getUsername())) {User user1 = selectByUsername(user.getUsername());if (user1 == null) {updateUser.setUsername(user.getUsername());checkParam = true;} else {throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));}}//昵称if (!StringUtils.isEmpty(user.getNickname())) {updateUser.setNickname(user.getNickname());checkParam = true;}//性别if (user.getGender() != null) {// 设置性别updateUser.setGender(user.getGender());// 校验有效性 0女 1男 2保密if (updateUser.getGender() < 0 || updateUser.getGender() > 2) {// 处理方式1:设置成默认值// 处理方式2:设置为null, 表示保持原来的性别,不更新数据库updateUser.setGender((byte) 2);}// 更新标识checkParam = true;}//电话号码if (!StringUtils.isEmpty(user.getPhonenum())) {updateUser.setPhonenum(user.getPhonenum());checkParam = true;}//邮箱if (!StringUtils.isEmpty(user.getEmail())) {updateUser.setEmail(user.getEmail());checkParam = true;}//简介if (!StringUtils.isEmpty(user.getRemark())) {updateUser.setRemark(user.getRemark());checkParam = true;}if (checkParam == false) {throw new ApplicationException("所有信息不能同时为空");}int row = userMapper.updateByPrimaryKeySelective(updateUser);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
5.单元测试
@Testvoid modifyInfo() {User user=new User();user.setId(1L);user.setNickname("小张三");user.setRemark("我是快乐小张三");user.setGender((byte) 1);user.setPhonenum("123456");user.setEmail("159@qq.com");userService.modifyInfo(user);}
更新前
更新后
6.Controller实现方法外提供API接口
@ApiOperation("修改个人信息")@PostMapping("/modifyInfo")public AppResult<User> modifyInfo(HttpServletRequest httpServletRequest,@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) && gender == null &&StringUtils.isEmpty(email) && StringUtils.isEmpty(phoneNum) && StringUtils.isEmpty(remark)) {return AppResult.failed("要修改的信息不能全为空");}HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);User updateUser = new User();updateUser.setId(user.getId());updateUser.setUsername(username); // 用户名updateUser.setNickname(nickname); // 昵称updateUser.setGender(gender); // 性别updateUser.setPhonenum(phoneNum); // 电话updateUser.setEmail(email); // 邮箱updateUser.setRemark(remark); // 个人简介userService.modifyInfo(updateUser);user = userService.selectById(user.getId());session.setAttribute(AppConfig.SESSION_USER_KEY, user);return AppResult.success(user);}
7.测试API接口
修改前
修改后
8.实现前端逻辑,完成前后端
1.获取用户信息
$.ajax({type: 'get',url: '/user/info',// 成功回调success: function (respData) {if (respData.code == 0) {// 把查询到的数据设置到页面上initUserInfo(respData.data);} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
2.修改用户信息
$.ajax({type: "post",url: userURL,contentType: 'application/x-www-form-urlencoded',data: userInfo,// 成功回调success: function (respData) {if (respData.code == 0) {// 提示$.toast({heading: '成功',text: respData.message,icon: 'success'});// 修改个人信息成功后,更新页面的用户昵称if (type == 1) {let user = respData.data;// 个人中心$('#settings_nickname').html(user.nickname);// 导航栏$('#index_nav_nickname').html(user.nickname);} else if (type == 2) {// 跳转到登录页面location.assign("/sign-in.html");return;}} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
二十六.修改密码
1.在Mapper.xml中编写SQL语句
已经自动生成了updateByPrimaryKeySelective
2.在Mapper.java中定义方法
已经自动生成了updateByPrimaryKeySelective
3.定义Service接口
void modifyPassword(Long id, String newPassword, String oldPassword);
4.实现Serivce接口
@Overridepublic void modifyPassword(Long id, String newPassword, String oldPassword) {if (id < 0 || id == null || StringUtils.isEmpty(newPassword) || StringUtils.isEmpty(oldPassword)) {//打印日志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) {// 打印日志log.warn(ResultCode.FAILED_USER_NOT_EXISTS.toString() + " user id = " + id);// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS));}if (!MD5Utils.verifyOriginalAndCiphertext(oldPassword, user.getSalt(), user.getPassword())) {// 打印日志log.warn(ResultCode.FAILED_PASSWORD_CHECK.toString() + " user id = " + id + ", old password = " + oldPassword);// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PASSWORD_CHECK));}String salt = UUIDUtils.UUID_32();User updateUser = new User();updateUser.setId(user.getId());updateUser.setSalt(salt);updateUser.setPassword(MD5Utils.md5Salt(newPassword, salt));updateUser.setUpdatetime(new Date());int row = userMapper.updateByPrimaryKeySelective(updateUser);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
5.单元测试
@Testvoid modifyPassword() {userService.modifyPassword(2L, "654321","123456");log.info("更新成功");}
更新前
更新后
6.Controller实现方法外提供API接口
@ApiOperation("修改密码")@PostMapping("/modifyPwd")public AppResult modifyPassword(@ApiParam("原密码") @RequestParam("oldPassword") @NonNull String oldPassword,@ApiParam("新密码") @RequestParam("newPassword") @NonNull String newPassword,@ApiParam("确认密码") @RequestParam("passwordRepeat") @NonNull String passwordRepeat,HttpServletRequest httpServletRequest) {//判断两次输入的密码if (!newPassword.equals(passwordRepeat)) {return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);}//获取当前登录的用户信息HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);//修改密码userService.modifyPassword(user.getId(), newPassword, oldPassword);//销毁当前的sessionsession.invalidate();return AppResult.success("修改成功");}
7.测试API接口
8.实现前端逻辑,完成前后端
// 发送请求,提示响应结果$.ajax({type: "post",url: userURL,contentType: 'application/x-www-form-urlencoded',data: userInfo,// 成功回调success: function (respData) {if (respData.code == 0) {// 提示$.toast({heading: '成功',text: respData.message,icon: 'success'});// 修改个人信息成功后,更新页面的用户昵称if (type == 1) {let user = respData.data;// 个人中心$('#settings_nickname').html(user.nickname);// 导航栏$('#index_nav_nickname').html(user.nickname);} else if (type == 2) {// 提示$.toast({heading: '成功',text: respData.message,icon: 'success'});// 跳转到登录页面location.assign("/sign-in.html");return;}} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
二十七.站内信功能
这个功能涉及到的是message表
1.在Mapper.xml中编写SQL语句
直接使用z自动生成的
2.在Mapper.java中定义方法
3.定义Service接口
public interface IMessageService {void create(Message message);
}
4.实现Serivce接口
@Slf4j
@Service
public class MessageServiceImpl implements IMessageService {@Resourceprivate MessageMapper messageMapper;@Resourceprivate IUserService userService;@Overridepublic void create(Message message) {if (message == null || StringUtils.isEmpty(message.getContent()) || message.getPostUserId() == null || message.getReceiveUserId() == null) {//打印日志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.setCreateTime(new Date());message.setUpdateTime(new Date());message.setState((byte) 0);//发送消息int row = messageMapper.insertSelective(message);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
}
5.单元测试
@SpringBootTest
@Slf4j
class MessageServiceImplTest {@ResourceIMessageService messageService;@Testvoid create() {Message message=new Message();message.setPostUserId(1L);message.setReceiveUserId(2L);message.setContent("hello,路飞");messageService.create(message);log.info("插入成功");}
}
更新前
更新后
6.Controller实现方法外提供API接口
@RestController
@RequestMapping("/message")
@Slf4j
@Api(tags = "消息接口")
public class MessageController {@Resourceprivate IMessageService messageService;@Resourceprivate IUserService userService;@PostMapping("/create")@ApiOperation("发送消息")public AppResult create(@ApiParam("消息内容") @RequestParam("content") @NonNull String content,@ApiParam("消息接收方") @RequestParam("receiveUserId") @NonNull Long receiveUserId,HttpServletRequest httpServletRequest) {if (StringUtils.isEmpty(content) || receiveUserId == null) {return AppResult.failed("内容和接收者id不能为null");}HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);//校验用户是否存在if (user == null || user.getDeletestate() == null) {return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);}//校验用户状态if (user.getState() == 1) {return AppResult.failed(ResultCode.FAILED_USER_BANNED);}if (user.getId() == receiveUserId) {return AppResult.failed("不能给自己发送消息");}//查询接受者User receiveUser = userService.selectById(receiveUserId);//校验接受者状态if (receiveUser == null || receiveUser.getDeletestate() == 1) {return AppResult.failed("接收者状态异常");}//构建message对象Message message = new Message();message.setContent(content);message.setPostUserId(user.getId());message.setReceiveUserId(receiveUserId);messageService.create(message);return AppResult.success("发送成功");}}
7.测试API接口
发送前
发送后
8.实现前端逻辑,完成前后端
在index.html页面
// 发送站内信请求 url = message/send, 成功与失败都调用cleanMessageForm()方法,清空输入框$.ajax({type: "post",url: "/message/create",contentType: 'application/x-www-form-urlencoded',data: postData,success: function (respData) {if (respData.code == 0) {//清空cleanMessageForm()// 提示$.toast({heading: '成功',text: respData.message,icon: 'success'});} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
二十八.查看未读数
1.在Mapper.xml中编写SQL语句
<?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.javastudy.forum.dao.MessageMapper"><select id="selectByNum" resultType="java.lang.Long" parameterType="java.lang.Long">select count(*)from t_messagewhere state = 0and deleteState = 0and receiveUserId = #{userId,jdbcType=BIGINT};</select></mapper>
2.在Mapper.java中定义方法
Long selectByNum(Long userId);
3.定义Service接口
Long selectUnreadCount(Long userId);
4.实现Serivce接口
@Overridepublic Long selectUnreadCount(Long userId) {if (userId == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}User user = userService.selectById(userId);if (user == null || user.getDeletestate() == 1) {throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Long num = messageMapper.selectByNum(userId);return num;}
5.单元测试
@Testvoid selectUnreadCount() {Long aLong = messageService.selectUnreadCount(1L);log.info("id=1的用户未读数:"+aLong);}
6.Controller实现方法外提供API接口
@GetMapping("/getUnreadCount")@ApiOperation("获取未读数")public AppResult<Long> getUnreadCount(HttpServletRequest httpServletRequest) {HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);//检验用户if (user == null || user.getDeletestate() == 1) {return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);}Long num = messageService.selectUnreadCount(user.getId());return AppResult.success(num);}
7.测试API接口
8.实现前端逻辑,完成前后端
// ============ 获取用户未读站内信数量 ============// 成功后,处理小红点是否显示 #index_nva_message_badgefunction requestMessageUnreadCount() {$.ajax({type: "get",url: "/message/getUnreadCount",success: function (respData) {if (respData.code == 0) {var messageBadgeEl = $('#index_nva_message_badge');if (respData.data > 0) {messageBadgeEl.show();} else {messageBadgeEl.hide();}} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});}
二十九.获取消息列表
在获取消息的时候,我们也需要发送者的用户信息,因此在message实体类中加上一个属性
private User postUser;
1.在Mapper.xml中编写SQL语句
<resultMap id="AllInfoResultMap" type="com.javastudy.forum.model.Message" extends="BaseResultMap"><!-- 扩展⽤⼾信息结果, 注意查询结果列名的前缀为 u_ --><association property="postUser" resultMap="com.javastudy.forum.dao.UserMapper.BaseResultMap"columnPrefix="u_"></association></resultMap><select id="selectByReceiveUserId" resultMap="AllInfoResultMap" parameterType="java.lang.Long">select u.id as u_id,u.avatarurl as u_avatarurl,u.nickname as u_nickname,u.gender as u_gender,m.id,m.postUserId,m.receiveUserId,m.content,m.state,m.createTime,m.updateTimefrom t_message m,t_user uwhere m.postUserId = u.idand m.deleteState = 0and m.receiveUserId = #{receiveUserId,jdbcType=BIGINT}order by m.createTime desc, m.state asc</select>
2.在Mapper.java中定义方法
List<Message> selectByReceiveUserId(Long receiveUserId);
3.定义Service接口
List<Message> selectById(Long id);
4.实现Serivce接口
@Overridepublic List<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));}List<Message> messages = messageMapper.selectByReceiveUserId(id);return messages;}
5.单元测试
@Testvoid selectById() {List<Message> messages = messageService.selectById(1L);log.info(messages.toString());}
6.Controller实现方法外提供API接口
@GetMapping("/getAll")@ApiOperation("查询用户的所有站内信")public AppResult<List<Message>> getAll(HttpServletRequest httpServletRequest) {HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);//检验用户if (user == null || user.getDeletestate() == 1 || user.getId() == null) {return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);}List<Message> messages = messageService.selectById(user.getId());return AppResult.success(messages);}
7.测试API接口
8.实现前端逻辑,完成前后端
// ============ 获取用户所有站内信 ============function requestMessageList() {$.ajax({type:"get",url:"/message/getAll",success: function (respData) {if (respData.code == 0) {buildMessageList(respData.data);} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});}
三十.站内信已读状态更新
1.在Mapper.xml中编写SQL语句
使用已经生成的updateByPrimaryKeySelective
2.在Mapper.java中定义方法
使用已经生成的updateByPrimaryKeySelective
3.定义Service接口
void updateStateById(Long id,Byte state);
4.实现Serivce接口
@Overridepublic void updateStateById(Long id, Byte state) {// 非空校验, state : 0 未读 1 已读 2 已回复if (id == null || state == null || id < 0 || state < 0 || state > 2) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Message message = new Message();message.setId(id);message.setState(state);message.setUpdateTime(new Date());int row = messageMapper.updateByPrimaryKey(message);if (row != 1) {// 打印日志log.warn(ResultCode.ERROR_SERVICES.toString());// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));}}
5.单元测试
@Testvoid updateStateById() {messageService.updateStateById(1L, (byte) 1);log.info("更新成功");}
更新前
更新后
6.Controller实现方法外提供API接口
@PostMapping("/markRead")@ApiOperation("更新为已读")public AppResult markRead(HttpServletRequest httpServletRequest,@ApiParam("消息id") @RequestParam("id") Long id) {Message message = messageService.selectById(id);//校验消息的状态if (message == null || message.getDeleteState() == 1) {return AppResult.failed(ResultCode.FAILED_MESSAGE_NOT_EXISTS);}HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);//校验接受者是否和当前用户一致if (user.getId() != message.getReceiveUserId()) {return AppResult.failed(ResultCode.FAILED_FORBIDDEN);}messageService.updateStateById(id, (byte) 1);return AppResult.success("更改状态成功");}
7.测试API接口
更新前
更新后
8.实现前端逻辑,完成前后端
// 发送请求,更新状态为已读if (messageItem.state == 0 && statusDotEl.hasClass('status-dot-animated bg-red')) {$.ajax({type: 'post',url: '/message/markRead',contentType: 'application/x-www-form-urlencoded',data: { id: messageItem.id },// 成功回调success: function (respData) {if (respData.code == 0) {// 更新页面显示效果和messageItem.statestatusDotEl.removeClass('status-dot-animated bg-red');// 修改未读为已读statusDescEl.html('[已读]');// 修改本地的对象状态属性messageItem.state = 1;}}});}
三十一.站内信回复功能
1.在Mapper.xml中编写SQL语句
使用已经生成的updateByPrimaryKeySelective
2.在Mapper.java中定义方法
使用已经生成的updateByPrimaryKeySelective
3.定义Service接口
void reply(Long repliedId, Message message);
4.实现Serivce接口
@Overridepublic void reply(Long repliedId, Message message) {if (repliedId == null || repliedId < 0 || message == null) {//打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());//抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}Message repliedMessage = selectById(repliedId);if (repliedMessage == null || repliedMessage.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);}
5.单元测试
@Testvoid reply() {Message message = new Message();message.setContent("我也爱你");message.setPostUserId(1L);message.setReceiveUserId(2L);messageService.reply(2L, message);}
更新前
更新后
6.Controller实现方法外提供API接口
@PostMapping("/reply")@ApiOperation("回复站内信")public AppResult reply(HttpServletRequest httpServletRequest,@ApiParam("要回复的站内信Id") @RequestParam("repliedId") @NonNull Long repliedId,@ApiParam("站内信的内容") @RequestParam("content") @NonNull String content) {//校验当前用户状态HttpSession session = httpServletRequest.getSession(false);User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);if (user.getState() == 1) {// 返回错误描述return AppResult.failed(ResultCode.FAILED_USER_BANNED);}//校验要回复的站内信状态Message message = messageService.selectById(repliedId);if (message == null || message.getDeleteState() == 1) {return AppResult.failed(ResultCode.FAILED_MESSAGE_NOT_EXISTS);}// 不能给自己回复if (user.getId() == message.getPostUserId()) {// 返回错误描述return AppResult.failed("不能回复自己的站内信");}//构造对象Message postMessage = new Message();postMessage.setContent(content);postMessage.setUpdateTime(new Date());postMessage.setCreateTime(new Date());postMessage.setPostUserId(user.getId());postMessage.setReceiveUserId(message.getPostUserId());messageService.reply(repliedId, postMessage);return AppResult.success("回复成功");}
7.测试API接口
更新前
更新后
8.实现前端逻辑,完成前后端
$.ajax({type: "post",url: "/message/reply",contentType: 'application/x-www-form-urlencoded',data: postData,success: function (respData) {if (respData.code == 0) {// 回复成功后刷新未读标识和站内信列表requestMessageUnreadCount();requestMessageList();// 清空输入区cleanMessageReplyForm();// 提示$.toast({heading: '成功',text: respData.message,icon: 'success'});} else {// 失败$.toast({heading: '警告',text: respData.message,icon: 'warning'});}},// 失败 (HTTP)error: function () {$.toast({heading: '错误',text: '访问出现问题,请联系管理员',icon: 'error'});}});
三十二.项目的部署与发布
1.部署linux服务器
1.创建数据库
可以将sql语句变成一个文件,然后执行下面的
source /root/java78/table.sql
2.将代码打包
打包完成之后,将jar包拖拽到linux服务器上
3.运行代码
后台启动项目
nohup java -jar Blog_Spring-0.0.1-SNAPSHOT.jar &
查看日志
cd logs/
tail -f spring.log
终止当前的服务
ps -ef | grep [ ]
kill -9 pid
注意:如果开启多个服务,需要开端口,给防火墙添加端口号
查看防火墙状态(如果没开启,建议开启,不开启可以直接访问,开启了需要进行已下的操作访问)
systemctl status firewalld
启动防火墙和关闭防火墙
systemctl start firewalld
systemctl stop firewalld
查看开放的端口号
firewall-cmd --list-ports
开启8080端口
firewall-cmd --permanent --add-port=8080/tcp
重启防火墙
firewall-cmd --reload
设置开机启动
systemctl enable firewalld
添加安全组
2.访问测试
访问地址:比特论坛 - 用户登录
待拓展的功能
头像上传
分页展示
记录用户点赞的帖子
回复楼中楼
回复站内信楼中楼
热帖+Redis缓存
站内信及时通知
用户权限管理
版块管理
用户管理
最后.项目描述
论坛项目围绕帖子与用户两个核心角色进行业务处理,大家要能够理清每个功能的业务流程,并正确的写出实现代码。
在描述项目时,建议先介绍一下基本功能,比如对帖子的操作(增删改查),点赞,回复,对用户信息的修改,个人中心等,再具体描述技术实现过程,一般采用的格式为:用XXX技术实现了XXX功能,达到了XXX效果,以下是几个示例:
- 使用统一返回格式+全局错误信息定义处理前后端交互时的返回结果
- 使用@ControllerAdvice+@ExceptionHandler实现全局异常处理
- 使用拦截器实现用户登录校验
- 使用MybatisGeneratorConfig生成常的增删改查方法
- 集成Swagger实现自动生成API测试接口
- 使用jQuery完成AJAX请求,并处理HTML页面标签
- 对数据库中常用的查询字段建立索引,并使用查询计划分析索引是否生效