论坛系统(中-2)
软件开发
实现业务功能
个人中心
页面结构介绍
个人中心的页面结构分为三部分
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查询数据库并且获取对象
具体代码
@Overridepublic 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("退出成功");// 注销sessionsession.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*/@Overridepublic 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 {//注入自定义的登录拦截器@Resourceprivate LoginInterceptor loginInterceptor;// 配置拦截路径@Overridepublic 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_boardwhere state = 0 and deleteState = 0order by sort asclimit 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@Resourceprivate BoardMapper boardMapper;@Overridepublic 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;@Resourceprivate 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*/@Overridepublic 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查询板块信息
@Overridepublic 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*/@Overridepublic 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之间注入不能成环@Resourceprivate ArticleMapper articleMapper;@Resourceprivate IUserService userService;@Resourceprivate IBoardService boardService;@Overridepublic 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表里面的数据增加了
同时用户表和板块表里面的文章数也增加了
测试代码
@Resourceprivate IArticleService articleService;@Testvoid create() {Article article = new Article();article.setUserId(1L);// asdarticle.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@Resourceprivate IArticleService articleService;@Resourceprivate 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); // 板块idarticle.setUserId(user.getId());// 作者id// 调用ServicearticleService.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*/@Overridepublic List<Article> selectAll() {// 调用DAOList<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">selectu.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.updateTimefrom t_article a,t_user uwhere a.userId = u.idand a.deleteState = 0and a.boardId = #{boardId,jdbcType=BIGINT}order by a.createTime desc;</select>
2. 在Mapper.java中定义方法

3. 定义Service接口
在IArticleService里面定义接口
4. 实现Service接口
在ArticleServiceImpl里面实现接口
实现步骤
1> 对boardId进行非空校验
2> 根据boardId查询板块信息
3> 对板块信息进行非空校验
4> 不为空就调用mapper, 通过boardId把所有和板块相关的帖子进行返回
具体代码
/*** 根据板块id查询所有的帖子列表* @param boardId 板块id* @return*/@Overridepublic 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给校验过了, 因此直接调用serviceBoard 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">SELECTu.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.updateTimeFROMt_article a,t_user u,t_board bWHEREa.userId = u.idAND a.deleteState = 0AND 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 帖子详情*/@Overridepublic Article selectDetailById(Long id) {// 非空校验if (id == null || id <= 0) {// 打印日志log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());// 参数校验失败// 抛出异常throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));}// 调用DAOArticle 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());// 获取文章作者idupdateArticle.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一样
测试
编写前端
相关文章:
论坛系统(中-2)
软件开发 实现业务功能 个人中心 页面结构介绍 个人中心的页面结构分为三部分 1> 导航栏 2> 正文部分 3> 页脚部分 index.html 的页面结构 1> 导航栏 2> 正文部分 3> 页脚部分 获取用户信息 实现逻辑 ⽤⼾提交请求,服务器根据是否传⼊I…...
火山 RTC 引擎9 ----集成 appkey
一、集成 appkey 1、网易RTC 初始化过程 1)、添加头文件 实现互动直播 - 互动直播 2.0网易云信互动直播产品的基本功能包括音视频通话和连麦直播,当您成功初始化 SDK 之后,您可以简单体验本产品的基本业务流程,例如主播加入房间…...
Protobuf协议生成和使用
知识点一 利用protoc.exe编译器生成脚本文件 //1.打开cmd窗口 //2.进入protoc.exe所在文件夹(也可以直接将exe文件拖入cmd窗口中) //3.输入转换指令 //protoc.exe -I配置路径 --csharp_out输出路径 配置文件名 //注意&…...
2025年—ComfyUI_关于ComfyUI的零碎小知识
之前有个朋友问我要了一个软件安装包,我分享了网盘链接,过了会儿朋友说解压后点击安装一直提示失败,还发给我报错信息,我从没遇到过,也不知做何解,于是要了截图,看着不对劲,问其在哪…...
用 SamGeo 库实现遥感影像自动分割:从本地 TIFF 到 SHP/GeoJSON 的一站式处理(Python 脚本实现)
背景:地理空间数据处理的智能化转型与 SAM 模型的革新应用 在遥感测绘、城市规划、环境监测等领域,地理空间影像数据的自动化处理一直是提升效率的核心需求。传统的影像分割方法依赖人工标注或复杂的参数调优,难以应对海量卫星 / 无人机影像的快速分析;而栅格数据(如 Geo…...
Excel导入校验
校验监听器 /*** Excel 校验监听器* param <T>*/ public class AnalysisValidReadListener<T> extends AnalysisEventListener<T> {private static final Logger logger LoggerFactory.getLogger(AnalysisValidReadListener.class);private static final i…...
【批量图片查找】在电脑上如何根据文件名清单一次性查找多张图片并复制到指定文件夹,基于Python的解决方案
一、应用场景 这个工具适用于以下场景: 设计师需要从大量素材中筛选特定图片复制并保存摄影师需要根据文件名批量整理照片查找筛选复制电商运营人员需要从产品库中提取特定商品图片复制到指定文件夹数据分析师需要批量收集特定图片复制保存用于处理任何需要从大量图…...
湖北理元理律师事务所观察:债务服务中的“倾听者价值”
在债务纠纷解决过程中,法律专业能力与心理支持同样重要。调研显示,72%的债务人在咨询初期存在“隐瞒真实负债”“抗拒沟通”等行为,直接影响方案有效性。湖北理元理律师事务所通过服务模式创新,尝试破解这一难题。 建立信任的三大…...
GPT-4.1特点?如何使用GPT-4.1模型,GPT-4.1编码和图像理解能力实例展示
几天前,OpenAI在 API 中推出了三个新模型:GPT-4.1、GPT-4.1 mini 和 GPT-4.1 nano。这些模型的性能全面超越 GPT-4o 和 GPT-4o mini(感觉这个GPT-4.1就是GPT-4o的升级迭代版本),主要在编码和指令跟踪方面均有显著提升。还拥有更大的上下文窗口…...
网络工程师案例分析
✅ Huawei Super VLAN 通信规则总结 🌐 基本结构 Super VLAN:逻辑 VLAN,承载三层网关(VLANIF 接口)。 Sub VLAN:实际的用户 VLAN,不配置 IP,仅做二层转发。 🔒 通信规…...
tcp/ip协议
OSI参考模型 应用层:OSI最高层。确定进程之间通信性质 协议:http:80,https:443,ftp:21,telnet:23,ssh:22,smtp:25,pop3 表示层:处理流经结点的数据编码的表示方式问题,以保证一个系统应用层发出的消息可被另一系统的应用层读出,数据压缩和加…...
小红书的视频怎么保存没有水印(方法分享)
你是不是也经常在小红书上刷到超赞的旅行vlog、美妆教程或美食探店视频,想保存下来慢慢看,却发现下载后总有烦人的水印?别急!今天教你一招,3秒轻松保存无水印高清视频,简单又实用! 为什么需要无…...
RK3568解码1080P视频时遇到系统崩溃内核挂掉的解决方案
接上篇rk3568。 实际使用 rock_mpp库硬解码时,会遇到解码1080P视频整个系统卡死,内核崩溃的问题。 以下是内核崩溃的日志,下面这句是典型的内核某块驱动挂掉的信息。 [ 292.469580] Unable to handle kernel NULL pointer dereference at…...
C++ —— Lambda 表达式
🎁个人主页:工藤新一 🔍系列专栏:C面向对象(类和对象篇) 🌟心中的天空之城,终会照亮我前方的路 🎉欢迎大家点赞👍评论📝收藏⭐文章 文章目录 L…...
Keepalived相关配置和高可用
目录 一. Keepalived的工作原理 二. 实现单独的心跳网卡 三. keepalive一些优化 3.1 主从之间加密验证 3.2 修改心跳线发送时间 四. 添加独立日志 五. 抢占模式,非抢占模式,延迟抢占模式 六. 单播地址和多播地址 1. 单播地址(Unicast…...
gtest 库的安装和使用
目录 介绍 安装 使用 介绍 官方文档:GoogleTest 入门 |GoogleTest 谷歌测试 gtest 库是谷歌开源的 C测试单元框架,方便我们测试程序的正确性。 安装 sudo apt-get install libgtest-dev 使用 GTest 中的断言的宏可以分为两类: • ASS…...
Python训练营打卡——DAY30(2025.5.19)
目录 模块和库的导入 一、导入官方库 1. 标准导入:导入整个库 2. 从库中导入特定项 3. 非标准导入:导入整个库 二、模块、包的定义 三、使用案例 场景1: main.py 和 circle.py 都在同一目录 场景2: main.py 和 circle.py 都在根目录的子目录 mo…...
Django框架的前端部分使用Ajax请求一
Ajax请求 目录 1.ajax请求使用 2.增加任务列表功能(只有查看和新增) 3.代码展示集合 这篇文章, 要开始讲关于ajax请求的内容了。这个和以前文章中写道的Vue框架里面的axios请求, 很相似。后端代码, 会有一些细节点, 跟前几节文章写的有些区别。 一、ajax请求使用 我们先…...
w~自动驾驶~合集3
我自己的原文哦~ https://blog.51cto.com/whaosoft/13269720 #FastOcc 推理更快、部署友好Occ算法来啦! 在自动驾驶系统当中,感知任务是整个自驾系统中至关重要的组成部分。感知任务的主要目标是使自动驾驶车辆能够理解和感知周围的环境元素&…...
LeetCode 39. 组合总和 LeetCode 40.组合总和II LeetCode 131.分割回文串
LeetCode 39. 组合总和 需要注意的是题目已经明确了数组内的元素不重复(重复的话需要执行去重操作),且元素都为正整数(如果存在0,则会出现死循环)。 思路1:暴力解法 对最后结果进行去重 每一…...
C++(2)关键字+数据类型 +数据类型输入
(1)如下关键字是 不能用于定义变量名和常量名的 !。 如int int 这样就会报错 所以注意即可 。 (2)标识符命名规则 (即变量和常量的命名规则) 最主要注意 第一个 字符必须是字母或是下划线 —…...
第二道re
题目来源:天狩CTF竞赛平台 Lihuas for 题目提示说是for循环,不管了干吧 先看加没加壳,没有,直接无脑IDAF5 代码功能概述 程序会要求用户输入一个 flag,然后将输入的每个字符与索引值进行异或运算,并将结…...
【C语言内存函数】--memcpy和memmove的使用和模拟实现,memset函数的使用,memcmp函数的使用
目录 一.memcpy的使用和模拟实现 1.1--memcpy的使用演示 1.2--memcpy的模拟实现 二.memmove的使用和模拟实现 2.1--memmove的使用演示 2.2--memmove的模拟实现 三.memset函数的使用 3.1--memset的使用演示 3.2--总结 四.memcmp函数的使用 4.1--memcmp的使用演示 4.2…...
java集合详细讲解
Java 8 集合框架详解 Java集合框架是Java中最重要、最常用的API之一,Java 8对其进行了多项增强。下面我将全面讲解Java 8中的集合框架。 一、集合框架概述 Java集合框架主要分为两大类: Collection - 单列集合 List:有序可重复Set…...
UniApp 实现的文件预览与查看功能#三方框架 #Uniapp
UniApp 实现的文件预览与查看功能 前言 在开发移动应用时,文件预览功能是一个非常常见的需求。无论是查看PDF文档、图片还是Office文件,都需要一个稳定且易用的预览解决方案。本文将详细介绍如何在UniApp中实现各类文件的预览功能,并分享一…...
用户行为日志分析的常用架构
## 1. 经典Lambda架构 Lambda架构是一种流行的大数据处理架构,特别适合用户行为日志分析场景。 ### 1.1 架构组成 Lambda架构包含三层: - **批处理层(Batch Layer)**: 存储全量数据并进行离线批处理 - **实时处理层(Speed Layer)**: 处理最新数据&…...
【VBA/word】批量替换字体大小
将5号或6号字体改为10.5号字体(循环10次) AI复制的文案问题调整 Sub Change5or6ptTo16pt_10Loops()Dim rng As RangeDim doc As DocumentDim found As BooleanDim i As IntegerDim totalChanges As LongDim targetSizes As VariantDim size As VariantSe…...
C++类与对象--3 C++对象模型和this指针
3.1 类成员分开存储 成员变量和成员函数在内存中是分开存储的只有非静态成员变量是存储在对象上的 C为空对象分配1字节的空间非空对象的大小为其内部非成员变量大小总和 静态成员不占对象空间不同对象的成员函数共享一个函数实例,不占对象空间(通过th…...
DV SSL证书管理主要有哪些功能?
在互联网信息传输高速发展的今天,用户对网站安全性的要求越来越高。SSL证书已成为网站“身份认证数据加密”的标配。其中,DV SSL证书由于其签发快速、价格低廉、使用广泛,成为大量中小型网站、个人博客、电商平台的首选。然而,选择…...
el-tree结合el-tree-transfer实现穿梭框里展示树形数据
参考文章:我把他的弹框单拉出来一个独立文件作为组件方便使用,遇到一些问题记录一下。 testComponet.vue <template><div class"per_container"><div class"per_con_left"><div class"per_con_title&q…...
浅谈GC机制-三色标记和混合写屏障
标记清除法 stw(stop the world):暂停所有goroutine,扫描出可达与不可达对象,进行回收 三色标记法 不暂停,并发扫描,从根节点出发,扫描过对象的为黑,下一个可达对象为…...
Python训练营打卡 Day30
模块和库的导入 知识点回顾: 导入官方库的三种手段 直接导入整个库:使用 import library_name 语法。 导入库中的特定模块或函数:使用 from library_name import module_name 或 from library_name import function_name。 导入库并起别名&…...
深入探讨死区生成:原理、实现与应用
在电力电子、信号处理等众多领域中,“死区生成”是一个十分关键的概念,它能有效避免器件误动作、减少干扰,保障系统稳定运行。今天就通过问答的形式,和大家深入聊聊死区生成相关知识。 什么是死区生成? 死区生成是指…...
OpenCV 环境搭建与概述
// //OpenCV-4.11.0 C VS2019 // 一、OpenCV学习路线 1、入门: OpenCV图像读写、视频读写、基本像素处理、基本卷积处理、基本C开发知识。 2、初级: OpenCV自定义卷积操作、图像梯度、边缘提取、二值分析、视频分析、形态学处理、几何变换与透视变换。 3、中级: 角点查找、BL…...
c/c++的opencv均值函数
C/C 中的均值函数:从基础到应用 📊 在 C/C 编程中,计算一组数值的**均值(平均值)**是一项非常基础且常见的操作。无论是数据分析、信号处理、图像处理还是机器学习,均值函数都扮演着重要的角色。本文将详细…...
go 数据类型转换
graph TDA[整型<br>int, int8, int16, int32, int64] -->|类型转换| B[浮点型<br>float32, float64]B -->|类型转换| AA -->|类型转换| C[布尔型<br>bool]C -->|类型转换| AB -->|类型转换| D[复数型<br>complex64, complex128]D -->…...
Go内存管理
内存管理 文章目录 内存管理何为内存?内存为什么需要管理?内存管理的方式操作系统存储模型操作系统是怎么管理内存的?虚拟内存与物理内存认识虚拟内存分页管理 Golang 内存模型TCMalloc核心概念go内存管理核心概念GO内存分配GO 内存逃逸机制一…...
解决软件连接RabbitMQ突发System.IO.IOException: 无法从传输连接中读取数据: 远程主机强迫关闭了一个现有的连接异常
一、问题描述 系统再运行时,突然出现 System.Exception: [RabbitMQ.Send Error] RabbitMQ.Client.Exceptions.AlreadyClosedException: Already closed: The AMQP operation was interrupted: AMQP close-reason, initiated by Library, code541, text“Unexpected…...
基于局部显著位置感知的异常掩码合成方法在CT图像肺部疾病异常检测与病变定位中的应用|文献速递-深度学习医疗AI最新文献
Title 题目 Local salient location-aware anomaly mask synthesis for pulmonary disease anomaly detection and lesion localization in CT images 基于局部显著位置感知的异常掩码合成方法在CT图像肺部疾病异常检测与病变定位中的应用 01 文献速递介绍 肺部疾病是全球发…...
【cursor疑惑】cursor续杯后使用agent对话时,提示“需要pro或商业订阅的用户才能使用“
背景 cursor的pro会员体验过期了,想再次体验deepseek、Claude等agent对话提示:“免费版本不可以使用agent对话功能(英文忘记截图了,大意是这样)”。 处理方法 Step-1:再次续杯cursor的pro会员14天体验 详情,见:【c…...
2022年下半年信息系统项目管理师——综合知识真题及答案(3)
2022年下半年信息系统项目管理师 ——综合知识真题及答案(3) 零、时光宝盒 (https://blog.csdn.net/weixin_69553582 逆境清醒) 此文是我2025-05-19回复头条上某作者文章时的评论记录,原作者的文章是写那些被有组织…...
华为云Flexus+DeepSeek征文|基于华为云Flexus云服务的云服务器单机部署Dify-LLM应用开发平台
目录 一、前言 二、华为云Flexus云服务优势 三、华为云Flexus一键部署Dify 3.1 选择模板 3.2 参数配置 3.3 资源栈设置 3.4 配置确认 3.5 创建执行计划 3.6 部署 四、Dify-LLM应用开发平台初体验 4.1 访问Dify-LLM应用开发平台 4.2 设置管理员账户 4.3 登录Dify-LLM应用开发平台…...
NC105NC106美光固态颗粒NC108NC109
NC105NC106美光固态颗粒NC108NC109 美光固态颗粒技术矩阵深度解析:NC105/NC106/NC108/NC109的性能博弈与市场卡位 一、技术基因图谱:解密NC系列颗粒的底层架构 1. TLC与QLC的技术路线分野 美光NC系列颗粒呈现出清晰的技术分层:NC105/NC10…...
洛谷U536262 井底之“鸡” 附视频讲解
题目截图 题目背景 2024年山东财经大学新生赛的时候,xz_chicken厌倦了在鸡舍的生活,于是决定把他的主人ZQH写成QH鸡,然而除了在第一题中已经出现了ZQH所在队伍征途再起的合影,同时xz_chicken还露出其他马脚,比如说答案…...
特征筛选方法总结(面试准备15)
非模型方法 一.FILTER过滤法: 1.缺失值比例(80%以上缺失则删除)/方差 注意: 连续变量只删方差为0的,因为变量取值范围会影响方差大小。 离散类的看各类取值占比,如果是三分类变量可以视作连续变量。 函数:V…...
深入解析分布式数据库TiDB:原理、优化与架构实践
前言 在云计算与大数据时代,传统单机数据库面临三大挑战:海量数据存储、高并发访问和实时分析需求。MySQL 分库分表方案复杂、NoSQL 缺乏 ACID 支持、MPP 数仓难以处理 OLTP… 在这样的背景下,TiDB 应运而生。作为一款开源的分布式 NewSQL 数…...
YouTube视频字幕转成文章算重复内容吗?
很多创作者误以为「自己说的话不算抄袭」,却不知道YouTube自动生成的字幕早已被搜索引擎存档。 去年就有案例:某美食博主将教程视频字幕转为图文,结果原创度检测仅42%,导致页面权重暴跌。 本文揭秘5个实操技巧:从删除…...
codeup添加流水线docker自动化部署
在项目根目录下增加Dockerfile文件 # 使用基础镜像 FROM maven:3.8.4-openjdk-17-slim AS build # 设置工作目录 WORKDIR /app # 复制项目源代码 COPY . . # 构建项目 RUN mvn clean package -DskipTests # 验证JAR包是否生成 RUN ls -l target/your-project.jar # 使用合适的…...
面试点补充
目录 1. 搭建lnmp Linux 系统基础命令 nginx相关命令 MySQL 相关命令 PHP 相关命令 验证命令 下载并部署 Discuz! X3.4 论坛 到 Nginx 网站 2. 脑裂 2.1 脑裂的定义 2.2 脑裂产生的原因 1. 主备节点之间的心跳线中断 2. 优先级冲突 3. 系统或服务负载过高 2.3 如何…...
深入解析 Oracle session_cached_cursors 参数及性能对比实验
在 Oracle 数据库管理中,session_cached_cursors参数扮演着至关重要的角色,它直接影响着数据库的性能和资源利用效率。本文将深入剖析该参数的原理、作用,并通过性能对比实验,直观展示不同参数设置下数据库的性能表现。 一、sessi…...