Build a Tech Blog from Scratch: Spring Boot + Vue3 Full‑Stack Tutorial (Part 1)
This article launches a new series that walks through building a complete technical blog system from the ground up, detailing the chosen Spring Boot and Vue3 stack, phased feature roadmap, database schema, project structure, and initial unified response and global exception handling code.
Technology Selection
Frontend framework: Vue3 + Composition API – flexible code organization, TypeScript‑friendly.
Build tool: Vite – fast cold start and instant hot updates.
UI component library: Element Plus – mature in China with comprehensive documentation.
State management: Pinia – lightweight official replacement for Vuex.
HTTP client: Axios – widely used.
Backend framework: Spring Boot 2.7.x – extensive ecosystem.
ORM: MyBatis‑Plus – reduces about 80% of CRUD code.
Database: MySQL 8.0 + Redis – conventional combination for persistence and caching.
Security: Spring Security + JWT – standard front‑back separation for authentication.
API documentation: Knife4j (enhanced Swagger) – auto‑generated, convenient for debugging.
File storage: Qiniu Cloud / Alibaba Cloud OSS – store images externally rather than in the database.
Feature Planning
Phase 1 – Basic Version
User registration/login (JWT)
Article list and detail pages
Article categories and tags
Comment system (post & reply)
Admin backend for managing articles and categories
Phase 2 – Advanced Version
Article search powered by Elasticsearch with tokenization
Read‑count statistics using Redis deduplication
Like & favorite functionality
Follow / follower relationships
Personal homepage with activity feed
Friendship links
Phase 3 – High‑End Version
Message notifications via WebSocket and internal messaging
Third‑party login (GitHub / WeChat)
Article review workflow with sensitive‑word filtering
Data statistics (UV/PV, daily active users)
Rich‑text editor supporting Markdown and drag‑and‑drop image upload
Database Design (Core Tables)
User Table user
CREATE TABLE `user` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '加密密码',
`nickname` varchar(50) COMMENT '昵称',
`avatar` varchar(500) COMMENT '头像URL',
`email` varchar(100) COMMENT '邮箱',
`role` varchar(20) DEFAULT 'user' COMMENT 'user/admin',
`status` tinyint DEFAULT 1 COMMENT '1-正常 0-禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uk_username` (`username`)
);Article Table article
CREATE TABLE `article` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`title` varchar(200) NOT NULL COMMENT '标题',
`summary` varchar(500) COMMENT '摘要',
`content` longtext COMMENT 'Markdown内容',
`cover_image` varchar(500) COMMENT '封面图',
`category_id` bigint COMMENT '分类ID',
`user_id` bigint NOT NULL COMMENT '作者ID',
`view_count` int DEFAULT 0 COMMENT '阅读量',
`like_count` int DEFAULT 0 COMMENT '点赞数',
`comment_count` int DEFAULT 0 COMMENT '评论数',
`status` tinyint DEFAULT 1 COMMENT '1-发布 2-草稿 3-私密',
`is_top` tinyint DEFAULT 0 COMMENT '0-不置顶 1-置顶',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY `idx_category` (`category_id`),
KEY `idx_user` (`user_id`),
KEY `idx_create_time` (`create_time`)
);Category Table category
CREATE TABLE `category` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '分类名',
`sort_order` int DEFAULT 0 COMMENT '排序',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP
);Article‑Tag Association article_tag and Tag Table tag
CREATE TABLE `article_tag` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`article_id` bigint NOT NULL,
`tag_id` bigint NOT NULL,
UNIQUE KEY `uk_article_tag` (`article_id`, `tag_id`)
);
CREATE TABLE `tag` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`name` varchar(30) NOT NULL COMMENT '标签名',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_name` (`name`)
);Comment Table comment
CREATE TABLE `comment` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`article_id` bigint NOT NULL,
`user_id` bigint NOT NULL,
`parent_id` bigint DEFAULT 0 COMMENT '父评论ID,0表示顶级评论',
`reply_to_user_id` bigint COMMENT '回复的目标用户ID',
`content` varchar(1000) NOT NULL,
`like_count` int DEFAULT 0,
`status` tinyint DEFAULT 1 COMMENT '1-正常 0-删除',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
KEY `idx_article` (`article_id`),
KEY `idx_user` (`user_id`)
);Project Initialization
Backend Structure (Maven Multi‑Module)
blog-backend/
├── blog-common/ # Common utilities and unified response
├── blog-framework/ # Framework config (Security, MyBatis‑Plus)
├── blog-generator/ # MyBatis‑Plus code generator
├── blog-admin/ # Admin API
├── blog-api/ # Front‑end API
└── blog-system/ # System management (users, roles)Frontend Structure (Vite + Vue3)
blog-frontend/
├── src/
│ ├── api/ # API wrappers
│ ├── assets/ # Static assets
│ ├── components/ # Shared components
│ ├── composables/ # Composition functions
│ ├── layout/ # Layout components
│ ├── router/ # Route definitions
│ ├── stores/ # Pinia stores
│ ├── utils/ # Utility functions
│ ├── views/ # Pages
│ │ ├── home/ # Home page
│ │ ├── article/ # Article detail
│ │ ├── user/ # User center
│ │ └── admin/ # Admin panel
│ └── main.js
├── index.html
└── vite.config.jsUnified Response & Global Exception Handling
Result<T> Class
@Data
public class Result<T> {
private Integer code;
private String msg;
private T data;
private Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> error(String msg) {
return new Result<>(500, msg, null);
}
public static <T> Result<T> error(Integer code, String msg) {
return new Result<>(code, msg, null);
}
}GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(";"));
return Result.error(400, msg);
}
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统繁忙,请稍后重试");
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Coder Trainee
Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
