目录
一、项目简介
1. 使用统一返回格式+全局错误信息定义处理前后端交互时的返回结果
2.使用@ControllerAdvice+@ExceptionHandler实现全局异常处理
3.使用拦截器实现用户登录校验
4. 使用MybatisGeneratorConfig生成常的增删改查方法
5. 集成Swagger实现自动生成API测试接口
6. 使用jQuery完成AJAX请求,并处理HTML页面标签
7. 对数据库中常用的查询字段段建立索引,并使用查询计划分析索引是否生效
8.对用户密码进行MD5加密
二、技术选型及交互图
三、数据库设计
四、接口设计及思考
接口设计共分具体8个步骤:
编写类与映射⽂件
生成类与映射⽂件(了解)
创建generatorConfig.xml
运⾏插件⽣成⽂件
4.1 回复帖子
4.1.1.1 实现逻辑
4.1.1.2创建Service接⼝
4.1.1.3 实现Service接⼝
4.1.1.4 实现Controller
4.1.1.5 测试接口
4.1.1.6 实现前端页面
4.2 点赞帖子
4.2.1.1 参数要求
4.2.1.2 创建Service接⼝
4.2.1.3 实现Service接⼝
4.3 删除帖子
4.3.1.1 参数要求
4.3.1.2 创建Service接口
4.3.1.3 实现Service接⼝
4.3.1.4 实现Controller
五、项目测试
测试用例 云上社群系统测试用例 - 飞书云文档 (feishu.cn)
编写功能测试用例,对每个模块进行功能测试
用swagger框架进行API管理和接口规整,并对每一个接口都进行了测试
单元测试
一、项目简介
项目URL:云上社群 - 用户登录
1. 使用统一返回格式+全局错误信息定义处理前后端交互时的返回结果
package com.example.forum.common;
/**
* @author Dian
* Description
* Date:2023/8/7:19:06
*/
public enum ResultCode {
//全局定义
SUCCESS (0, "操作成功"),
FAILED (1000, "操作失败"),
FAILED_UNAUTHORIZED (1001, "未授权"),
FAILED_PARAMS_VALIDATE (1002, "参数校验失败"),
FAILED_FORBIDDEN (1003, "禁止访问"),
FAILED_CREATE (1004, "新增失败"),
FAILED_NOT_EXISTS (1005, "资源不存在"),
//用户的定义
FAILED_USER_EXISTS (1101, "用户已存在"),
FAILED_USER_NOT_EXISTS (1102, "用户不存在"),
FAILED_LOGIN (1103, "用户名或密码错误"),
FAILED_USER_BANNED (1104, "您已被禁言, 请联系管理员, 并重新登录."),
FAILED_TWO_PWD_NOT_SAME (1105, "两次输入的密码不一致"),
FAILED_PASSWOED_CHECK (1106, "密码校验失败"),
//版块的定义
FAILED_BOARD_EXISTS (1201, "版块已存在"),
FAILED_BOARD_NOT_EXISTS (1202, "版块不存在"),
// 帖子相关的定义
FAILED_ARTICLE_NOT_EXISTS (1302, "帖子不存在"),
FAILED_ARTICLE_STATE (1301,"帖子有效性异常"),
//服务器定义
ERROR_SERVICES (2000, "服务器内部错误"),
ERROR_IS_NULL (2001, "IS NULL.");
long code;
String message;
public long getCode() {
return code;
}
public String getMessage() {
return message;
}
ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() {
return "code = " + code + ",message = " + message +".";
}
}
系统实现前后端分离,统⼀返回JSON格式的字符串,需要定义⼀个类,其中包含状态码,描述信息,返回的结果数据,在com.example.forum.common包下创建AppResult类
@ApiModel("统一返回对象")
public class AppResult<T> {
@ApiModelProperty("状态码")
@JsonInclude(JsonInclude.Include.ALWAYS)
private long code;
@JsonInclude(JsonInclude.Include.ALWAYS)
@ApiModelProperty("描述信息")
private String message;
@JsonInclude(JsonInclude.Include.ALWAYS)
@ApiModelProperty("具体对象")
private T data;
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.message);
}
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.message,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;
}
}
2.使用@ControllerAdvice+@ExceptionHandler实现全局异常处理
/**
* @author Dian
* Description
* Date:2023/8/8:14:33
*/
public class ApplicationException extends RuntimeException{
private static final long serialVersionUID = -3533806916645793660L;
protected AppResult errorResult;
// 指定状态码,异常描述
public ApplicationException(AppResult errorResult) {
super(errorResult.getMessage());
this.errorResult = errorResult;
}
// ⾃定义异常描述
public ApplicationException(String message) {
super(message);
}
// 指定异常
public ApplicationException(Throwable cause) {
super(cause);
}
// ⾃定义异常描述,异常信息
public ApplicationException(String message, Throwable cause) {
super(message, cause);
}
public AppResult getErrorResult() {
return errorResult;
}
}
/**
* @author Dian
* Description
* Date:2023/8/8:14:40
*/
/**
* 统一异常处理
*/
//添加注解
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理⾃定义的已知异常
* @param e ApplicationException
* @return AppResult
*/
// 以JSON形式返回BODY中的数据
@ResponseBody
@ExceptionHandler(ApplicationException.class)
public AppResult handleApplication(ApplicationException e){
//打印异常
e.printStackTrace();
//记录日志
log.error(e.getMessage());
//获取异常信息,判断自定义的异常是否为空
if(e.getErrorResult()!= null){
//返回异常类中记录的状态
return e.getErrorResult();
}
//根据异常信息封装Appresult
return AppResult.failed(e.getMessage());
}
/**
* 处理全未捕获的其他异常
* @param e Exception
* @return AppResult
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public AppResult handleException(Exception e){
//打印异常
e.printStackTrace();
//记录日志
log.error(e.getMessage());
//非空校验
if(e.getMessage() == null){
return AppResult.failed(ResultCode.ERROR_SERVICES);
}
//根据异常信息封装Appresult
return AppResult.failed(e.getMessage());
}
3.使用拦截器实现用户登录校验
/**
* @author Dian
* Description
* Date:2023/8/11:14:38
*/
@Component// 加入到Spring中
public class LoginInterceptor implements HandlerInterceptor {
// 从配置文件中读取配置内容
@Value("${bit-forum.login.url}")
private String defautURL;
/**
* 预处理(请求的前置处理)回调方法<br/>
*
* @return true 继续请求流程 </br> false 中止请求流程
* @throws Exception
*/
@Override
public 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;
}
// 校验不通过时要处理的逻辑
// 1. 返回一个错误的HTTP状态码
// response.setStatus(403);
// 2. 跳转到某一个页面
// 对URL前缀做校验(确保目标URL从根目录开发)
if(!defautURL.startsWith("/")){
defautURL = "/" +defautURL;
}
response.sendRedirect(defautURL);
return false;
}
}
forum:
login:
url: sign-in.html # 未登录状况下强制跳转⻚⾯
/**
* @author Dian
* Description
* Date:2023/8/11:14:55
*/
@Configuration// 把当前配置类加入到Spring中
public class AppInterceptorConfigure implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加登录拦截器
registry.addInterceptor(loginInterceptor) // 添加⽤⼾登录拦截器
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/sign-in.html") // 排除登录HTML
.excludePathPatterns("/sign-up.html") // 排除注册HTML
.excludePathPatterns("/user/login") // 排除登录api接⼝
.excludePathPatterns("/user/register") // 排除注册api接⼝
.excludePathPatterns("/user/logout") // 排除退出api接⼝
.excludePathPatterns("/swagger*/**") // 排除登录swagger下所有
.excludePathPatterns("/v3*/**") // 排除登录v3下所有,与swag
.excludePathPatterns("/dist/**") // 排除所有静态⽂件
. excludePathPatterns("/image/**")
.excludePathPatterns("/**.ico")
.excludePathPatterns("/js/**");
}
}
4. 使用MybatisGeneratorConfig生成常的增删改查方法
在做项目的时候,写类与映射⽂件时学习到的,了解到这个Maven插件,就是一个代码生成器,可以非常方便的生成mapper文件。
5. 集成Swagger实现自动生成API测试接口
• @Api: 作⽤在Controller上,对控制器类的说明◦ tags="说明该类的作⽤,可以在前台界⾯上看到的注解"• @ApiModel: 作⽤在响应的类上,对返回响应数据的说明• @ApiModelProerty:作⽤在类的属性上,对属性的说明• @ApiOperation: 作⽤在具体⽅法上,对API接⼝的说明,描述方法• @ApiParam: 作⽤在⽅法中的每⼀个参数上,对参数的属性进⾏说明@RequestParam:请求时指定参数名@NonNull:请求时参数不能为空
6. 使用jQuery完成AJAX请求,并处理HTML页面标签
7. 对数据库中常用的查询字段段建立索引,并使用查询计划分析索引是否生效
8.对用户密码进行MD5加密
密码在数据库中不会去存明文,明文密码 + 扰动字符串(salt盐 随机字符) = 密文密码
MD5(MD5(明文密码) + salt) = 密码对应的密文
创建MD5加密⼯具类
项⽬中使⽤commons-codec,它是Apache提供的⽤于摘要运算、编码解码的⼯具包。常⻅的编码解码⼯具Base64、MD5、Hex、SHA1、DES等。• pom.xml中导⼊依赖,SpringBoot已经对这个包做了版本管理,所以这⾥不⽤指定版本号
/**
* @author Dian
* Description
* Date:2023/8/8:20:26
*/
public class MD5Utils {
/**
* 普通MD5加密
* @param str 原始字符串
* @return 一次MD5加密后的密文
*/
public static String md5 (String str) {
return DigestUtils.md5Hex(str);
}
/**
* 原始字符串与Key组合进行一次MD5加密
* @param str 原始字符串
* @param key
* @return 组合字符串一次MD5加密后的密文
*/
public static String md5 (String str, String key) {
return DigestUtils.md5Hex(str + key);
}
/**
* 原始字符串加密后与扰动字符串组合再进行一次MD5加密
* @param str 原始字符串
* @param salt 扰动字符串
* @return 加密后的密文
*/
public static String md5Salt (String str, String salt) {
return DigestUtils.md5Hex(DigestUtils.md5Hex(str) + salt);
}
/**
* 校验原文与盐加密后是否与传入的密文相同
* @param original 原字符串
* @param salt 扰动字符串
* @param ciphertext 密文
* @return true 相同, false 不同
*/
public static boolean verifyOriginalAndCiphertext (String original, String salt, String ciphertext) {
String md5text = md5Salt(original, salt);
if (md5text.equalsIgnoreCase(ciphertext)) {
return true;
}
return false;
}
}
创建⽣成UUID⼯具类
/**
* @author Dian
* Description
* Date:2023/8/8:20:22
*/
public class UUIDUtils {
/**
* 生成一个标准的36字符的UUID
* @return
*/
public static String UUID_36 () {
return UUID.randomUUID().toString();
}
/**
* 生成一个32字符的UUID
* @return
*/
public static String UUID_32 () {
return UUID.randomUUID().toString().replace("-", "");
}
}
Service层
@Override
public void 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 existsUser = selectByName(user.getUsername());
if (existsUser != null) {
// 打印日志
log.warn(ResultCode.FAILED_USER_EXISTS.toString() + " username = " + user.getUsername());
// 抛出异常
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));
}
}
Controller层
@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); // 昵称
// 处理密码
// 1. 生成盐
String salt = UUIDUtils.UUID_32();
// 2. 生成密码的密文
String encryptPassword = MD5Utils.md5Salt(password, salt);
// 3. 设置密码和盐
user.setPassword(encryptPassword);
user.setSalt(salt);
// 调用Service
userService.createNormalUser(user);
// 返回结果
return AppResult.success("注册成功");
}
二、技术选型及交互图
三、数据库设计
数据库名: forum_db
公共字段:⽆特殊要求的情况下,每张表必须有⻓整型的⾃增主键,删除状态、创建时间、更新时 间,如下所⽰:
共建五张表 ,具体操作如下:
SQL脚本
-- ----------------------------
-- 创建数据库,并指定字符集
-- ----------------------------
drop database if exists forum_db;
create database forum_db character set utf8mb4 collate utf8mb4_general_ci;
-- 选择数据库
use forum_db;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 创建帖子表 t_article
-- ----------------------------
DROP TABLE IF EXISTS `t_article`;
CREATE TABLE `t_article` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '帖子编号,主键,自增',
`boardId` bigint(20) NOT NULL COMMENT '关联板块编号,非空',
`userId` bigint(20) NOT NULL COMMENT '发帖人,非空,关联用户编号',
`title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '标题,非空,最大长度100个字符',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '帖子正文,非空',
`visitCount` int(11) NOT NULL DEFAULT 0 COMMENT '访问量,默认0',
`replyCount` int(11) NOT NULL DEFAULT 0 COMMENT '回复数据,默认0',
`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0正常 1 禁用,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0 否 1 是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '修改时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建帖子回复表 t_article_reply
-- ----------------------------
DROP TABLE IF EXISTS `t_article_reply`;
CREATE TABLE `t_article_reply` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号,主键,自增',
`articleId` bigint(20) NOT NULL COMMENT '关联帖子编号,非空',
`postUserId` bigint(20) NOT NULL COMMENT '楼主用户,关联用户编号,非空',
`replyId` bigint(20) NULL DEFAULT NULL COMMENT '关联回复编号,支持楼中楼',
`replyUserId` bigint(20) NULL DEFAULT NULL COMMENT '楼主下的回复用户编号,支持楼中楼',
`content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '回贴内容,长度500个字符,非空',
`likeCount` int(11) NOT NULL DEFAULT 0 COMMENT '点赞数,默认0',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1禁用,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '帖子回复表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建版块表 t_board
-- ----------------------------
DROP TABLE IF EXISTS `t_board`;
CREATE TABLE `t_board` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '版块编号,主键,自增',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '版块名,非空',
`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '帖子数量,默认0',
`sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序优先级,升序,默认0,',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态,0 正常,1禁用,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '版块表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建站内信表 for t_message
-- ----------------------------
DROP TABLE IF EXISTS `t_message`;
CREATE TABLE `t_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '站内信编号,主键,自增',
`postUserId` bigint(20) NOT NULL COMMENT '发送者,并联用户编号',
`receiveUserId` bigint(20) NOT NULL COMMENT '接收者,并联用户编号',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内容,非空,长度255个字符',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0未读 1已读,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否,1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒,非空',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒,非空',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '站内信表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- 创建用户表 for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户编号,主键,自增',
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名,非空,唯一',
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '加密后的密码',
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称,非空',
`phoneNum` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址',
`gender` tinyint(4) NOT NULL DEFAULT 2 COMMENT '0女 1男 2保密,非空,默认2',
`salt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '为密码加盐,非空',
`avatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户头像URL,默认系统图片',
`articleCount` int(11) NOT NULL DEFAULT 0 COMMENT '发帖数量,非空,默认0',
`isAdmin` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否管理员,0否 1是,默认0',
`remark` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注,自我介绍',
`state` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态 0 正常,1 禁言,默认0',
`deleteState` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否删除 0否 1是,默认0',
`createTime` datetime NOT NULL COMMENT '创建时间,精确到秒',
`updateTime` datetime NOT NULL COMMENT '更新时间,精确到秒',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `user_username_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;
-- 写入版块信息数据
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (1, 'Java', 0, 1, 0, 0, '2023-01-14 19:02:18', '2023-01-14 19:02:18');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (2, 'C++', 0, 2, 0, 0, '2023-01-14 19:02:41', '2023-01-14 19:02:41');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (3, '前端技术', 0, 3, 0, 0, '2023-01-14 19:02:52', '2023-01-14 19:02:52');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (4, 'MySQL', 0, 4, 0, 0, '2023-01-14 19:03:02', '2023-01-14 19:03:02');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (5, '面试宝典', 0, 5, 0, 0, '2023-01-14 19:03:24', '2023-01-14 19:03:24');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (6, '经验分享', 0, 6, 0, 0, '2023-01-14 19:03:48', '2023-01-14 19:03:48');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (7, '招聘信息', 0, 7, 0, 0, '2023-01-25 21:25:33', '2023-01-25 21:25:33');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (8, '福利待遇', 0, 8, 0, 0, '2023-01-25 21:25:58', '2023-01-25 21:25:58');
INSERT INTO `t_board` (`id`, `name`, `articleCount`, `sort`, `state`, `deleteState`, `createTime`, `updateTime`) VALUES (9, '灌水区', 0, 9, 0, 0, '2023-01-25 21:26:12', '2023-01-25 21:26:12');
-- 写入帖子表数据
insert into t_article values (null, 1, 1, '测试数据-标题1', '测试数据-内容1', 0,0,0,0,0, '2023-08-11 16:52:00','2023-08-11 16:52:00');
insert into t_article values (null, 1, 1, '测试数据-标题2', '测试数据-内容2', 0,0,0,0,0, '2023-08-11 16:53:00','2023-08-11 16:53:00');
insert into t_article values (null, 1, 1, '测试数据-标题3', '测试数据-内容3', 0,0,0,0,0, '2023-08-11 16:54:00','2023-08-11 16:54:00');
insert into t_article values (null, 2, 2, '测试数据-标题4', '测试数据-内容4', 0,0,0,0,0, '2023-08-11 16:55:00','2023-08-11 16:55:00');
insert into t_article values (null, 2, 2, '测试数据-标题5', '测试数据-内容5', 0,0,0,0,0, '2023-08-11 16:56:00','2023-08-11 16:56:00');
-- 写入回复表数据
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');
四、接口设计及思考
本项目共设计23个接口,介绍其设计思路及其中三个的具体实现。
接口设计共分具体8个步骤:
1.在Mapper.xml中编写语句
2.在Mapper.java中定义方法
3.定义Service接口
4.实现Service接口
5.单元测试
6.Controller实现方法并对外提供API接口
7.测试API接口(swaggerUI)
8.实现前端逻辑,完成前后端交互
编写类与映射⽂件
生成类与映射⽂件(了解)
<mybatis-generator-plugin-version>1.4.1</mybatis-generator-plugin-version>
<!-- 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
创建generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 驱动包路径,location中路径替换成自己本地路径 -->
<classPathEntry location="D:\apache-tomcat-8.5.49\webapps\blog\WEB-INF\lib\mysql-connector-java-5.1.49.jar"/>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 禁用自动生成的注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
<property name="suppressDate" value="true"/>
</commentGenerator>
<!-- 连接配置 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/forum_db?characterEncoding=utf8&useSSL=false"
userId="root"
password="123456">
</jdbcConnection>
<javaTypeResolver>
<!-- 小数统一转为BigDecimal -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 实体类生成位置 -->
<javaModelGenerator targetPackage="com.example.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.example.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>
</generatorConfiguratio
运⾏插件⽣成⽂件
• 在 src/main/resources下创建mapper⽬录
回复帖子部分
4.1 回复帖子
• 在帖⼦详情⻚⾯⽤⼾可以发表回复
4.1.1.1 实现逻辑
4.1.1.2创建Service接⼝
/**
* 新增一个回复记录
* @param articleReply
*/
@Transactional
void create (ArticleReply articleReply);
}
4.1.1.3 实现Service接⼝
4.1.1.4 实现Controller
4.1.1.5 测试接口
4.1.1.6 实现前端页面
// ====================== 回复帖⼦ ======================
$('#details_btn_article_reply').click(function () {
let articleIdEl = $('#details_article_id');
let replyContentEl = $('#details_article_reply_content');
// ⾮空校验
if (!replyContentEl.val()) {
// 提⽰
$.toast({
heading: '提⽰',
text: '请输⼊回复内容',
icon: 'warning'
});
return;
}
// 构造帖⼦对象
let articleReplyObj = {
articleId: articleIdEl.val(),
content: replyContentEl.val()
}
// 发送请求
$.ajax({
type: 'post',
url: '/article/reply',
// 发送的数据
data: articleReplyObj,
success: function (respData) {
// ⽤状态码判断是否成功
if (respData.code == 0) {
// 清空输⼊区
editor.setValue('');
// 更新回贴数
currentArticle.replyCount = currentArticle.replyCount + 1;
$('#details_article_replyCount').html(currentArticle.replyCount);
// 构建回贴⻚⾯
loadArticleDetailsReply();
$.toast({
heading: '提⽰',
text: respData.message,
icon: 'success'
});
} else {
// 提⽰
$.toast({
heading: '提⽰',
text: respData.message,
icon: 'info'
});
}
},
error: function () {
$.toast({
heading: '错误',
text: '出错了,请联系管理员',
icon: 'error'
});
}
});
});
4.2 点赞帖子
4.2.1.1 参数要求
4.2.1.2 创建Service接⼝
/**
* 点赞
* @param id 帖子Id
*/
void thumbsUpById(Long id);
}
4.2.1.3 实现Service接⼝
@Override
public void thumbsUpById(Long id) {
//非空校验
if (id == null || id <= 0) {
// 打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
//查询帖子信息
Article article = selectById(id);
if (article == null || article.getState() == 1 || article.getDeleteState() == 1) {
//打印日志
log.info(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
}
//构造更新对象
Article update = new Article();
update.setId(article.getId());
update.setLikeCount(article.getLikeCount() + 1);
//更新数据库
int row = articleMapper.updateByPrimaryKeySelective(update);
if (row != 1) {
log.info(ResultCode.FAILED_CREATE.toString() + "userId = " + article.getUserId());
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
}
}
}
@ApiOperation("点赞")
@PostMapping("/thumbsUp")
public AppResult thumbsUp(HttpServletRequest request,
@ApiParam(value = "帖子Id") @RequestParam(value = "id") @NonNull Long id){
// 获取⽤⼾信息
HttpSession session = request.getSession(false);
User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
// 判断是否被禁⾔
if (user.getState() != 0) {
// ⽇志
log.warn(ResultCode.FAILED_USER_BANNED.toString() + ", userId = " +
user.getId());
// 返回错误信息
return AppResult.failed(ResultCode.FAILED_USER_BANNED);
}
// 更新点赞数
articleService.thumbsUpById(id);
// 返回结果
return AppResult.success();
}
4.3 删除帖子
4.3.1.1 参数要求
4.3.1.2 创建Service接口
/**
* 版块中的帖子数量 -1
* @param id 版块Id
*/
void subOneArticleCountById(Long id);
}
/**
* 用户发帖数 -1
* @param id
*/
void subOneArticleCountById(Long id);
}
/**
* 根据Id删除帖子
* @param id 帖子Id
*/
@Transactional
void deleteById(Long id);
}
4.3.1.3 实现Service接⼝
@Override
public void subOneArticleCountById(Long id) {
//非空校验
if (id == null || id < 0) {
//打印日志
log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
}
//查询板块信息
Board board = selectById(id);
// 校验版块是否存在
if (board == null) {
// 打印日志
log.warn(ResultCode.FAILED_BOARD_NOT_EXISTS.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_BOARD_NOT_EXISTS));
}
// 构造要更新的对象
Board updateBoard = new Board();
updateBoard.setId(board.getId()); // 版块ID
updateBoard.setArticleCount(board.getArticleCount() - 1); // 帖子数量
if (updateBoard.getArticleCount() < 0) {
//如果小于0那么设置为0
updateBoard.setArticleCount(0);
}
// 调用DAO
int row = boardMapper.updateByPrimaryKeySelective(updateBoard);
if (row != 1) {
// 打印日志
log.warn(ResultCode.FAILED.toString() + ",受影响的行数不等于1.");
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED));
}
}
}
@Override
public void subOneArticleCountById(Long id) {
//非空校验
if (id == null || id < 0) {
//打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
//根据用户名查询用户信息
User user = userMapper.selectById(id);
//校验用户是否存在
if(user == null){
// 打印日志
log.warn(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString() + ", user id = " + id);
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS));
}
//构造要更新的对象
User updateUser = new User();
updateUser.setId(user.getId());//用户Id
updateUser.setArticleCount(user.getArticleCount() - 1);//帖子数量-1
//判断-1之后,用户的发帖数是否小于0
if(updateUser.getArticleCount() < 0){
//如果小于0,则设置为0
updateUser.setArticleCount(0);
}
//调用DAO
int row = userMapper.updateByPrimaryKeySelective(updateUser);
if (row != 1) {
// 打印日志
log.warn(ResultCode.FAILED.toString() + ",受影响的行数不等于1.");
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED));
}
@Override
public void deleteById(Long id) {
//非空校验
if (id == null || id <= 0) {
// 打印日志
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
//根据Id查询帖子信息
Article article = articleMapper.selectByPrimaryKey(id);
if (article == null || article.getDeleteState() == 1) {
//打印日志
log.info(ResultCode.FAILED_ARTICLE_NOT_EXISTS.toString() + ",article id = " + id);
//抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_NOT_EXISTS));
}
//构造更新对象
Article updateArticle = new Article();
updateArticle.setId(article.getId());
updateArticle.setDeleteState((byte) 1);
//更新数据库
int row = articleMapper.updateByPrimaryKeySelective(updateArticle);
if (row != 1) {
log.info(ResultCode.ERROR_SERVICES.toString());
throw new ApplicationException(AppResult.failed(ResultCode.ERROR_SERVICES));
}
//更新版块中的帖子数量
boardService.subOneArticleCountById(article.getUserId());
//更新用户发帖数
userService.subOneArticleCountById(article.getUserId());
log.info("删除帖子成功,article id = " + article.getId() + ",user id = " + article.getUserId() + ".");
}
4.3.1.4 实现Controller
/**
* 根据Id删除帖⼦
*
* @param id 帖⼦Id
* @return
*/
@ApiOperation("删除帖⼦")
@PostMapping("/delete")
public AppResult deleteById (HttpServletRequest request,
@ApiParam("帖⼦Id") @RequestParam("id") @NonNull Long
id) {
// 1. ⽤⼾是否禁⾔
HttpSession session = request.getSession(false);
User user = (User) session.getAttribute(AppConfig.SESSION_USER_KEY);
if (user.getState() == 1) {
// 返回错误描述
return AppResult.failed(ResultCode.FAILED_USER_BANNED);
}
//查询帖子信息
Article article = articleService.selectById(id);
//2.帖子是否存在或已删除
if (article == null || article.getDeleteState() == 1) {
// 返回错误描述
return AppResult.failed(ResultCode.FAILED_ARTICLE_NOT_EXISTS);
}
// 3. ⽤⼾是否是作者
if (article.getUserId() != user.getId()) {
// 返回错误描述
return AppResult.failed(ResultCode.FAILED_FORBIDDEN);
}
// 调⽤Service执⾏删除
articleService.deleteById(id);
// 返回成功
return AppResult.success();
}
五、项目测试
测试环境
操作系统:Windows 11 家庭版
项目运行:IDEA2022.2.3、maven、JDK1.8
浏览器:Chorme、FireFox
网络:127.0.0.1:8080
测试技术: 主要采用自动化测试以及手工测试
测试人员: 李点点
项目名称
PersonalBlog博客园
开发时间
2023年7月--2023年8月
风险
项目上线风险:无风险
测试用例 云上社群系统测试用例 - 飞书云文档 (feishu.cn)
编写功能测试用例,对每个模块进行功能测试
登录模块:
场景1:
用swagger框架进行API管理和接口规整,并对每一个接口都进行了测试
用户登录:
单元测试
@SpringBootTest
class ArticleServiceImplTest {
@Resource
private IArticleService articleService;
@Resource
private ObjectMapper objectMapper;
@Test
void selectAll() throws JsonProcessingException {
List<Article> articles = articleService.selectAll();
System.out.println(objectMapper.writeValueAsString(articles));
}
@Test
void selectByBoardId() throws JsonProcessingException {
// JAVA 版块
List<Article> articles = articleService.selectByBoardId(1l);
System.out.println(objectMapper.writeValueAsString(articles));
// C++版块
articles = articleService.selectByBoardId(2l);
System.out.println(objectMapper.writeValueAsString(articles));
// 不存在的版块
articles = articleService.selectByBoardId(100l);
System.out.println(objectMapper.writeValueAsString(articles));
}
@Test
@Transactional
void create() {
Article article = new Article();
article.setBoardId(9l);
article.setUserId(1l);
article.setTitle("单元测试标题1");
article.setContent("单元测试内容1");
// 调用Service
articleService.create(article);
System.out.println("写入成功");
}
@Test
@Transactional
void selectById() throws JsonProcessingException {
Article article = articleService.selectById(1l);
System.out.println(objectMapper.writeValueAsString(article));
Article article2 = articleService.selectById(5l);
System.out.println(objectMapper.writeValueAsString(article));
Article article3 = articleService.selectById(3l);
System.out.println(objectMapper.writeValueAsString(article));
}
@Test
@Transactional
void updateVisitCountById() {
articleService.updateVisitCountById(1l);
System.out.println("更新成功");
articleService.updateVisitCountById(100l);
System.out.println("更新成功");
}
@Test
@Transactional
void modify() {
articleService.modify(11l, "测试提示效果111", "测试提示效果111");
System.out.println("更新成功");
}
@Test
@Transactional
void updateById() {
Article article = new Article();
article.setId(1l);
article.setUpdateTime(new Date());
articleService.updateById(article);
System.out.println("更新成功");
}
@Test
void selectByUserId() throws JsonProcessingException {
List<Article> articleList = articleService.selectByUserId(1l);
System.out.println(objectMapper.writeValueAsString(articleList));
articleList = articleService.selectByUserId(2l);
System.out.println(objectMapper.writeValueAsString(articleList));
articleList = articleService.selectByUserId(100l);
System.out.println(objectMapper.writeValueAsString(articleList));
}
}
登录页面:
@SpringBootTest
class UserServiceImplTest {
@Resource
private IUserService userService;
@Resource
private ObjectMapper objectMapper;
@Test
void selectByName() throws JsonProcessingException {
User user = userService.selectByName("boy");
System.out.println(objectMapper.writeValueAsString(user));
System.out.println("============================================");
user = userService.selectByName("boy111");
System.out.println(objectMapper.writeValueAsString(user));
user = userService.selectByName("");
System.out.println(objectMapper.writeValueAsString(user));
}
@Test
void createNormalUser() {
//构造用户
User user = new User();
user.setUsername("TestUser");
user.setNickname("单元测试yonghu");
user.setPassword("123456");
user.setSalt("123456");
//调用Service
userService.createNormalUser(user);
System.out.println("注册成功");
System.out.println("====================================");
user.setUsername("bitboy");
//调用Service
userService.createNormalUser(user);
System.out.println("注册成功");
}
@Test
void login() throws JsonProcessingException {
User user = userService.login("ld","123456");
System.out.println(objectMapper.writeValueAsString(user));
//
// user = userService.login("123456","123456");
// System.out.println(objectMapper.writeValueAsString(user));
}
@Test
@Transactional
void selectById() throws JsonProcessingException {
User user = userService.selectById(1l);
System.out.println(objectMapper.writeValueAsString(user));
User user2 = userService.selectById(2l);
System.out.println(objectMapper.writeValueAsString(user2));
User user3 = userService.selectById(20l);
System.out.println(objectMapper.writeValueAsString(user3));
}
@Test
@Transactional
void addOneArticleCountById() {
userService.addOneArticleCountById(1l);
System.out.println("更新成功");
userService.addOneArticleCountById(2l);
System.out.println("更新成功");
//
// userService.addOneArticleCountById(100l);
// System.out.println("更新成功");
}
@Test
@Transactional
void modifyInfo() {
User user = new User();
user.setId(2l);
user.setNickname("girl111");
user.setGender((byte) 0);
user.setPhoneNum("15319706666");
user.setEmail("qq@qq.com");
user.setRemark("我是一个美丽的小女孩");
// 调用Service
userService.modifyInfo(user);
System.out.println("更新成功");
}
@Test
void modifyPassword() {
userService.modifyPassword(1l, "654321", "123456");
System.out.println("更新成功");
}
}
版块页面:
@SpringBootTest
class BoardServiceImplTest {
@Resource
private IBoardService boardService;
@Resource
private ObjectMapper objectMapper;
@Test
void selectByNum() throws JsonProcessingException {
List<Board> boards = boardService.selectByNum(9);
System.out.println(objectMapper.writeValueAsString(boards));
}
@Test
@Transactional
void selectById() throws JsonProcessingException {
// JAVA
Board board = boardService.selectById(1l);
System.out.println(objectMapper.writeValueAsString(board));
// C++
board = boardService.selectById(2l);
System.out.println(objectMapper.writeValueAsString(board));
// 不存在
board = boardService.selectById(100l);
System.out.println(objectMapper.writeValueAsString(board));
}
@Test
@Transactional
void addOneArticleCountById() {
boardService.addOneArticleCountById(1l);
System.out.println("更新成功");
boardService.addOneArticleCountById(2l);
System.out.println("更新成功");
// boardService.addOneArticleCountById(100l);
// System.out.println("更新成功");
}
}