FireflyBlog + blogspring 前后端联调完整指南

2469 字
12 分钟
FireflyBlog + blogspring 前后端联调完整指南
AI 概括

点击按钮,AI 将为你生成这篇文章的摘要

一、项目架构概览#

用户浏览器
├── HTTPS ──→ f3f3.top (Vercel)
│ ├── Astro SSR 页面
│ ├── /api/* (Astro API 路由,代理到后端)
│ └── /admin/* (管理后台)
└── HTTPS ──→ api.f3f3.top (阿里云 ECS)
├── Nginx (反向代理 + SSL)
└── Spring Boot (Java 17)
├── SQLite 数据库
└── 媒体文件存储

服务清单#

服务域名部署位置技术栈
前端f3f3.topVercelAstro 6.3 + Tailwind CSS
后端 APIapi.f3f3.top阿里云 ECSSpring Boot 3.3 + Java 17
Nginxapi.f3f3.top阿里云 ECS反向代理 + SSL
数据库-阿里云 ECSSQLite (文件)
文件存储-阿里云 ECS本地磁盘

域名 DNS 配置#

记录类型代理
f3f3.topA172.67.167.189 (Cloudflare)橙色云朵
api.f3f3.topA47.104.152.119 (阿里云)橙色云朵

注意:两个域名都通过 Cloudflare 代理,SSL 模式设为 Flexible(Cloudflare 处理 HTTPS,到源站走 HTTP)。


二、阿里云服务器配置#

服务器信息#

项目
公网 IP47.104.152.119
私网 IP172.28.19.236
操作系统Ubuntu 22.04.5 LTS
内核5.15.0-177-generic
SSH 端口22
用户名root

SSH 连接配置#

首次连接(密钥认证失败时)#

服务器默认只允许密钥登录,首次需要通过密码设置公钥:

Terminal window
# 在本地终端执行(会提示输入密码)
ssh root@47.104.152.119
# 登录后执行以下命令设置公钥
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKELbP3RHgJrLDl7sZEtMWwB6fTYKXfgCwBU5zIDmc0/ 2860556024@qq.com' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

重要echo 命令必须在一行内完成,Web 终端容易自动换行导致报错。如果遇到 syntax error near unexpected token 'newline',说明命令被截断了,需要重新输入。

后续连接(密钥认证)#

Terminal window
ssh root@47.104.152.119

阿里云安全组配置#

在 ECS 控制台 → 实例详情 → 安全组 → 入方向添加规则:

协议端口范围授权对象用途
TCP22/220.0.0.0/0SSH
TCP80/800.0.0.0/0HTTP
TCP443/4430.0.0.0/0HTTPS

服务器目录结构#

/opt/
├── portfolio-backend.jar # Spring Boot JAR 包 (84MB)
└── portfolio/
├── application.yml # 应用配置文件
└── storage/
├── data/
│ └── portfolio.db # SQLite 数据库
└── media/ # 上传的媒体文件

已安装软件#

软件版本安装方式
JavaOpenJDK 17.0.19apt install openjdk-17-jre-headless
Nginx1.18.0apt install nginx
Certbotlatestapt install certbot python3-certbot-nginx

三、后端部署详细步骤#

1. 构建 JAR 包#

在本地 Windows 环境:

Terminal window
cd E:/blogspring/backend
mvn clean package -DskipTests

产物路径:E:/blogspring/backend/target/portfolio-backend-0.1.0-SNAPSHOT.jar

2. 上传到服务器#

Terminal window
scp E:/blogspring/backend/target/portfolio-backend-0.1.0-SNAPSHOT.jar root@47.104.152.119:/opt/portfolio-backend.jar

3. 创建配置文件#

在服务器上创建 /opt/portfolio/application.yml

server:
port: ${PORT:8080}
spring:
datasource:
url: jdbc:sqlite:/opt/portfolio/storage/data/portfolio.db
driver-class-name: org.sqlite.JDBC
jpa:
open-in-view: false
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.community.dialect.SQLiteDialect
format_sql: true
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
content:
storage-root: /opt/portfolio/storage/media
media-base-url: https://api.f3f3.top/api/files
admin-token: my-secret-token-2024
allowed-origins:
- https://f3f3.top
- https://api.f3f3.top
logging:
level:
root: INFO
org.hibernate.SQL: WARN

4. 创建 systemd 服务#

创建 /etc/systemd/system/portfolio.service

[Unit]
Description=Portfolio Backend API
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/portfolio
ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /opt/portfolio-backend.jar --spring.config.additional-location=/opt/portfolio/application.yml
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target

启动服务:

Terminal window
systemctl daemon-reload
systemctl enable portfolio
systemctl start portfolio
systemctl status portfolio

5. 配置 Nginx 反向代理#

创建 /etc/nginx/sites-available/api.f3f3.top

server {
listen 80;
listen 443 ssl;
server_name api.f3f3.top;
ssl_certificate /etc/letsencrypt/live/api.f3f3.top/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.f3f3.top/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:8080;
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;
}
}

启用配置:

Terminal window
ln -sf /etc/nginx/sites-available/api.f3f3.top /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

6. 配置 SSL 证书#

Terminal window
certbot --nginx -d api.f3f3.top --non-interactive --agree-tos --email 2860556024@qq.com

注意:如果使用 Cloudflare Flexible 模式,Nginx 配置中 HTTP 不要重定向到 HTTPS,否则会造成重定向循环。

修复后 Nginx 配置要点:

  • 同时监听 80 和 443 端口
  • 不设置 return 301 https://... 重定向
  • 直接代理到后端

四、前端部署#

Vercel 部署#

Terminal window
cd E:/Astro/FireflyBlog
vercel --prod --yes

关键配置文件#

.env(环境变量)#

PUBLIC_API_BASE_URL=https://api.f3f3.top

src/lib/runtime-config.ts#

const apiBase = (import.meta.env.PUBLIC_API_BASE_URL || '').replace(/\/$/, '');
function getBaseUrl(): string {
if (apiBase) return apiBase;
if (typeof window !== 'undefined') return window.location.origin;
return 'http://localhost:4321';
}
const base = getBaseUrl();
export const runtimeConfig = {
apiBaseUrl: base,
publicApiBaseUrl: `${base}/api`,
adminApiBaseUrl: `${base}/api`,
mediaBaseUrl: `${base}/api/media`,
};

SSR 模式下的 Prerender 问题#

Astro output: "server" 模式下,使用 getStaticPaths() 的页面必须添加 export const prerender = true;,否则会报 TypeError: Cannot read properties of undefined

需要添加的文件:

// src/pages/[...page].astro(首页/分页)
export const prerender = true;
// src/pages/posts/[...slug].astro(文章详情)
export const prerender = true;
// src/pages/gallery/[album].astro(相册)
export const prerender = true;

原因:SSR 模式下 getStaticPaths() 被忽略,Astro.props.pageundefined

Media 上传代理路由#

文件:src/pages/api/media/upload.ts

import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => {
const backendBase = (import.meta.env.PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
if (!backendBase) {
return new Response("PUBLIC_API_BASE_URL is not configured", { status: 500 });
}
const token = request.headers.get("x-admin-token");
if (!token) {
return new Response("Missing X-ADMIN-TOKEN header", { status: 401 });
}
const formData = await request.formData();
const upstream = await fetch(`${backendBase}/api/admin/media/upload`, {
method: "POST",
headers: { "X-ADMIN-TOKEN": token },
body: formData,
});
const body = await upstream.text();
return new Response(body, {
status: upstream.status,
headers: { "Content-Type": "application/json" },
});
};

请求链路

Admin 页面 → /api/media/upload (Astro 代理) → api.f3f3.top/api/admin/media/upload (Spring Boot)

Admin Dashboard Token 修复#

src/scripts/admin-dashboard.ts 中的上传请求必须携带 Token:

// 修复前(缺少 X-ADMIN-TOKEN header)
const response = await fetch(`${runtimeConfig.adminApiBaseUrl}/media/upload`, {
method: "POST",
body: formData
});
// 修复后
const response = await fetch(`${runtimeConfig.adminApiBaseUrl}/media/upload`, {
method: "POST",
headers: { "X-ADMIN-TOKEN": state.token },
body: formData
});

五、前后端对接流程#

API 对照表#

前端 apiClient.ts 调用路径 → 后端 Spring Boot 端点:

前端调用后端端点说明
GET /api/public/projectsGET /api/public/projects获取所有项目
GET /api/public/blog-postsGET /api/public/blog-posts获取所有文章
GET /api/public/blog-posts/:slugGET /api/public/blog-posts/:slug按 slug 获取文章
GET /api/public/profileGET /api/public/profile获取个人信息
GET /api/public/worksGET /api/public/works获取所有作品

Admin API 对照表#

前端调用后端端点说明
GET /api/admin/projectsGET /api/admin/projects获取所有项目(含未发布)
POST /api/admin/projectsPOST /api/admin/projects创建项目
PUT /api/admin/projects/:idPUT /api/admin/projects/:id更新项目
DELETE /api/admin/projects/:idDELETE /api/admin/projects/:id删除项目
POST /api/admin/media/uploadPOST /api/admin/media/upload上传媒体文件

后端实体类型#

src/lib/blogspring-types.ts
export type Project = {
id: number;
title: string;
description?: string;
link: string;
coverImageUrl?: string;
videoUrl?: string;
tags?: string[];
featured?: boolean;
published?: boolean;
sortOrder?: number;
};
export type BlogPost = {
id: number;
title: string;
slug: string;
summary?: string;
content?: string;
contentHtml?: string;
headings?: { slug: string; text: string; depth: number }[];
coverImageUrl?: string;
publishDate: string;
featured?: boolean;
tags?: string[];
};
export type Profile = {
id: number;
fullName: string;
headline?: string;
bio?: string;
email?: string;
phone?: string;
wechat?: string;
location?: string;
jobTitle?: string;
heroImageUrl?: string;
resumeUrl?: string;
primaryCtaLabel?: string;
primaryCtaLink?: string;
secondaryCtaLabel?: string;
secondaryCtaLink?: string;
socialLinks?: Record<string, string>;
};
export type Work = {
id: number;
name: string;
description?: string;
url: string;
imageUrl?: string;
videoUrl?: string;
tags?: string[];
isShow?: boolean;
sortOrder?: number;
};
export type MediaAsset = {
id: number;
originalFilename: string;
relativePath: string;
publicUrl: string;
mimeType?: string;
sizeInBytes?: number;
category?: string;
};

六、常用运维命令#

服务器管理#

Terminal window
# SSH 连接
ssh root@47.104.152.119
# 查看 Java 服务状态
systemctl status portfolio
# 重启 Java 服务
systemctl restart portfolio
# 查看 Java 日志
journalctl -u portfolio -f
# 查看 Java 日志(最近 50 行)
journalctl -u portfolio -n 50
# 停止服务
systemctl stop portfolio
# 查看 Nginx 状态
systemctl status nginx
# 重启 Nginx
systemctl restart nginx
# 测试 Nginx 配置
nginx -t
# 查看 Nginx 错误日志
tail -f /var/log/nginx/error.log

本地构建和部署#

Terminal window
# 构建后端 JAR
cd E:/blogspring/backend
mvn clean package -DskipTests
# 上传到服务器
scp E:/blogspring/backend/target/portfolio-backend-0.1.0-SNAPSHOT.jar root@47.104.152.119:/opt/portfolio-backend.jar
# 在服务器上重启服务
ssh root@47.104.152.119 "systemctl restart portfolio"
# 部署前端到 Vercel
cd E:/Astro/FireflyBlog
vercel --prod --yes
# 查看 Vercel 部署状态
vercel ls
# 查看 Vercel 部署日志
vercel logs f3f3.top --limit 20

API 测试#

Terminal window
# 测试后端 API(本地)
curl http://localhost:8080/api/public/profile
# 测试后端 API(公网)
curl https://api.f3f3.top/api/public/profile
# 测试前端
curl -I https://f3f3.top/
# 测试完整链路
curl https://api.f3f3.top/api/public/projects
curl https://api.f3f3.top/api/public/blog-posts

数据库操作#

Terminal window
# 连接到 SQLite 数据库
sqlite3 /opt/portfolio/storage/data/portfolio.db
# 查看所有表
.tables
# 查看表结构
.schema
# 查看数据
SELECT * FROM blog_posts;
SELECT * FROM projects;
# 退出
.quit

七、排查问题清单#

首页 500 错误#

现象f3f3.top 返回 500,其他页面正常

原因[...page].astro 缺少 export const prerender = true;

解决:在 src/pages/[...page].astro 顶部添加 export const prerender = true;

api.f3f3.top 返回 522#

现象:Cloudflare 返回 522 Connection Timed Out

原因:阿里云安全组未放行端口 80/443

解决:在 ECS 安全组入方向添加 TCP 80 和 443 规则

api.f3f3.top 返回 404 DEPLOYMENT_NOT_FOUND#

现象:Vercel 返回 404

原因:域名没有指向正确的部署

解决:确保 Cloudflare DNS 的 A 记录指向 47.104.152.119

媒体上传失败#

现象:管理后台上传文件报错

原因

  1. X-ADMIN-TOKEN header 未携带
  2. 后端 api.f3f3.top 未部署
  3. Token 不匹配

解决

  1. 检查 src/scripts/admin-dashboard.ts 中上传请求是否有 headers: { "X-ADMIN-TOKEN": state.token }
  2. 确认后端服务运行中:systemctl status portfolio
  3. 确认 Token 一致:application.yml 中的 admin-token 与管理后台输入的相同

SSL 证书申请失败#

现象:certbot 报 unauthorized 错误

原因:Cloudflare 代理导致 ACME 验证不通

解决:Cloudflare Flexible 模式下不申请证书也能用,Cloudflare 自动处理 HTTPS

Nginx 重定向循环#

现象:页面不断刷新,无法加载

原因:Nginx 配置了 HTTP→HTTPS 重定向,Cloudflare Flexible 模式下形成循环

解决:Nginx 同时监听 80 和 443,不设置 return 301 https://...

阿里云 ICP 备案拦截(403)#

现象:Cloudflare Flexible 模式下 api.f3f3.top 返回 403,页面显示”非备案拦截”

原因:阿里云要求所有域名完成 ICP 备案才能通过 80 端口(HTTP)访问。未备案时阿里云会拦截 HTTP 请求。

解决

  1. 申请 ICP 备案(推荐):阿里云控制台 → 备案管理 → 添加域名 f3f3.top,备案通过后 80 端口正常
  2. 临时方案:改用 Cloudflare Full 模式(走 443 端口,不受备案拦截),但需修复 Nginx SSL 配置

Cloudflare 525 SSL 握手失败#

现象:Cloudflare Full (Strict) 模式下 api.f3f3.top 返回 525

原因:Cloudflare 无法与源站建立 SSL 连接。可能原因:

  1. 阿里云安全组未放行 443 端口
  2. Nginx SSL 配置与 Cloudflare 不兼容
  3. 证书链不完整

解决

  1. 确认安全组已放行 443 端口
  2. 将 Cloudflare SSL 模式改为 Full(不是 Full Strict)
  3. 检查 Nginx SSL 配置:openssl s_client -connect 47.104.152.119:443 -servername api.f3f3.top

八、前端注意事项#

本地 pnpm build 可能报 EPERM: operation not permitted, symlink 错误。这是 Windows 权限问题,不影响 Vercel 部署(Vercel 用 Linux 构建)。

2. Cloudflare Flexible 模式#

Cloudflare SSL 设为 Flexible 时:

  • 用户 → Cloudflare(HTTPS)→ 源站(HTTP)
  • 源站不需要 SSL 证书
  • 但源站的 HTTP 响应会被 Cloudflare 转为 HTTPS 返回给用户

3. CORS 跨域#

如果前端和后端域名不同,后端需要配置 CORS:

// Spring Boot WebConfig.java
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://f3f3.top", "https://api.f3f3.top")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}

4. Service Worker 缓存#

Vercel 部署后如果页面不更新,可能是 Service Worker 缓存问题。在浏览器中:

  1. 打开 DevTools → Application → Service Workers
  2. 点击 Unregister
  3. 或勾选 Update on reload

九、服务器安全注意事项#

  1. SSH 密钥认证:已配置公钥认证,确保 ~/.ssh/authorized_keys 权限为 600
  2. 防火墙:建议开启 ufw,只放行 22、80、443 端口
  3. 定期更新apt update && apt upgrade -y
  4. 服务监控systemctl status portfolio 确认服务正常
  5. 日志检查journalctl -u portfolio -f 查看运行日志
  6. 备份数据库cp /opt/portfolio/storage/data/portfolio.db /opt/portfolio/storage/data/backup_$(date +%Y%m%d).db

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
FireflyBlog + blogspring 前后端联调完整指南
https://f3f3.top/posts/p744fc34f/
作者
lyf
发布于
2026-05-29
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
lyf
Hello, I'm LyF.
公告
欢迎来到一飞的博客!。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
14
分类
5
标签
16
总字数
48,064
运行时长
0
最后活动
0 天前

文章目录

🤖 AI 助手

👋 你好!

我可以帮你解答关于这篇文章的问题