From d4dc35bc877ed0922dba5e4a7a87638a4cdf39a3 Mon Sep 17 00:00:00 2001 From: jxw Date: Thu, 22 Oct 2026 14:51:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=8D=9A=E5=AE=A2UI?= =?UTF-8?q?=E4=B8=BA=E7=8E=B0=E4=BB=A3=E6=9E=81=E5=AE=A2=E9=A3=8E=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0Nginx=E5=8F=8D=E4=BB=A3=E4=B8=8EMarkdown?= =?UTF-8?q?=E9=98=85=E8=AF=BB=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 15 + .../com/example/blog/BlogApplication.java | 224 +++++- deploy.sh | 28 + docker-compose.yml | 1 + frontend/index.html | 701 ++++++++++++++++-- frontend/nginx.conf | 19 + 6 files changed, 905 insertions(+), 83 deletions(-) create mode 100644 .gitignore create mode 100644 deploy.sh create mode 100644 frontend/nginx.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7723bec --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Idea / VSCode +.idea/ +*.iml +.vscode/ + +# Build artifacts +target/ +*.jar +*.war + +# Logs & temp +*.log +.DS_Store +dist/ +node_modules/ diff --git a/backend/src/main/java/com/example/blog/BlogApplication.java b/backend/src/main/java/com/example/blog/BlogApplication.java index fb5da18..8da4687 100644 --- a/backend/src/main/java/com/example/blog/BlogApplication.java +++ b/backend/src/main/java/com/example/blog/BlogApplication.java @@ -3,38 +3,230 @@ 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/posts") +@RequestMapping("/api") public class BlogApplication { - private static final List> posts = new ArrayList<>(); + 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 { - Map p1 = new HashMap<>(); - p1.put("id", "1"); - p1.put("title", "Hello World! 我的第一篇云端博客"); - p1.put("content", "这是运行在 Google Cloud 台湾节点上的 Spring Boot 3.x 容器!"); - p1.put("author", "旺总"); - p1.put("date", "2026-10-22"); - posts.add(p1); + 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 - public List> getPosts() { - return posts; + @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()); } - @PostMapping - public Map addPost(@RequestBody Map post) { - post.put("id", String.valueOf(posts.size() + 1)); - posts.add(0, post); + @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/deploy.sh b/deploy.sh new file mode 100644 index 0000000..d3ec630 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +echo "==========================================" +echo "🚀 [CI/CD] 开始构建并部署旺总的全栈个人博客..." +echo "==========================================" + +# 1. 拉取最新代码 +if [ -d ".git" ]; then + echo "📥 [Git] 拉取远程最新代码..." + git pull origin main || true +fi + +# 2. 重新编译后端与前端容器 +echo "🐳 [Docker] 重新构建并运行全栈容器..." +docker compose down +docker compose up -d --build + +# 3. 检查服务健康状态 +echo "⏳ [HealthCheck] 等待服务启动..." +sleep 3 +docker ps --filter "name=blog-" + +echo "==========================================" +echo "🎉 部署完成!" +echo "🌐 博客前端: http://<你的服务器IP>:8088" +echo "🔌 后端 API: http://<你的服务器IP>:8089/api/posts" +echo "==========================================" diff --git a/docker-compose.yml b/docker-compose.yml index d40e026..aec750b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,3 +15,4 @@ services: - "8088:80" volumes: - ./frontend:/usr/share/nginx/html + - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro diff --git a/frontend/index.html b/frontend/index.html index 868ed42..4e5cc37 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,80 +1,647 @@ - + - 我的个人技术博客 - - - -
-
-

⚡ 旺总的技术全栈博客

-

基于 Spring Boot 3 + Vue 架构 · Docker 容器驱动 · CI/CD 自动部署

-
- -
- - - - -
- -
加载中...
-
- + + 旺总的技术全栈博客 | Full-Stack & Cloud-Native + + + + + + + + + + + + - async function publish() { - const title = document.getElementById('title').value; - const author = document.getElementById('author').value; - const content = document.getElementById('content').value; - if (!title || !content) return alert('标题和内容不能为空!'); +
- await fetch(API_URL, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title, author, content, date: new Date().toLocaleDateString() }) - }); + +
+
+ + +
+ 旺 +
+
+
+ 旺总的技术全栈博客 + +
+ +
+
- document.getElementById('title').value = ''; - document.getElementById('content').value = ''; - loadPosts(); - } + +
+ + - loadPosts(); + + + + + +
+
+
+ + +
+ + +
+
+ +
+
+
+
+ 👨‍💻 +
+ +
+
+
+

旺总 (WangZong)

+ 全栈工程师 +
+

+ 专注 Spring Boot 微服务架构 · Docker / K8s 云原生容器化 · CI/CD 自动化交付 +

+ +
+ ⚡ Spring Boot 3 + 🐳 Docker Compose + 🌿 Vue 3 / Vite + 🚀 Gitea Actions + 🤖 DeepSeek R1 +
+
+
+ + +
+
+
{{ stats.totalPosts || posts.length }}
+
公开文章
+
+
+
+
{{ stats.totalViews || 2061 }}
+
阅读总次
+
+
+
+
{{ stats.totalLikes || 165 }}
+
点赞认可
+
+
+
+
+ + +
+ +
+ +
+ + +
+ + +
+
+ + +
+
+
+
+ + {{ post.category || '技术' }} + + · + 📅 {{ post.date }} + · + ⏱️ {{ post.readTime || '3 min' }} +
+
+ + +

+ {{ post.title }} +

+ + +

+ {{ post.summary || post.content.substring(0, 140) }}... +

+ + +
+ +
+ + #{{ tag }} + +
+ + +
+ + + {{ post.views || 1 }} + + + + + + 阅读全文 → + +
+
+
+
+ + +
+
🔍
+

没有找到相关文章

+

换个搜索词或点击右上方“发布文章”添加一篇吧!

+
+
+ + +
+
+ +
+
+ + {{ activePost.category }} + + · + 📅 {{ activePost.date }} +
+ +
+ + +
+

+ {{ activePost.title }} +

+
+ 👤 作者: {{ activePost.author || '旺总' }} + · + ⏱️ 预计阅读: {{ activePost.readTime || '3 min' }} + · + 👁️ {{ activePost.views }} 浏览 +
+ + +
+
+ + +
+ + +
+
+
+
+ + +
+
+
+

+ ✍️ 撰写并发布新博客 +

+ +
+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + 支持代码块高亮及标题格式 +
+ +
+ +
+ + +
+
+
+
+ + +
+
+
+ © 2026 旺总的技术全栈博客 · 驱动于 + Google Cloud 台湾节点 +
+
+ Spring Boot 3 + · + Docker Compose + · + Gitea CI/CD +
+
+
+ +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..3529260 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + # 反向代理后端 Spring Boot API 接口,解决无法连接后端与跨域问题 + location /api/ { + proxy_pass http://backend:8080/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +}