本项目实战教程配有免费视频教程,配套代码完全开源。手把手从零开始搭建一个目前应用最广泛的 Springboot+Vue 前后端分离多用户社区项目。本项目难度适中,为便于大家学习,每一集视频教程对应在 Github 上的每一次提交。
Vue Vuex Vue Router Axios Bulma Buefy Element Vditor DarkReader
Spring Boot Mysql Mybatis MyBatis-Plus Spring Security JWT Lombok
在数据库导入
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80022
Source Host : localhost:3306
Source Schema : doubao
Target Server Type : MySQL
Target Server Version : 80022
File Encoding : 65001
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for bms_billboard
-- ----------------------------
DROP TABLE IF EXISTS `bms_billboard`;
CREATE TABLE `bms_billboard` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`content` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '公告',
`create_time` datetime NULL DEFAULT NULL COMMENT '公告时间',
`show` tinyint(1) NULL DEFAULT NULL COMMENT '1:展示中,0:过期',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COMMENT = '全站公告' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of bms_billboard
-- ----------------------------
INSERT INTO `bms_billboard` VALUES (2, 'R1.0 开始已实现护眼模式 ,妈妈再也不用担心我的眼睛了。', '2020-11-19 17:16:19', 0);
INSERT INTO `bms_billboard` VALUES (4, '系统已更新至最新版 1.0.1', NULL, 1);
-- ----------------------------
-- Table structure for bms_follow
-- ----------------------------
DROP TABLE IF EXISTS `bms_follow`;
CREATE TABLE `bms_follow` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`parent_id` varchar(20) CHARACTER SET utf8mb4 NULL DEFAULT NULL COMMENT '被关注人 ID',
`follower_id` varchar(20) CHARACTER SET utf8mb4 NULL DEFAULT NULL COMMENT '关注人 ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 130 CHARACTER SET = utf8mb4 COMMENT = '用户关注' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of bms_follow
-- ----------------------------
INSERT INTO `bms_follow` VALUES (65, '1329723594994229250', '1317498859501797378');
INSERT INTO `bms_follow` VALUES (85, '1332912847614083073', '1332636310897664002');
INSERT INTO `bms_follow` VALUES (129, '1349290158897311745', '1349618748226658305');
-- ----------------------------
-- Table structure for bms_post
-- ----------------------------
DROP TABLE IF EXISTS `bms_post`;
CREATE TABLE `bms_post` (
`id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
`title` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '标题',
`content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT 'markdown 内容',
`user_id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '作者 ID',
`comments` int NOT NULL DEFAULT 0 COMMENT '评论统计',
`collects` int NOT NULL DEFAULT 0 COMMENT '收藏统计',
`view` int NOT NULL DEFAULT 0 COMMENT '浏览统计',
`top` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否置顶,1-是,0-否',
`essence` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否加精,1-是,0-否',
`section_id` int NULL DEFAULT 0 COMMENT '专栏 ID',
`create_time` datetime NOT NULL COMMENT '发布时间',
`modify_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
UNIQUE INDEX `title`(`title`) USING BTREE,
INDEX `user_id`(`user_id`) USING BTREE,
INDEX `create_time`(`create_time`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '话题表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for bms_comment
-- ----------------------------
DROP TABLE IF EXISTS `bms_comment`;
CREATE TABLE `bms_comment` (
`id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
`content` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '内容',
`user_id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '作者 ID',
`topic_id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'topic_id',
`create_time` datetime NOT NULL COMMENT '发布时间',
`modify_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '评论表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for ums_user
-- ----------------------------
DROP TABLE IF EXISTS `ums_user`;
CREATE TABLE `ums_user` (
`id` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户 ID',
`username` varchar(15) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '用户名',
`alias` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户昵称',
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '密码',
`avatar` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机',
`score` int NOT NULL DEFAULT 0 COMMENT '积分',
`token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT 'token',
`bio` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '个人简介',
`active` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否激活,1:是,0:否',
`status` bit(1) NULL DEFAULT b'1' COMMENT '状态,1:使用,0:停用',
`role_id` int NULL DEFAULT NULL COMMENT '用户角色',
`create_time` datetime NOT NULL COMMENT '加入时间',
`modify_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `user_name`(`username`) USING BTREE,
INDEX `user_email`(`email`) USING BTREE,
INDEX `user_create_time`(`create_time`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of ums_user
-- ----------------------------
INSERT INTO `ums_user` VALUES ('1349290158897311745', 'admin', 'admin', '$2a$10$8qx711TBg/2hxfL7N.sxf.0ROMhR/iuPhQx33IFqGd7PLgt5nGJTO', 'https://s3.ax1x.com/2020/12/01/DfHNo4.jpg', '23456@qq.com', NULL, 2, '', '自由职业者', b'1', b'1', NULL, '2021-01-13 17:40:17', NULL);
INSERT INTO `ums_user` VALUES ('1349618748226658305', 'zhangsan', 'zhangsan', '$2a$10$7K3yYv8sMV5Xsc2facXTcuyDo8JQ4FJHvjZ7qtWYcJdei3Q6Fvqdm', 'https://s3.ax1x.com/2020/12/01/DfHNo4.jpg', '23456@qq.com', NULL, 0, '', '自由职业者', b'1', b'1', NULL, '2021-01-14 15:25:59', NULL);
SET FOREIGN_KEY_CHECKS = 1;
创建 maven 工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<mybatis-plus.version>3.4.2</mybatis-plus.version>
<fastjson.version>1.2.75</fastjson.version>
<hutool.version>5.5.7</hutool.version>
<jwt.version>0.9.1</jwt.version>
<emoji-java.version>5.1.1</emoji-java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!--emoji-java 表情包-->
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId>
<version>${emoji-java.version}</version>
</dependency>
<!-- lettuce pool 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--yaml-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--bean validator-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
# 端口号
server:
port: 8081
# 数据库
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/nodepad_mblog?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=GMT%2B8
type: com.zaxxer.hikari.HikariDataSource
# sql 日志打印
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
// 我们这里还没有配置数据库,exclude = {DataSourceAutoConfiguration.class 就是启动时不加载数据库
// @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@Slf4j
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(BlogApplication.class, args);
String port = context.getEnvironment().getProperty("server.port");
log.info("http://localhost:"+port);
}
}
启动项目测试有没有报错
public interface IErrorCode {
/**
* 错误编码: -1 失败;200 成功
*
* @return 错误编码
*/
Integer getCode();
/**
* 错误描述
*
* @return 错误描述
*/
String getMessage();
}
public enum ApiErrorCode implements IErrorCode {
/**
* 成功
*/
SUCCESS(200, "操作成功"),
/**
* 失败
*/
FAILED(-1, "操作失败"),
/**
* 未登录,Token 过期
*/
UNAUTHORIZED(401, "暂未登录或 token 已经过期"),
/**
* 权限不足
*/
FORBIDDEN(403, "权限不足"),
/**
* 参数校验错误
*/
VALIDATE_FAILED(404, "参数检验失败");
private final Integer code;
private final String message;
ApiErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
@Override
public String toString() {
return "ApiErrorCode{" +
"code=" + code +
", message='" +message + '\'' +
'}';
}
}
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Optional;
@Data
@NoArgsConstructor
public class ApiResult<T> implements Serializable {
private static final long serialVersionUID = -4153430394359594346L;
/**
* 业务状态码
*/
private long code;
/**
* 结果集
*/
private T data;
/**
* 接口描述
*/
private String message;
/**
* 全参
*
* @param code 业务状态码
* @param message 描述
* @param data 结果集
*/
public ApiResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public ApiResult(IErrorCode errorCode) {
errorCode = Optional.ofNullable(errorCode).orElse(ApiErrorCode.FAILED);
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
/**
* 成功
*
* @param data 结果集
* @return {code:200,message:操作成功,data:自定义}
*/
public static <T> ApiResult<T> success() {
return new ApiResult<T>(ApiErrorCode.SUCCESS.getCode(), ApiErrorCode.SUCCESS.getMessage(), null);
}
/**
* 成功
*
* @param data 结果集
* @return {code:200,message:操作成功,data:自定义}
*/
public static <T> ApiResult<T> success(T data) {
return new ApiResult<T>(ApiErrorCode.SUCCESS.getCode(), ApiErrorCode.SUCCESS.getMessage(), data);
}
/**
* 成功
*
* @param data 结果集
* @param message 自定义提示信息
* @return {code:200,message:自定义,data:自定义}
*/
public static <T> ApiResult<T> success(T data, String message) {
return new ApiResult<T>(ApiErrorCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
*/
public static <T> ApiResult<T> failed() {
return failed(ApiErrorCode.FAILED);
}
/**
* 失败返回结果
*
* @param message 提示信息
* @return {code:枚举 ApiErrorCode 取,message:自定义,data:null}
*/
public static <T> ApiResult<T> failed(String message) {
return new ApiResult<T>(ApiErrorCode.FAILED.getCode(), message, null);
}
/**
* 失败
*
* @param errorCode 错误码
* @return {code:封装接口取,message:封装接口取,data:null}
*/
public static <T> ApiResult<T> failed(IErrorCode errorCode) {
return new ApiResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
*
* @param errorCode 错误码
* @param message 错误信息
* @return {code:枚举 ApiErrorCode 取,message:自定义,data:null}
*/
public static <T> ApiResult<T> failed(IErrorCode errorCode, String message) {
return new ApiResult<T>(errorCode.getCode(), message, null);
}
/**
* 参数验证失败返回结果
*/
public static <T> ApiResult<T> validateFailed() {
return failed(ApiErrorCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
*
* @param message 提示信息
*/
public static <T> ApiResult<T> validateFailed(String message) {
return new ApiResult<T>(ApiErrorCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> ApiResult<T> unauthorized(T data) {
return new ApiResult<T>(ApiErrorCode.UNAUTHORIZED.getCode(), ApiErrorCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> ApiResult<T> forbidden(T data) {
return new ApiResult<T>(ApiErrorCode.FORBIDDEN.getCode(), ApiErrorCode.FORBIDDEN.getMessage(), data);
}
}
public class ApiException extends RuntimeException {
private IErrorCode errorCode;
public ApiException(IErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public ApiException(String message) {
super(message);
}
public IErrorCode getErrorCode() {
return errorCode;
}
}
public class ApiAsserts {
/**
* 抛失败异常
*
* @param message 说明
*/
public static void fail(String message) {
throw new ApiException(message);
}
/**
* 抛失败异常
*
* @param errorCode 状态码
*/
public static void fail(IErrorCode errorCode) {
throw new ApiException(errorCode);
}
}
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 捕获自定义异常
*/
@ResponseBody
@ExceptionHandler(value = ApiException.class)
public ApiResult<Map<String, Object>> handle(ApiException e) {
if (e.getErrorCode() != null) {
return ApiResult.failed(e.getErrorCode());
}
return ApiResult.failed(e.getMessage());
}
}
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.