diff --git a/backend/pom.xml b/backend/pom.xml
index 63fd831..2661289 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -19,6 +19,19 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
diff --git a/backend/src/main/java/com/example/blog/BlogApplication.java b/backend/src/main/java/com/example/blog/BlogApplication.java
index 8da4687..5bef686 100644
--- a/backend/src/main/java/com/example/blog/BlogApplication.java
+++ b/backend/src/main/java/com/example/blog/BlogApplication.java
@@ -2,231 +2,10 @@ package com.example.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.web.bind.annotation.*;
-
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.*;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.stream.Collectors;
@SpringBootApplication
-@RestController
-@CrossOrigin(origins = "*")
-@RequestMapping("/api")
public class BlogApplication {
- public static class Post {
- private String id;
- private String title;
- private String summary;
- private String content;
- private String author;
- private String date;
- private String category;
- private List tags;
- private String readTime;
- private int views;
- private int likes;
-
- public Post() {}
-
- public Post(String id, String title, String summary, String content, String author,
- String date, String category, List tags, String readTime, int views, int likes) {
- this.id = id;
- this.title = title;
- this.summary = summary;
- this.content = content;
- this.author = author;
- this.date = date;
- this.category = category;
- this.tags = tags;
- this.readTime = readTime;
- this.views = views;
- this.likes = likes;
- }
-
- public String getId() { return id; }
- public void setId(String id) { this.id = id; }
- public String getTitle() { return title; }
- public void setTitle(String title) { this.title = title; }
- public String getSummary() { return summary; }
- public void setSummary(String summary) { this.summary = summary; }
- public String getContent() { return content; }
- public void setContent(String content) { this.content = content; }
- public String getAuthor() { return author; }
- public void setAuthor(String author) { this.author = author; }
- public String getDate() { return date; }
- public void setDate(String date) { this.date = date; }
- public String getCategory() { return category; }
- public void setCategory(String category) { this.category = category; }
- public List getTags() { return tags; }
- public void setTags(List tags) { this.tags = tags; }
- public String getReadTime() { return readTime; }
- public void setReadTime(String readTime) { this.readTime = readTime; }
- public int getViews() { return views; }
- public void setViews(int views) { this.views = views; }
- public int getLikes() { return likes; }
- public void setLikes(int likes) { this.likes = likes; }
- }
-
- private static final List POSTS = new CopyOnWriteArrayList<>();
-
- static {
- POSTS.add(new Post(
- "1",
- "云原生实践:在 Google Cloud 台湾节点搭建高性能 Ubuntu 与 1Panel",
- "本文记录了如何通过 GCP 赠金在亚洲低延迟机房部署 4核16G 高性能云服务器,并使用现代化容器面板 1Panel 进行运维管理实录。",
- "# 云原生实践:在 Google Cloud 台湾节点搭建高性能服务器\n\n" +
- "在挑选云服务器的过程中,综合对比了各大厂商的免费策略与性能表现,最终选择了 **Google Cloud 台湾节点 (`asia-east1`)**。\n\n" +
- "## 核心硬件与网络配置\n\n" +
- "- **规格**:`e2-standard-4`(4 vCPU,16 GB 内存)\n" +
- "- **系统**:Ubuntu 22.04 LTS (x86_64)\n" +
- "- **延迟**:到中国大陆仅 30~50ms,操作极为丝滑。\n\n" +
- "## 为什么采用 1Panel 面板?\n\n" +
- "相比传统臃肿的面料,1Panel 深度拥抱 **Docker 容器化**,所有应用(MySQL、OpenResty、Gitea)完全容器隔离,真正做到“环境随用随建,数据挂载持久化”。\n\n" +
- "```bash\n# 快速检查 Docker 运行状态\ndocker ps --format \"table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\"\n```\n\n" +
- "> 💡 **运维心得**:永远在云平台控制台统一放行防火墙端口,切勿盲目在系统内部启用 UFW,避免失联风险。",
- "旺总",
- "2026-10-22",
- "云原生 & Linux",
- Arrays.asList("GCP", "Ubuntu", "1Panel", "Linux"),
- "4 min",
- 528,
- 42
- ));
-
- POSTS.add(new Post(
- "2",
- "探索本地大模型:从 Ollama 运行 DeepSeek R1 到 CPU 推理性能调优",
- "深度解析在纯 CPU 服务器上运行 DeepSeek R1 和通义千问 2.5 的真实表现,揭开 2050 Token 预填充延迟背后的机制。",
- "# 探索本地大模型:从 Ollama 到 Open WebUI 性能调优实战\n\n" +
- "近期 DeepSeek R1 推理思考模型火遍全球,我们在 4核16G 的服务器上进行了本地大模型的全链路压测。\n\n" +
- "## 1. 架构搭建\n\n" +
- "- **模型发动机**:`Ollama` 容器\n" +
- "- **交互界面**:`Open WebUI` 容器\n" +
- "- **网络通信**:通过 Docker 网关 `http://172.17.0.1:11434` 极速直连\n\n" +
- "## 2. 性能排查与调优秘籍\n\n" +
- "测试过程中遇到了“每次提问卡顿 50 秒”的问题,经过 `top` 分析和指标剖析:\n\n" +
- "1. **解除容器 CPU 配额**:确保 4 线程全开(`eval rate` 提升至 14 tokens/s);\n" +
- "2. **内存永久常驻**:注入 `OLLAMA_KEEP_ALIVE=-1`,避免 5 分钟超时后重新读盘;\n" +
- "3. **清空全局默认 Prompt**:消除了默认附带的 2050 历史 Token,读题耗时从 60s 骤降至 0.2s!\n\n" +
- "```bash\n# 测试模型运行性能\ndocker exec -it 1Panel-ollama-nzre ollama run qwen2.5:1.5b --verbose\n```\n\n" +
- "> 纯 CPU 跑 7B 参数是物理带宽瓶颈,日常开发推荐搭配 1.5B~3B 模型,速度与逻辑达到最佳平衡!",
- "旺总",
- "2026-10-22",
- "AI & 本地模型",
- Arrays.asList("DeepSeek", "Ollama", "OpenWebUI", "性能调优"),
- "6 min",
- 892,
- 68
- ));
-
- POSTS.add(new Post(
- "3",
- "全栈工程师实战:Spring Boot 3 + Vue 3 博客与 Gitea CI/CD 自动化流水线",
- "从零手写前后端分离架构,配合私有 Gitea 仓库与自动化部署脚本,实现 push 代码后全自动打包上线。",
- "# 全栈工程化实践:Spring Boot 3 + Vue 3 + CI/CD 自动化\n\n" +
- "现代化开发的精髓在于**“把重复劳动交给自动化”**。我们搭建了一套小而美但功能极其完备的企业级全栈流水线。\n\n" +
- "## 全栈架构一览\n\n" +
- "```text\n[开发者本地 IDEA] ──git push──▶ [私有 Gitea 仓库]\n │\n (Webhook 触发流水线)\n ▼\n [deploy.sh 自动化脚本]\n │\n ┌─────────────┴─────────────┐\n ▼ ▼\n [Spring Boot 3 容器] [Nginx 静态反代容器]\n```\n\n" +
- "## Nginx 反向代理配置关键点\n\n" +
- "前端容器中通过反代 `/api/` 到后端服务容器,天然解决跨域和外部端口分散问题:\n\n" +
- "```nginx\nlocation /api/ {\n proxy_pass http://backend:8080/api/;\n proxy_set_header Host $host;\n}\n```\n\n" +
- "从此,改完代码只需一条 `git push`,剩下的事情交给服务器自动化即可!",
- "旺总",
- "2026-10-22",
- "全栈开发 & CI/CD",
- Arrays.asList("Spring Boot", "Vue 3", "CI/CD", "Docker Compose"),
- "5 min",
- 641,
- 55
- ));
- }
-
- @GetMapping("/posts")
- public List getPosts(
- @RequestParam(required = false) String category,
- @RequestParam(required = false) String search) {
- return POSTS.stream()
- .filter(p -> category == null || category.isEmpty() || category.equalsIgnoreCase("all") || p.getCategory().equalsIgnoreCase(category))
- .filter(p -> search == null || search.isEmpty() ||
- p.getTitle().toLowerCase().contains(search.toLowerCase()) ||
- p.getSummary().toLowerCase().contains(search.toLowerCase()))
- .collect(Collectors.toList());
- }
-
- @GetMapping("/posts/{id}")
- public Post getPost(@PathVariable String id) {
- return POSTS.stream()
- .filter(p -> p.getId().equals(id))
- .findFirst()
- .map(p -> {
- p.setViews(p.getViews() + 1);
- return p;
- })
- .orElse(null);
- }
-
- @PostMapping("/posts")
- public Post addPost(@RequestBody Post post) {
- String newId = String.valueOf(POSTS.size() + 1);
- post.setId(newId);
- if (post.getDate() == null || post.getDate().isEmpty()) {
- post.setDate(LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
- }
- if (post.getAuthor() == null || post.getAuthor().isEmpty()) {
- post.setAuthor("旺总");
- }
- if (post.getCategory() == null || post.getCategory().isEmpty()) {
- post.setCategory("技术杂谈");
- }
- if (post.getReadTime() == null || post.getReadTime().isEmpty()) {
- post.setReadTime("3 min");
- }
- if (post.getTags() == null || post.getTags().isEmpty()) {
- post.setTags(Collections.singletonList(post.getCategory()));
- }
- post.setViews(1);
- post.setLikes(0);
- POSTS.add(0, post);
- return post;
- }
-
- @PostMapping("/posts/{id}/like")
- public Map likePost(@PathVariable String id) {
- Map res = new HashMap<>();
- for (Post p : POSTS) {
- if (p.getId().equals(id)) {
- p.setLikes(p.getLikes() + 1);
- res.put("success", true);
- res.put("likes", p.getLikes());
- return res;
- }
- }
- res.put("success", false);
- return res;
- }
-
- @DeleteMapping("/posts/{id}")
- public Map deletePost(@PathVariable String id) {
- boolean removed = POSTS.removeIf(p -> p.getId().equals(id));
- Map res = new HashMap<>();
- res.put("success", removed);
- return res;
- }
-
- @GetMapping("/stats")
- public Map getStats() {
- Map stats = new HashMap<>();
- stats.put("totalPosts", POSTS.size());
- stats.put("totalViews", POSTS.stream().mapToInt(Post::getViews).sum());
- stats.put("totalLikes", POSTS.stream().mapToInt(Post::getLikes).sum());
- Set categories = POSTS.stream().map(Post::getCategory).collect(Collectors.toSet());
- stats.put("categoriesCount", categories.size());
- return stats;
- }
-
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
diff --git a/backend/src/main/java/com/example/blog/common/GlobalExceptionHandler.java b/backend/src/main/java/com/example/blog/common/GlobalExceptionHandler.java
new file mode 100644
index 0000000..47c4b08
--- /dev/null
+++ b/backend/src/main/java/com/example/blog/common/GlobalExceptionHandler.java
@@ -0,0 +1,16 @@
+package com.example.blog.common;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Result handleException(Exception e) {
+ return Result.error(e.getMessage() != null ? e.getMessage() : "服务器内部错误");
+ }
+}
diff --git a/backend/src/main/java/com/example/blog/common/Result.java b/backend/src/main/java/com/example/blog/common/Result.java
new file mode 100644
index 0000000..aa6d86a
--- /dev/null
+++ b/backend/src/main/java/com/example/blog/common/Result.java
@@ -0,0 +1,38 @@
+package com.example.blog.common;
+
+public class Result {
+ private int code;
+ private String message;
+ private T data;
+
+ public Result() {}
+
+ public Result(int code, String message, T data) {
+ this.code = code;
+ this.message = message;
+ this.data = data;
+ }
+
+ public static Result success(T data) {
+ return new Result<>(200, "操作成功", data);
+ }
+
+ public static Result success() {
+ return new Result<>(200, "操作成功", null);
+ }
+
+ public static Result error(String message) {
+ return new Result<>(500, message, null);
+ }
+
+ public static Result error(int code, String message) {
+ return new Result<>(code, message, null);
+ }
+
+ public int getCode() { return code; }
+ public void setCode(int 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; }
+}
diff --git a/backend/src/main/java/com/example/blog/config/DataInitializer.java b/backend/src/main/java/com/example/blog/config/DataInitializer.java
new file mode 100644
index 0000000..b9f937c
--- /dev/null
+++ b/backend/src/main/java/com/example/blog/config/DataInitializer.java
@@ -0,0 +1,103 @@
+package com.example.blog.config;
+
+import com.example.blog.entity.Category;
+import com.example.blog.entity.Post;
+import com.example.blog.repository.CategoryRepository;
+import com.example.blog.repository.PostRepository;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Component
+public class DataInitializer implements CommandLineRunner {
+
+ private final PostRepository postRepository;
+ private final CategoryRepository categoryRepository;
+
+ public DataInitializer(PostRepository postRepository, CategoryRepository categoryRepository) {
+ this.postRepository = postRepository;
+ this.categoryRepository = categoryRepository;
+ }
+
+ @Override
+ public void run(String... args) {
+ // 1. 初始化分类
+ if (categoryRepository.count() == 0) {
+ List categories = Arrays.asList(
+ new Category("云原生 & Linux", "容器化编排、GCP/云服务器运维、系统调优与网络配置实战"),
+ new Category("Java & Spring Boot", "Spring Boot 3 微服务架构、JPA 数据库持久化与后端工程规范"),
+ new Category("AI & 本地模型", "Ollama 本地大模型部署、DeepSeek R1 性能压测与应用集成"),
+ new Category("全栈开发 & CI/CD", "前后端分离架构、Gitea Actions 自动化流水线交付")
+ );
+ categoryRepository.saveAll(categories);
+ }
+
+ // 2. 初始化预置文章
+ if (postRepository.count() == 0) {
+ Post p1 = new Post();
+ p1.setTitle("云原生实践:在 Google Cloud 台湾节点搭建高性能 Ubuntu 与 1Panel");
+ p1.setSummary("本文记录了如何通过 GCP 赠金在亚洲低延迟机房部署 4核16G 高性能云服务器,并使用现代化容器面板 1Panel 进行运维管理实录。");
+ p1.setContent("# 云原生实践:在 Google Cloud 台湾节点搭建高性能服务器\n\n" +
+ "在挑选云服务器的过程中,综合对比了各大厂商的免费策略与性能表现,最终选择了 **Google Cloud 台湾节点 (`asia-east1`)**。\n\n" +
+ "## 核心硬件与网络配置\n" +
+ "- **规格**:`e2-standard-4`(4 vCPU,16 GB 内存)\n" +
+ "- **系统**:Ubuntu 22.04 LTS (x86_64)\n" +
+ "- **延迟**:到中国大陆仅 30~50ms,操作极为丝滑。\n\n" +
+ "## 为什么采用 1Panel 面板?\n" +
+ "相比传统臃肿的面料,1Panel 深度拥抱 **Docker 容器化**,所有应用(MySQL、OpenResty、Gitea)完全容器隔离,真正做到“环境随用随建,数据挂载持久化”。\n\n" +
+ "```bash\n# 快速检查 Docker 运行状态\ndocker ps --format \"table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\"\n```\n\n" +
+ "> 💡 **运维心得**:永远在云平台控制台统一放行防火墙端口,切勿盲目在系统内部启用 UFW,避免失联风险。");
+ p1.setAuthor("旺总");
+ p1.setCategoryName("云原生 & Linux");
+ p1.setTags("GCP,Ubuntu,1Panel,Linux");
+ p1.setReadTime("4 min");
+ p1.setViews(682);
+ p1.setLikes(58);
+
+ Post p2 = new Post();
+ p2.setTitle("探索本地大模型:从 Ollama 运行 DeepSeek R1 到 CPU 推理性能调优");
+ p2.setSummary("深度解析在纯 CPU 服务器上运行 DeepSeek R1 和通义千问 2.5 的真实表现,揭开 2050 Token 预填充延迟背后的机制。");
+ p2.setContent("# 探索本地大模型:从 Ollama 到 Open WebUI 性能调优实战\n\n" +
+ "近期 DeepSeek R1 推理思考模型火遍全球,我们在 4核16G 的服务器上进行了本地大模型的全链路压测。\n\n" +
+ "## 1. 架构搭建\n" +
+ "- **模型发动机**:`Ollama` 容器\n" +
+ "- **交互界面**:`Open WebUI` 容器\n" +
+ "- **网络通信**:通过 Docker 网关 `http://172.17.0.1:11434` 极速直连\n\n" +
+ "## 2. 性能排查与调优秘籍\n" +
+ "测试过程中遇到了“每次提问卡顿 50 秒”的问题,经过 `top` 分析和指标剖析:\n" +
+ "1. **解除容器 CPU 配额**:确保 4 线程全开(`eval rate` 提升至 14 tokens/s);\n" +
+ "2. **内存永久常驻**:注入 `OLLAMA_KEEP_ALIVE=-1`,避免 5 分钟超时后重新读盘;\n" +
+ "3. **清空全局默认 Prompt**:消除了默认附带的 2050 历史 Token,读题耗时从 60s 骤降至 0.2s!\n\n" +
+ "```bash\n# 测试模型运行性能\ndocker exec -it 1Panel-ollama-nzre ollama run qwen2.5:1.5b --verbose\n```\n\n" +
+ "> 纯 CPU 跑 7B 参数是物理带宽瓶颈,日常开发推荐搭配 1.5B~3B 模型,速度与逻辑达到最佳平衡!");
+ p2.setAuthor("旺总");
+ p2.setCategoryName("AI & 本地模型");
+ p2.setTags("DeepSeek,Ollama,OpenWebUI,性能调优");
+ p2.setReadTime("6 min");
+ p2.setViews(1048);
+ p2.setLikes(96);
+
+ Post p3 = new Post();
+ p3.setTitle("全栈工程师实战:Spring Boot 3 + MySQL 8.0 持久化与 Gitea CI/CD 自动化交付");
+ p3.setSummary("从零构建工业级 Spring Boot 3 分层架构,结合 MySQL 8.0 数据库持久化存储与私有 Gitea Actions 自动化流水线。");
+ p3.setContent("# 全栈工程化实践:Spring Boot 3 + MySQL 8.0 + CI/CD 自动化\n\n" +
+ "现代化开发的精髓在于**“把数据交给专业数据库持久化,把重复劳动交给自动化流水线”**。\n\n" +
+ "## 全栈架构一览\n" +
+ "```text\n[开发者本地 IDEA] ──git push──▶ [私有 Gitea 仓库]\n │\n (Gitea Actions 触发流水线)\n ▼\n [Docker Compose 全栈构建]\n │\n ┌───────────────────────────┼───────────────────────────┐\n ▼ ▼ ▼\n [MySQL 8.0 容器] [Spring Boot 3 容器] [Nginx 静态反代容器]\n```\n\n" +
+ "## 数据持久化规范\n" +
+ "- 数据文件挂载至命名卷 `mysql_data`,容器重启或重建数据零丢失;\n" +
+ "- Spring Data JPA 实现对象关系映射,自动完成数据库结构维护;\n" +
+ "- 统一 RESTful API 与响应封装,全栈开发得心应手!");
+ p3.setAuthor("旺总");
+ p3.setCategoryName("全栈开发 & CI/CD");
+ p3.setTags("Spring Boot,MySQL,JPA,CI/CD");
+ p3.setReadTime("5 min");
+ p3.setViews(815);
+ p3.setLikes(72);
+
+ postRepository.saveAll(Arrays.asList(p1, p2, p3));
+ }
+ }
+}
diff --git a/backend/src/main/java/com/example/blog/controller/CategoryController.java b/backend/src/main/java/com/example/blog/controller/CategoryController.java
new file mode 100644
index 0000000..2266edc
--- /dev/null
+++ b/backend/src/main/java/com/example/blog/controller/CategoryController.java
@@ -0,0 +1,33 @@
+package com.example.blog.controller;
+
+import com.example.blog.common.Result;
+import com.example.blog.entity.Category;
+import com.example.blog.repository.CategoryRepository;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/categories")
+@CrossOrigin(origins = "*")
+public class CategoryController {
+
+ private final CategoryRepository categoryRepository;
+
+ public CategoryController(CategoryRepository categoryRepository) {
+ this.categoryRepository = categoryRepository;
+ }
+
+ @GetMapping
+ public Result> listCategories() {
+ return Result.success(categoryRepository.findAll());
+ }
+
+ @PostMapping
+ public Result createCategory(@RequestBody Category category) {
+ if (category.getName() == null || category.getName().trim().isEmpty()) {
+ throw new IllegalArgumentException("分类名称不能为空");
+ }
+ return Result.success(categoryRepository.save(category));
+ }
+}
diff --git a/backend/src/main/java/com/example/blog/controller/CommentController.java b/backend/src/main/java/com/example/blog/controller/CommentController.java
new file mode 100644
index 0000000..644ab5a
--- /dev/null
+++ b/backend/src/main/java/com/example/blog/controller/CommentController.java
@@ -0,0 +1,30 @@
+package com.example.blog.controller;
+
+import com.example.blog.common.Result;
+import com.example.blog.entity.Comment;
+import com.example.blog.service.CommentService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/posts/{postId}/comments")
+@CrossOrigin(origins = "*")
+public class CommentController {
+
+ private final CommentService commentService;
+
+ public CommentController(CommentService commentService) {
+ this.commentService = commentService;
+ }
+
+ @GetMapping
+ public Result> getComments(@PathVariable Long postId) {
+ return Result.success(commentService.getCommentsByPostId(postId));
+ }
+
+ @PostMapping
+ public Result addComment(@PathVariable Long postId, @RequestBody Comment comment) {
+ return Result.success(commentService.addComment(postId, comment));
+ }
+}
diff --git a/backend/src/main/java/com/example/blog/controller/PostController.java b/backend/src/main/java/com/example/blog/controller/PostController.java
new file mode 100644
index 0000000..f52876a
--- /dev/null
+++ b/backend/src/main/java/com/example/blog/controller/PostController.java
@@ -0,0 +1,53 @@
+package com.example.blog.controller;
+
+import com.example.blog.common.Result;
+import com.example.blog.entity.Post;
+import com.example.blog.service.PostService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/posts")
+@CrossOrigin(origins = "*")
+public class PostController {
+
+ private final PostService postService;
+
+ public PostController(PostService postService) {
+ this.postService = postService;
+ }
+
+ @GetMapping
+ public Result> listPosts(
+ @RequestParam(required = false) String category,
+ @RequestParam(required = false) String search) {
+ return Result.success(postService.listPosts(category, search));
+ }
+
+ @GetMapping("/{id}")
+ public Result getPost(@PathVariable Long id) {
+ return Result.success(postService.getPostById(id));
+ }
+
+ @PostMapping
+ public Result createPost(@RequestBody Post post) {
+ return Result.success(postService.createPost(post));
+ }
+
+ @PutMapping("/{id}")
+ public Result updatePost(@PathVariable Long id, @RequestBody Post post) {
+ return Result.success(postService.updatePost(id, post));
+ }
+
+ @DeleteMapping("/{id}")
+ public Result deletePost(@PathVariable Long id) {
+ postService.deletePost(id);
+ return Result.success();
+ }
+
+ @PostMapping("/{id}/like")
+ public Result likePost(@PathVariable Long id) {
+ return Result.success(postService.likePost(id));
+ }
+}
diff --git a/backend/src/main/java/com/example/blog/controller/StatsController.java b/backend/src/main/java/com/example/blog/controller/StatsController.java
new file mode 100644
index 0000000..ece0fcd
--- /dev/null
+++ b/backend/src/main/java/com/example/blog/controller/StatsController.java
@@ -0,0 +1,24 @@
+package com.example.blog.controller;
+
+import com.example.blog.common.Result;
+import com.example.blog.service.PostService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/stats")
+@CrossOrigin(origins = "*")
+public class StatsController {
+
+ private final PostService postService;
+
+ public StatsController(PostService postService) {
+ this.postService = postService;
+ }
+
+ @GetMapping
+ public Result