feat: 重构博客UI为现代极客风,增加Nginx反代与Markdown阅读器

This commit is contained in:
2026-10-22 14:51:47 +08:00
parent cb08080ca7
commit d4dc35bc87
6 changed files with 905 additions and 83 deletions
@@ -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<Map<String, String>> 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<String> 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<String> 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<String> getTags() { return tags; }
public void setTags(List<String> 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<Post> POSTS = new CopyOnWriteArrayList<>();
static {
Map<String, String> 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<Map<String, String>> getPosts() {
return posts;
@GetMapping("/posts")
public List<Post> 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<String, String> addPost(@RequestBody Map<String, String> 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<String, Object> likePost(@PathVariable String id) {
Map<String, Object> 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<String, Object> deletePost(@PathVariable String id) {
boolean removed = POSTS.removeIf(p -> p.getId().equals(id));
Map<String, Object> res = new HashMap<>();
res.put("success", removed);
return res;
}
@GetMapping("/stats")
public Map<String, Object> getStats() {
Map<String, Object> 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<String> 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);
}