论坛系统(中-2)

发布于:2025-05-20 ⋅ 阅读:(12) ⋅ 点赞:(0)

软件开发

实现业务功能

个人中心

页面结构介绍

个人中心的页面结构分为三部分

1> 导航栏

2> 正文部分

3> 页脚部分

index.html 的页面结构

1> 导航栏

2> 正文部分

3> 页脚部分

 获取用户信息

实现逻辑

⽤⼾提交请求,服务器根据是否传⼊Id参数决定返回哪个⽤⼾的详情

1. 不传⽤⼾Id,返回当前登录⽤⼾的详情 从session中获取

2. 传⼊⽤⼾Id,返回指定Id的⽤⼾详情 根据传入的ID从数据库中查询

参数要求
参数名 描述 类型 默认值 条件
id ⽤⼾Id long 可以为空

接口规范

// 请求
GET /user/info HTTP/1.1
GET /user/info?id=1 HTTP/1.1
// 响应
HTTP/1.1 200
Content-type: applicatin/json

// 下面返回的是具体的User对象
{
"code": 0,
"message": "成功",
"data": {
"id": 25,
"username": "user223",
"nickname": "user223",
"phoneNum": null,
"email": null,
"gender": 1,
"avatarUrl": null,
"articleCount": 0,
"isAdmin": 0,
"state": 0,
"createTime": "2023-04-08 15:06:10",
"updateTime": "2023-04-08 15:06:10"
}
}

 编写后端代码
1. 在Mapper.xml中编写SQL语句

之前就生成好了


2. 在Mapper.java中定义方法

也是之前生成好的


3. 定义Service接口

在IUserService定义selectById⽅法,如下



4. 实现Service接口

 1> 非空校验

2> 调用DAO查询数据库并且获取对象
具体代码

    @Override
    public User selectById(Long id) {
        // 1. 非空校验
        if (id == null) {
            // 打印日志
            // 参数校验失败
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 2. 调用DAO查询数据库并获取对象
        User user = userMapper.selectByPrimaryKey(id);
        // 返回结果
        return user;
    }
5. 单元测试

查询成功

6. Controller实现方法并对外提供API接口

步骤

1> 根据id的值来判断是通过那种方式来获取User对象

2> 如果id为空就从session中进行获取用户信息

3> 如果id不为空, 就用id去调用service通过id来查询用户信息

4> 判断返回的用户对象是不是空的

5> 返回正确的结果

具体代码

    @Operation(summary = "获取用户信息")
    @GetMapping("/info")
    public AppResult<User> getUserInfo(HttpServletRequest request,
                                       @Parameter(description = "用户id") @RequestParam(value = "id", required = false) Long id) {
        // 定义返回的User对象
        User user = null;
        // 根据Id的值判断User对象的获取
        if (id == null) {
            // 1. 如果id为空, 从session中获取当登录的信息
            HttpSession session = request.getSession(false);// 如果session没有,不会创建新的session
            // 判断session和用户信息是否有效
            if (session == null || session.getAttribute(AppConfig.USER_SESSION) == null) {
                // 用户没有登录, 直接返回错误信息
                return AppResult.failed(ResultCode.FAILED_FORBIDDEN);// 禁止访问
            }
            // 有效, 就从session中获取当前登录的用户信息
            user = (User) session.getAttribute(AppConfig.USER_SESSION);
        } else {
            // 2. 如果id不为空, 那么就从数据库中按照Id查询用户信息
            user = userService.selectById(id);
        }
        // 判断返回的用户对象是不是空的
        if (user == null) {
            return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);//用户不存在
        }
        // 返回正确的结果
        return AppResult.success(user);
    }
}

7. 测试API接口

分别测试登录和没登陆带id的情况

我们观察返回结果,发现我们的用户名, 密码, 创建跟新事件, 删除字段都返回了, 这个是不对的, 会造成信息泄露, 因此,我们需要进行处理

8. 修复返回值存在的缺陷

通过观察登录成功的返回结果发现,⽤⼾信息中的password, salt, deletState不应该返回给前

台,在User类中的对应属性上加@JsonIgnore(指定类中的属性不参与JSON字符串)注解,可以使对应的字段不参与JSON的序列化

序列化: JAVA对象--> JSON字符串

反序列化: JSON字符串-->JAVA对象

把@JsonIgnore过滤字段

对于日期, 我们需要去yml来进行设置正确的日期格式

修改⽇期格式为yyyy-MM-dd HH:mm:ss, 在application.yml中添加配置
# 在spring下加⼊⼦节点
spring:
# JSON序列化配置
jackson:
default-property-inclusion: NON_NULL # 不为null时序列化
然后我们在对应字段上加上@JsonFormate注解

 测试: 发现密码和盐过滤掉了, 然后创建和更新时间是正确的

但是上述的配置会影响我登录的返回结果, 我的data没有了, 此时我需要对统一返回结果进行强制序列化返回

 加上@JsonInclude注解, 设置不管什么情况都会进行序列化返回

再进行登录测试: 发现data成功显示

 

编写前端代码

根据后端的响应数据, 来显示在前端

编写ajax请求

//========================= 获取用户信息 =======================
        // 成功后,手动设置用户信息
        // $('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
        $.ajax({
            // 请求的方法
            type: 'get',
            // 没有参数,表示获取当前登录用户的信息
            url: 'user/info',
            // 成功回调
            success: function (respData) {
                // 判断响应的状态码
                if (respData.code == 0) {
                    // 设置页面上用户的信息
                    let user = respData.data;
                    // 判断用户头像是否有效
                    if (!user.avatarUrl) {
                        // 设置默认的头像地址
                        user.avatarUrl = avatarUrl;
                    }
                    // 设置页面上的头像
                    $('#index_nav_avatar').css('background-image', 'url(' + user.avatarUrl + ')');
                    // 用户昵称
                    $('#index_nav_nickname').html(user.nickname);
                    // 设置用户组
                    let subName = user.isAdmin == 1 ? '管理员' : '普通用户';
                    $('#index_nav_name_sub').html(subName);
                    currentUserId = user.id;

                } else {
                    // 提示信息
                    $.toast({
                        heading: '警告',
                        text: respData.message,
                        icon: 'warning'
                    });
                }
            },
            // 失败回调
            error: function () {
                // 提示信息
                $.toast({
                    heading: '错误',
                    text: '访问出现问题,请与管理员联系.',
                    icon: 'error'
                });
            }
        });

成功显示

 退出功能

实现流程

• 流程⾮常简单,具体的实现逻辑如下:

1. ⽤⼾访问退出接⼝

2. 服务器注销Session

3. 返回成功或失败

4. 如果返回成功浏览器跳转到相应⻚⾯

5. 结束

• 退出后跳转到哪个⻚⾯交给前端⾃⼰处理,建议跳转到登录⻚⾯。
 

接口规范

// 请求
GET http://127.0.0.1:58080/user/logout HTTP/1.1
// 响应
HTTP/1.1 200
Content-Type: application/json
{"code":0,"message":"成功","data":null}

后端代码编写

销毁session对象并且解绑所有的用户数据

具体代码

 @PostMapping("/logout")
    @Operation(summary = "用户退出")
    public AppResult logout (HttpServletRequest request){
        // 获取session对象
        HttpSession session = request.getSession();
        if(session != null){
            // 打印日志
            log.info("退出成功");
            // 注销session
            session.invalidate();
        }
        // 退出成功响应
        return AppResult.success();

    }
前端代码编写

我们需要对a标签绑定事件

编写ajax请求

        $('#index_user_logout').click(function () {
            $.ajax({
                type: 'get',
                url: 'user/logout',
                complete: function () {
                    // 当前请求完成时,不论成功与失败都跳转到登录页面
                    location.assign('/sign-in.html');
                }
            });
        });

 complete的解释

测试

配置拦截器

为什么要配置拦截器

为了达到封装复用的效果, 不每一个接口都进行判断session是否有效, 统一的校验工作抽取出来, 用拦截器对请求进行过滤(除了登录之外的的url都不准进行访问)

在interceptor包下创建LoginInterceptor

自定义登录拦截器

1> 类上实现HanlderInterceptor接口

2> 重写前置处理方法preHandle

3> 获取session对象, 并对session进行有效性验证

4> 验证不通过就跳转到登录页面, response.sendRedirect

package org.xiaobai.forum.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.xiaobai.forum.config.AppConfig;

/**
 * 登录拦截器
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {


    @Value("${forum.login.url}")
    private String defaultURL;

    /**
     *  前置处理(对请求进行预处理)
     * @param request
     * @param response
     * @param handler
     * @return true: 继续流程<br/> false: 流程终端
     * @throws Exception
     */

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取session 对象
        HttpSession session = request.getSession(false);// 获取不到session也不创建新的session
        // 判断session是否有效
        if (session != null && session.getAttribute(AppConfig.USER_SESSION) != null) {
            // 用户为已经登录, 校验通过
            return true;
        }
        // 校验url是否正确
        if (!defaultURL.startsWith("/")) {
            defaultURL = "/" + defaultURL;
        }
        // 校验不通过, 跳转到登录页面
        response.sendRedirect(defaultURL);
        // 终止流程
        return false;
    }
}

注意 

1> 对url进行有效性判断

2> 使用yml把目标页面放在配置文件中, 降低耦合性


修改application.yml配置⽂件,添加跳转页面
# 项⽬⾃定义相关配置
forum:
  login:
    url: sign-in.html # 未登录状况下强制跳转⻚⾯
在interceptor包下创建AppInterceptorConfigurer

步骤

1> 类上实现WebMvcConfigurer

2> 注入自定义的登录拦截器

3> 添加登录拦截器

4> 添加拦截的路径

具体代码

package org.xiaobai.forum.interceptor;

import jakarta.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 *  注册拦截器
 *  配置拦截规则
 *
 */
// 表示一个配置类, 注入到spring
@Configuration
public class AppInterceptorConfigurer implements WebMvcConfigurer {

    //注入自定义的登录拦截器
    @Resource
    private LoginInterceptor loginInterceptor;

    // 配置拦截路径
    @Override
    public void addInterceptors(InterceptorRegistry registry) { WebMvcConfigurer.super.addInterceptors(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("/js/**")
                .excludePathPatterns("/**.ico");
    }
}

获取在首页中显示的板块

实现方式

• 在⾸⻚显⽰的版块信息,可以通过以下两种⽅式解决

• ⽅法⼀:单独提供查询前N条记录的接⼝,⽤来控制⾸⻚中版块的个数(按照sort进行升序排序)

• ⽅法⼆:通过配置表中state字段来实现,提供查询所有版块的接⼝

• 两种⽅式任选⼀个都可以,项⽬中使⽤⽅法⼀

实现逻辑

1. 用户访问首页

2. 服务器查询有效的版块并按排序字段排序

3. 返回版块集合

没有参数要求

接口规范

// 请求
GET http://127.0.0.1:58080/board/topList HTTP/1.1
// 响应
HTTP/1.1 200
Content-Type: application/json
{
"code": 0,
"message": "成功",
"data": [
{
"id": 1,
"name": "Java",
"articleCount": 5,
"sort": 1,
"state": 0,
"createTime": "2023-01-14 11:02:18",
"updateTime": "2023-01-14 11:02:18"
},
{
"id": 2,
"name": "C++",
"articleCount": 1,
"sort": 2,
"state": 0,
"createTime": "2023-01-14 11:02:41",
"updateTime": "2023-01-14 11:02:41"
},
{
"id": 3,
"name": "前端技术",
"articleCount": 0,
"sort": 3,
"state": 0,
"createTime": "2023-01-14 11:02:52",
"updateTime": "2023-01-14 11:02:52"
}....}]}
 

后端代码实现
把板块信息写入
-- 写⼊版块信息数据
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语句

• 在src/main/resources/mapper/extension⽬录下新建 BoardExtMapper.xml( 注意命名空间和自动生成的.xml保持一致)

selectByNum 根据sort字段进行排序

编写mapper.xml 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xiaobai.forum.dao.BoardMapper">
    <select id="selectByNum" resultMap="BaseResultMap" parameterType="java.lang.Integer">
        select
            <include refid="Base_Column_List"/>
        from t_board
        where state = 0 and deleteState = 0
        order by sort asc
        limit 0,#{num,jdbcType=INTEGER}
    </select>
</mapper>

2. 在Mapper.java中定义方法

•org.xiaobai.forum.dao包下的BoardMapper中添加⽅法声明


3. 定义Service接口

• 在IBoradService定义⽅法,如下:

package org.xiaobai.forum.service;

import org.xiaobai.forum.model.Board;

import java.util.List;

public interface IBoradService {
    /**
     *  查询n挑记录
     * @param num 要查询的条数
     * @return
     */
    List<Board> selectByNum(Integer num);
}

4. 实现Service接口

• 在BoardServiceImpl中实现IBoardService中新增的⽅法

实现步骤

1> 注入BoardMapper

2> 对num进行非空校验

3> 调用DAO查询数据库中的数据

4> 返回结果

package org.xiaobai.forum.service.impl;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.common.ResultCode;
import org.xiaobai.forum.dao.BoardMapper;
import org.xiaobai.forum.exception.ApplicationException;
import org.xiaobai.forum.model.Board;
import org.xiaobai.forum.service.IBoradService;

import java.util.List;

@Slf4j
@Service
public class IBoardService implements IBoradService {
    // 注入boardMapper
    @Resource
    private BoardMapper boardMapper;
    @Override
    public List<Board> selectByNum(Integer num) {
        // 1. 进行非空校验
        if (num <= 0) {
            // 打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed((ResultCode.FAILED_PARAMS_VALIDATE)));// 参数校验异常
        }
        // 2. 调用DAO查询数据库中的数据
        List<Board> rs = boardMapper.selectByNum(num);
        // 3. 返回结果
        return rs;
    }
}

5. 单元测试

查询成功


6. Controller实现方法并对外提供API接口

• 在BoradController中提供对外的API接⼝

application.yml中添加配置

# 项⽬⾃定义相关配置(顶格写)

forum:

        index: # ⾸⻚配置节点

                board-num: 9 # ⾸⻚中显⽰的版块个数

实现步骤

1> 注入IBoardService 

2> 调用Service获取查询结果

3> 判断查询结果是否为空

4> 返回结果

具体代码

package org.xiaobai.forum.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.model.Board;
import org.xiaobai.forum.service.IBoradService;

import java.util.ArrayList;
import java.util.List;

@Tag(name = "板块接口", description = "主页中展示有多少个板块")
@Slf4j
@RestController
@RequestMapping("/Borad")
public class BoradController {
    // 从配置文件中读取值, 如果没有配置, 默认值为9
    @Value("${form.index.board-num:9}")
    private Integer indexBoardNum;

    @Resource
    private IBoradService boradService;

    @Operation(summary = "获取首页板块列表")
    @GetMapping("/topList")
    public AppResult<List<Board>> topList() {
        log.info("首页板块数为: " + indexBoardNum);
        // 调用Service 获取查询结果
        List<Board> boards = boradService.selectByNum(indexBoardNum);
        // 判断是否为空
        if (boards == null) {
            boards = new ArrayList<>();
        }
        // 返回结果
        return AppResult.success(boards);
    }
}

7. 测试API接口

在登录条件下访问该接口

前端代码实现 

根据后端响应值来构造板块信息

编写ajax

 $.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. ⽤⼾点击发新帖按钮,进⼊发帖⻚⾯

2. 选择版块,填⼊标题、正⽂后提交服务器

3. 服务器校验信息并写⼊数据库(文章表的insert)

4. 更新⽤⼾发帖数与版块帖⼦数(更新用户表和板块表)

5. 返回结果

3. 4要用数据库的事务

参数要求
参数名 描述 类型 默认值 条件
boardId 版块Id long 必须
title ⽂章标题 String 必须
content 帖⼦内容 String 必须
接口规范

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

实现后端代码
1. 在Mapper.xml中编写SQL语句

2. 在Mapper.java中定义方法

关于文章表的insert操作, 已经自动生成, 可以直接使用

关于更新用户表和文章表 update. 在更新用户和板块的帖子数的时候, 可以使用动态更新的方法只设置Id和要更新的帖子数量即可


3. 定义Service接口

 对于update文章表在IArticleService定义⽅法

对于update用户表在IBoardService定义⽅法更新帖子数量

更具板块id查询板块信息

对于insert, 在IUserService定义⽅法


4. 实现Service接口

实现IBoardServiceImpl

实现步骤

1> 对id进行非空校验

2> 查询相应的板块

3> 更新帖子数量(此时要重新创建一个对象, 然后把要更新的字段进行设置

4> 调用DAO, 执行更新操作

5> 判断受影响行数

/**
     * 更新板块中帖子的数量
     *
     * @param id 板块id
     */
    @Override
    public void addOneArticleCountById(Long id) {
        // 对id进行非空校验
        if (id == null || id <= 0) {
            // 打印日志
            log.warn(ResultCode.FAILED_BOARD_ARTICLE_COUNT.toString());// 更新帖子数失败
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_ARTICLE_COUNT));
        }
        // 查询对应的板块
        Board board = boardMapper.selectByPrimaryKey(id);
        if (board == null) {
            // 打印日志
            log.warn(ResultCode.ERROR_IS_NULL.toString());// 更新帖子数失败
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_IS_NULL));
        }
        // 更新帖子数量
        // 重新创建一个对象, 把要更新的字段设置进行
        Board updateBoard = new Board();
        updateBoard.setId(board.getId());
        updateBoard.setArticleCount(board.getArticleCount() + 1);// 更新帖子数量
        // 调用DAO, 执行更新操作
        int row = boardMapper.updateByPrimaryKeySelective(updateBoard); // 调用mapper更新帖子
        // 判断受影响行数
        if (row != 1) {
            log.warn(ResultCode.FAILED.toString() + ", 受影响行数不等于1 .");
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED));
        }

    }

 根据板块id查询板块信息

    @Override
    public Board selectById(Long id) {
        // 对id进行非空校验
        if (id == null || id <= 0) {
            // 打印日志
            log.warn(ResultCode.FAILED_BOARD_ARTICLE_COUNT.toString());// 更新帖子数失败
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_ARTICLE_COUNT));
        }
        Board board = boardMapper.selectByPrimaryKey(id);
        return board;
    }

动态更新需要重新创建对象 

 实现UserServiceImpl

步骤

1> 对id进行非空校验

2> 查询用户信息

3> 更新用户的发帖数量

4> 动态更新sql

5> 调用DAO, 执行更新操作

具体代码

 /**
     * 更新当前用户帖子数量
     * @param id 用户id
     */
    @Override
    public void addOneArticleCountById(Long id) {
        // 对id进行非空校验
        if (id == null || id <= 0) {
            // 打印日志
            log.warn(ResultCode.FAILED_BOARD_ARTICLE_COUNT.toString());// 更新帖子数失败
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_ARTICLE_COUNT));
        }
        // 查询用户信息
        User user = userMapper.selectByPrimaryKey(id);
        if (user == null) {
            // 打印日志
            log.warn(ResultCode.ERROR_IS_NULL.toString());// 更新用户数失败
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_IS_NULL));
        }
        // 更新用户的发帖数量
        User updateUser = new User();
        // 动态更新sql, 只需要把更新的字段进行设置即可
        updateUser.setId(user.getId());
        updateUser.setArticleCount(user.getArticleCount() + 1);
        // 调用DAO, 执行更新操作
        int row = userMapper.updateByPrimaryKeySelective(updateUser);
        if (row != 1) {
            log.warn(ResultCode.FAILED.toString() + ", 受影响行数不等于1 .");
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED));
        }
    }

实现ArticleServiceImpl
步骤

1> 把和新增文章相关的mapper注入, 把和更新用户表和板块表的文章数的service进行注入

2> 对传入的Article 进行非空校验

3> 设置文章的默认值(访问数, 回复数, 点赞数...)

4> 把设置好默认值的article插入到数据库

5> 根据article的用户id获取用户的信息

6> 对用户进行非空校验(是否找到指定的用户)

7> 如果找到了, 那就更新用户的发帖数目

8> 根据article的板块id获取板块的信息

9> 对板块进行非空校验(是否找到指定的板块)

10> 如果找到了, 那就更新板块中的发帖数

具体代码

package org.xiaobai.forum.service.impl;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.common.ResultCode;
import org.xiaobai.forum.dao.ArticleMapper;
import org.xiaobai.forum.exception.ApplicationException;
import org.xiaobai.forum.model.Article;
import org.xiaobai.forum.model.Board;
import org.xiaobai.forum.model.User;
import org.xiaobai.forum.service.IArticleService;
import org.xiaobai.forum.service.IBoardService;
import org.xiaobai.forum.service.IUserService;
import org.xiaobai.forum.utils.StringUtil;

import java.util.Date;

@Slf4j
@Service
public class ArticleServiceImpl implements IArticleService {
    // 注入mapper, 注意service之间注入不能成环
    @Resource
    private ArticleMapper articleMapper;
    @Resource
    private IUserService userService;
    @Resource
    private IBoardService boardService;

    @Override
    public void create(Article article) {
        // 进行非空校验
        if (article == null || article.getUserId() == null || article.getBoardId() == null
                || StringUtil.isEmpty(article.getTitle())
                || StringUtil.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.setDeleteState((byte) 0);
        article.setState((byte) 0);
        Date date = new Date();
        article.setCreateTime(date);
        article.setUpdateTime(date);
        // 把新写的文章写入数据库
        int articleRow = articleMapper.insertSelective(article);
        if (articleRow <= 0) {
            log.warn(ResultCode.FAILED_CREATE.toString());// 新增失败
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
        // 获取用户信息
        User user = userService.selectById(article.getUserId());
        // 没有找到指定的用户信息
        if (user == null) {
            log.warn(ResultCode.FAILED_CREATE.toString() + ", 发帖失败, user id = " + article.getUserId());// 新增失败
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
        // 更新用户的发帖数
        userService.addOneArticleCountById(user.getId());
        // 获取板块信息
        Board board = boardService.selectById(article.getBoardId());
        // 是否在数据库有对应的板块
        if (board == null) {
            log.warn(ResultCode.FAILED_CREATE.toString() + ", 发帖失败, board id = " + article.getUserId());// 新增失败
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
        }
        // 更新板块中的帖子数量
        boardService.addOneArticleCountById(board.getId());
        // 打印日志
        log.info(ResultCode.SUCCESS.toString() + ", user id = " + article.getUserId()
                + ", board if = " + article.getBoardId() + ", article id = " + article.getId() + "发帖成功");
    }
}

注意: service里不能出现service之间的循环调用


5. 单元测试

发现用户表里面的文章数量,板块表里面的文章数量都增加了

article表里面的数据增加了

同时用户表和板块表里面的文章数也增加了

测试代码

    @Resource
    private IArticleService articleService;
    @Test
    void create() {
        Article article = new Article();
        article.setUserId(1L);// asd
        article.setBoardId(1L);// java板块
        article.setTitle("测试新增文章接口");
        article.setContent("文章的内容...");
        articleService.create(article);
    }

6. Controller实现方法并对外提供API接口

• 在 ArticleController 中提供对外的API接⼝
步骤

1> 根据session获取用户

2> 判断用户是否禁言

3> 对板块进行校验(通过板块id校验板块是不是存在)

4> 构造文章对象

5> 调用service, 进行插入操作(插入后文章, 板块,用户的文章数量都会增加,因为是一个事务的)

6> 响应

具体代码

package org.xiaobai.forum.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.common.ResultCode;
import org.xiaobai.forum.config.AppConfig;
import org.xiaobai.forum.model.Article;
import org.xiaobai.forum.model.Board;
import org.xiaobai.forum.model.User;
import org.xiaobai.forum.service.IArticleService;
import org.xiaobai.forum.service.IBoardService;

@Tag(name = "文章接口", description = "对帖子进行发布,点赞,修改,删除操作...操作")
@Slf4j
@RestController
@RequestMapping("/article")
public class ArticleController {
    // 注入service
    @Resource
    private IArticleService articleService;
    @Resource
    private IBoardService boardService;

    @Operation(summary = "发布新帖")
    @PostMapping("/create")
    public AppResult create(HttpServletRequest request,
                            @Parameter(description = "板块id") @RequestParam("boardId") @NonNull Long boardId,
                            @Parameter(description = "文章标题") @RequestParam("title") @NonNull String title,
                            @Parameter(description = "文章内容") @RequestParam("content") @NonNull String content) {

        // 获取用户
        HttpSession session = request.getSession(false);
        User user = (User) session.getAttribute(AppConfig.USER_SESSION);
        // 判断用户是否禁言
        if (user.getState() == 1) {
            // 用户已禁言
            return AppResult.failed(ResultCode.FAILED_USER_BANNED);// 禁言
        }
        // 板块校验
        Board board = boardService.selectById(boardId);
        if (board == null || board.getDeleteState() == 1 || board.getState() == 1) {
            // 打印日志
            log.warn(ResultCode.FAILED_BOARD_BANNED.toString());// 板块状况异常
            // 返回响应
            return AppResult.failed(ResultCode.FAILED_BOARD_BANNED);
        }
        // 封装文章对象
        Article article = new Article();
        article.setTitle(title); //标题
        article.setContent(content); //正文
        article.setBoardId(boardId); // 板块id
        article.setUserId(user.getId());// 作者id
        // 调用Service
        articleService.create(article);
        // 响应
        return AppResult.success();
    }


}

7. 测试API接口

插入成功

实现前端代码 

editor.md实现输入文章标题内容的设置

editor.md的使用

Editor.md - 开源在线 Markdown 编辑器

获取表单数据

ajax请求

 // 构造帖子对象
      let postData = {
        boardId : boardIdEl.val(),
        title : titleEl.val(),
        content : contentEl.val()
      };
      
      // 提交, 成功后调用changeNavActive($('#nav_board_index'));回到首页并加载帖子列表
      // contentType: 'application/x-www-form-urlencoded'
      $.ajax({
        type: 'post',
        url : 'article/create',
        contentType : 'application/x-www-form-urlencoded',
        data : postData,
        // 回调
        success : function (respData) {
          if (respData.code == 0) {
            // 提示信息
            $.toast({
                heading: '成功',
                text: '发帖成功',
                icon: 'success'
            });
            // 发布成功
            changeNavActive($('#nav_board_index'));
          } else {
            // 提示信息
            $.toast({
                heading: '警告',
                text: respData.message,
                icon: 'warning'
            });
          }
        }, 
        error : function () {
          // 提示信息
          $.toast({
            heading: '错误',
            text: '访问出现问题,请与管理员联系.',
            icon: 'error'
          });
        }
      });

帖子列表

板块帖子列表

对应版块中显⽰的帖⼦列表以发布时间降序排列(点击不同的板块, 每个板块显示自己板块下所有的帖子)

不传⼊版块Id返回所有帖⼦

帖子列表中显示所有的帖子

实现逻辑

1. ⽤⼾点击某个版块或⾸⻚时,将版块Id做为参数向服务器发送请求

2. 服务器接收请求,并获取版块Id,查询对应版块下的所有帖⼦

3. 返回查询结果

参数要求
参数名 描述 类型 默认值 条件
boardId 版块Id Long

可为空(根据传入的Board来执行不同的查询

selectAll(), selectByBoardId(Long boardId)

接口规范

// 请求
// 返回指定版块下的帖⼦列表
GET http://127.0.0.1:58080/article/getAllByBoardId?boardId=1 HTTP/1.1
// 响应
HTTP/1.1 200
Content-Type: application/json
{
"code": 0,
"message": "成功",
"data": [
{
"id": 17,
"boardId": 1,
"userId": 1,
"title": "测试删除",
"visitCount": 8,
"replyCount": 1,
"likeCount": 1,
"state": 0,
"createTime": "2023-07-05 04:10:46",
"updateTime": "2023-07-05 11:22:43",
"board": {
"id": 1,
"name": "Java"
},
"user": {
"id": 1,
"nickname": "bitboy",
"phoneNum": null,
"email": null,
"gender": 1,
"avatarUrl": null
},
"own": false
},
{
"id": 15,
"boardId": 1,
"userId": 2,
"title": "单元测试",
"visitCount": 11,
"replyCount": 0,
"likeCount": 6,
"state": 0,
"createTime": "2023-07-03 11:30:36",
"updateTime": "2023-07-04 10:31:00",
"board": {
"id": 1,
"name": "Java"
},
"user": {
"id": 2,
"nickname": "bitgirl",
"phoneNum": null,
"email": null,
"gender": 2,
"avatarUrl": null
},
"own": false
},
{
"id": 11,
"boardId": 1,
"userId": 1,
"title": "testtest222",
"visitCount": 4,
"replyCount": 0,
"likeCount": 0,
"state": 0,
"createTime": "2023-07-02 09:19:00",
"updateTime": "2023-07-02 09:19:00",
"board": {
"id": 1,
"name": "Java"
},
"user": {
"id": 1,
"nickname": "bitboy",
"phoneNum": null,
"email": null,
"gender": 1,
"avatarUrl": null
},
"own": false
},
{
"id": 10,
"boardId": 1,
"userId": 1,
"title": "测试⻚⾯发帖",
"visitCount": 1,
"replyCount": 1,
"likeCount": 0,
"state": 0,
"createTime": "2023-07-02 09:17:47",
"updateTime": "2023-07-05 10:51:43",
"board": {
"id": 1,
"name": "Java"
},
"user": {
"id": 1,
"nickname": "bitboy",
"phoneNum": null,
"email": null,
"gender": 1,
"avatarUrl": null
},
"own": false
},
{
"id": 1,
"boardId": 1,
"userId": 1,
"title": "单元测试",
"visitCount": 13,
"replyCount": 2,
"likeCount": 3,
"state": 0,
"createTime": "2023-07-02 06:46:32",
"updateTime": "2023-07-05 10:16:43",
"board": {
"id": 1,
"name": "Java"
},
"user": {
"id": 1,
"nickname": "bitboy",
"phoneNum": null,
"email": null,
"gender": 1,
"avatarUrl": null
},
"own": false}]}

显示首页所有列表的后端代码实现
分析sql

 结果必须包含作者信息, 帖子简要信息, 因此我们需要把俩个表关联起来

1. 在Mapper.xml中编写SQL语句

创建ArticleExtMapper.xml并写内容

当前这个结果集没有一个对应的ResultMap或者ResultType接收它, 所以要自定义一个拓展的ResultMap

不同map的关系分析

具体的实现(在Article实体类里面写User这个字段


2. 在Mapper.java中定义方法

在dao层定义方法


3. 定义Service接口

在 IArticleService 添加selectAll方法


4. 实现Service接口

在ArticleServiceImpl里面实现selectAll方法

直接查询mapper里面的数据,返回结果即可

    /**
     * 查询所有的帖子列表
     * @return
     */

    @Override
    public List<Article> selectAll() {
        // 调用DAO
        List<Article> articles = articleMapper.selectAll();
        // 返回结果
        return articles;
    }

注意, 为了防止我们的avatarUrl因为为null而被我们的设置的json序列化条件给过滤掉, 我们也要加上@JsonInclude,注解设置不管为不为null都要进行序列化


5. 单元测试

查询出来了所有的帖子信息


6. Controller实现方法并对外提供API接口

在ArticleController中提供对外的API接⼝

    /**
     * 通过查询板块id返回所有帖子信息
     * @param boardId
     * @return
     */
    @Operation(summary = "获取帖子列表")
    @GetMapping("/getAllByBoardId")
    public AppResult<List<Article>> getAllByBoardId(@Parameter(description = "板块id") @RequestParam(value = "boardId",required = false)Long boardId){
        // 查询所有的帖子
        List<Article> articles = articleService.selectAll();
        if(articles == null){
            // 如果结果集为空, 那么就创建一个空集合
            articles = new ArrayList<>();
        }
        // 返回响应结果
        return AppResult.success(articles);
    }

注意: article==null, 此时article是"null"  articles = new ArrayList<>(); 里面的值是[] 


7. 测试API接口

查询成功

前端代码实现

article_list.html

编写ajax请求

$.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'
        });
      }

    });

实现成功, 但是我们要的是不同板块显示该板块的帖子和板块信息, 需要把上面的前后端都进行小改一下 

显示对应板块显示该板块下的所有帖子后端代码实现
1. 在Mapper.xml中编写SQL语句

在selectAll的基础上加上板块的条件

在AriticleExMapper.xml中添加下面代码

    <!--   根据板块ID查询所有未被删除的帖子列表, 不包含content-->
    <select id="selectAllByBoardId" resultMap="AllInfoBaseResultMap" parameterType="java.lang.Long">
        select
            u.id u_id,
            u.avatarUrl u_avatarUrl,
            u.nickname u_nickname,
            a.id,
            a.boardId,
            a.userId,
            a.title,
            a.visitCount,
            a.replyCount,
            a.likeCount,
            a.state,
            a.createTime,
            a.updateTime
        from t_article a,
             t_user u
        where a.userId = u.id
        and a.deleteState = 0
        and a.boardId = #{boardId,jdbcType=BIGINT}
        order by a.createTime desc;
    </select>

2. 在Mapper.java中定义方法

3. 定义Service接口

在IArticleService里面定义接口


4. 实现Service接口

在ArticleServiceImpl里面实现接口

实现步骤

1> 对boardId进行非空校验

2> 根据boardId查询板块信息

3> 对板块信息进行非空校验

4> 不为空就调用mapper, 通过boardId把所有和板块相关的帖子进行返回

具体代码

    /**
     *  根据板块id查询所有的帖子列表
     * @param boardId 板块id
     * @return
     */
    @Override
    public List<Article> selectAllByBoardId(Long boardId) {
        // 非空校验
        if(boardId == null || boardId <=0){
            // 打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());// 参数校验失败
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 校验板块是否存在
        Board board = boardService.selectById(boardId);
        // 非空校验
        if(board == null){
            // 打印日志
            log.warn(ResultCode.FAILED_BOARD_NOT_EXISTS.toString()+", board id = "+boardId);
            // 抛出异常
            throw new ApplicationException(AppResult.failed((ResultCode.FAILED_BOARD_NOT_EXISTS)));
        }
        // 调用DAO, 进行查询
        List<Article> articles = articleMapper.selectAllByBoardId(boardId);
        // 返回结果
        return articles;
    }

5. 单元测试

成功返回了板块的文章信息


6. Controller实现方法并对外提供API接口

实现的具体步骤

1> 根据是否传入boardId来进行分情况讨论

用户如果传入boardId, 则根据boardId查询板块下的所有帖子

用户如果没有传入boardId, 查询的是所有帖子

2> 对帖子列表集合进行非空校验

3> 返回响应结果

代码

    /**
     * 通过查询板块id返回所有帖子信息
     * @param boardId
     * @return
     */
    @Operation(summary = "获取帖子列表")
    @GetMapping("/getAllByBoardId")
    public AppResult<List<Article>> getAllByBoardId(@Parameter(description = "板块id") @RequestParam(value = "boardId",required = false)Long boardId){

        List<Article> articles;
        if(boardId == null){
            // 查询所有查询所有的帖子
            articles = articleService.selectAll();
        }else {
            // 有boardId就查询和boardId相关的帖子集合
            articles = articleService.selectAllByBoardId(boardId);
        }
        // 判断结果是否为空
        if(articles == null){
            // 如果结果集为空, 那么就创建一个空集合
            articles = new ArrayList<>();
        }
        // 返回响应结果
        return AppResult.success(articles);
    }

7. 测试API接口

成功返回指定的板块信息

编写前端代码

 article_list.html

后端是根据是否传入boardId来进行返回不同的板块信息, 现在的问题是怎么把boardId加入到url, 并且确定这个参数值

实现流程

ajax请求

成功显示

获取指定板块信息

实现逻辑

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

参数要求
参数名 描述 类型 默认值 条件
id 版块Id long 必须
接口规范

// 请求
GET http://127.0.0.1:58080/board/getById?id=1 HTTP/1.1
// 响应
HTTP/1.1 200
Content-Type: application/json
{
"code": 0,
"message": "成功",
"data": {
"id": 1,
"name": "Java",
"articleCount": 5,
"sort": 1,
"state": 0,
"createTime": "2023-01-14 11:02:18",
"updateTime": "2023-01-14 11:02:18"
}
}

后端代码实现
1. 在Mapper.xml中编写SQL语句

2. 在Mapper.java中定义方法

3. 定义Service接口

4. 实现Service接口

5. 单元测试

6. Controller实现方法并对外提供API接口

实现步骤

1> 直接调用service

2> 对查看结果进行校验

3> 返回结果

具体代码

    @Operation(summary = "获取板块信息")
    @GetMapping("/getById")
    public AppResult<Board> getById(@Parameter(description = "板块Id") @RequestParam("id") @NonNull Long id){
        // 因为id的非空校验已经由lombook给校验过了, 因此直接调用service
        Board board = boradService.selectById(id);
        // 对查看结果进行校验
        // 表示板块记录在数据库中不存在, 或者已删除状态
        if(board == null || board.getDeleteState()==1){
            // 打印日志
            log.warn(ResultCode.FAILED_NOT_EXISTS.toString());
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
        }
        // 返回结果
        return AppResult.success(board);
    }

7. 测试API接口

成功返回结果

前端代码实现

在相应的板块上添加帖子的数量

编写ajax

  $.ajax({
                type : 'get',
                url : 'board/getById?id=' + boardId,
                // 回调
                success : function (respData) {
                    if(respData.code == 0) {
                        let board = respData.data;
                        // 成功时,更新页面的内容
                        console.log(board)
                        console.log(board.name)
                        console.log(board.articleCount)
                        $('#article_list_board_title').html(board.name); // 版块名
                        $('#article_list_count_board').html('帖子数量: ' +board.articleCount);
                    } else {
                        // 失败
                        $.toast({
                            heading: '警告',
                            text: respData.message,
                            icon: 'warning'
                        });
                    }
                },
                error : function () {
                    // 提示信息
                    $.toast({
                        heading: '错误',
                        text: '访问出现问题,请与管理员联系.',
                        icon: 'error'
                    });
                }

            });

实现结果

获取帖子详情

实现逻辑

1. ⽤⼾点击帖⼦,将帖⼦Id做为参数向服务器发送请求

2. 服务器查询帖⼦信息

3. 帖⼦访问次数加1

4. 返回查询结果

参数要求
参数名 描述 类型 默认值 条件
id 帖⼦Id long 必须
接口规范

// 请求
GET http://127.0.0.1:58080/article/getById?id=1 HTTP/1.1
// 响应
HTTP/1.1 200
Content-Type: application/json
{
"code": 0,
"message": "成功",
"data": {
"id": 1,
"boardId": 1,
"userId": 1,
"title": "单元测试",
"visitCount": 14,
"replyCount": 2,
"likeCount": 3,
"state": 0,
"createTime": "2023-07-02 06:46:32",
"updateTime": "2023-07-05 10:16:43",
"content": "测试内容",
"board": {
"id": 1,D
"name": "Java"
},
"user": {
"id": 1,
"nickname": "bitboy",
"phoneNum": null,
"email": null,
"gender": 1,
"avatarUrl": null
},
"own": true
}
}

帖子详情后端代码编写
1. 在Mapper.xml中编写SQL语句

根据帖子Id查询帖子详情

在selectAll的基础上加入过滤条件, 帖子的id是用户点击的板块的帖子id,也要把板块也进行显示,就可以满足需求

ArticleExtmapper.xml

<!--    自定义结果集映射-->
    <resultMap id="AllInfoBaseResultMap" type="org.xiaobai.forum.model.Article" extends="ResultMapWithBLOBs">
<!--    关联的用户的映射-->
        <association property="user" resultMap="org.xiaobai.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_"/>
<!--    关联板块的映射-->
        <association property="board" resultMap="org.xiaobai.forum.dao.BoardMapper.BaseResultMap"
                     columnPrefix="b_">
        </association>
    </resultMap>    
<select id="selectById" resultMap="AllInfoBaseResultMap">
        SELECT
            u.id AS u_id,
            u.avatarUrl AS u_avatarUrl,
            u.nickname AS u_nickname,
            u.gender AS u_gender,
            u.isAdmin AS u_isAdmin,
            u.state AS u_state,
            u.deleteState AS u_deleteState,
            b.id AS b_id,
            b.name AS b_name,
            b.state As b_state,
            b.deleteState AS b_deleteState,
            a.id,
            a.boardId,
            a.userId,
            a.title,
            a.content,
            a.visitCount,
            a.replyCount,
            a.likeCount,
            a.state,
            a.createTime,
            a.updateTime
        FROM
            t_article a,
            t_user u,
            t_board b
        WHERE
            a.userId = u.id
          AND a.deleteState = 0
          AND a.id = #{id,jdbcType=BIGINT}
          AND a.boardId = b.id

    </select>

注意:

1> 根据帖子id查询帖子详情

2> 补充要返回的结果, 并且要返回的信息包含板块, 因此要进行三表关联

3> 定义关联对象


2. 在Mapper.java中定义方法


3. 定义Service接口

为了避免歧义,修改一下xml的id名字, selectDetailById

在IArticleService里面添加方法


4. 实现Service接口

实现步骤

1> 对id进行非空校验

2> 调用DAO, 根据用户id查询返回文章信息

3> 创建一个新的帖子对象, 然后更新里面的访问次数

4> 把新的帖子对象更新到数据库

5> 更新原始返回对象的访问次数

6> 返回原始对象

具体代码

 /**
     * 根据帖子Id查询详情
     * @param id 帖子Id
     * @return 帖子详情
     */
    @Override
    public Article selectDetailById(Long id) {
        // 非空校验
        if (id == null || id <= 0) {
            // 打印日志
            log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());// 参数校验失败
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
        }
        // 调用DAO
        Article article = articleMapper.selectDetailById(id);
        // 判断结果是否为空
        if (article == null) {
            // 打印日志
            log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());// 帖子不存在
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
        }
        // 更新帖子的访问次数
        Article updateArticle = new Article();
        updateArticle.setId(article.getId());// 获取文章作者id
        updateArticle.setVisitCount(article.getVisitCount() + 1);// 访问次数+1
        // 保存到数据库
        int row = articleMapper.updateByPrimaryKeySelective(updateArticle);
        if (row != 1) {
            // 打印日志
            log.warn(ResultCode.ERROR_SERVICES.toString());// 服务器内部错误
            // 抛出异常
            throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
        }
        // 更新返回对象的访问次数
        article.setVisitCount(article.getVisitCount() + 1);
        // 返回帖子详情
        return article;
    }

5. 单元测试

返回了我们需要的信息


6. Controller实现方法并对外提供API接口

步骤

1> 调用Service, 获取帖子详情

2> 判断结果是否为空

3> 返回结果

具体代码

    @Operation(summary = "根据帖子Id获取详情")
    @GetMapping("/details")
    public AppResult<Article> getDetails(@Parameter(description = "帖子Id") @RequestParam("id") @NonNull Long id){
        // 调用Service, 获取帖子详情
        Article article = articleService.selectDetailById(id);
        // 判断结果是否为空
        if(article == null){
            // 返回错误信息
            return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
        }
        // 返回结果
        return AppResult.success(article);
    }


7. 测试API接口

前端代码编写

点击帖子的标题就会来到帖子的详细界面

构造ajax请求

    $.ajax({
      type : 'get',
      url : 'article/details?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'
        });
      }
    });

帖子详情页面

帖子详情显示编辑与删除按钮

能否删除(显示删除按钮)的判断条件: 当前登录的用户与帖子作者是不是为同一个用户

有俩种方法: 前端/后端

后端代码编写

在Article类里面添加一个isOwn属性来判断是否是作者

controller里面添加是否当前的帖子的id和作者id一样

测试

编写前端


网站公告

今日签到

点亮在社区的每一天
去签到