FireflyBlog + blogspring 前后端联调完整指南
点击按钮,AI 将为你生成这篇文章的摘要
一、项目架构概览
用户浏览器 │ ├── HTTPS ──→ f3f3.top (Vercel) │ ├── Astro SSR 页面 │ ├── /api/* (Astro API 路由,代理到后端) │ └── /admin/* (管理后台) │ └── HTTPS ──→ api.f3f3.top (阿里云 ECS) ├── Nginx (反向代理 + SSL) └── Spring Boot (Java 17) ├── SQLite 数据库 └── 媒体文件存储服务清单
| 服务 | 域名 | 部署位置 | 技术栈 |
|---|---|---|---|
| 前端 | f3f3.top | Vercel | Astro 6.3 + Tailwind CSS |
| 后端 API | api.f3f3.top | 阿里云 ECS | Spring Boot 3.3 + Java 17 |
| Nginx | api.f3f3.top | 阿里云 ECS | 反向代理 + SSL |
| 数据库 | - | 阿里云 ECS | SQLite (文件) |
| 文件存储 | - | 阿里云 ECS | 本地磁盘 |
域名 DNS 配置
| 记录 | 类型 | 值 | 代理 |
|---|---|---|---|
| f3f3.top | A | 172.67.167.189 (Cloudflare) | 橙色云朵 |
| api.f3f3.top | A | 47.104.152.119 (阿里云) | 橙色云朵 |
注意:两个域名都通过 Cloudflare 代理,SSL 模式设为 Flexible(Cloudflare 处理 HTTPS,到源站走 HTTP)。
二、阿里云服务器配置
服务器信息
| 项目 | 值 |
|---|---|
| 公网 IP | 47.104.152.119 |
| 私网 IP | 172.28.19.236 |
| 操作系统 | Ubuntu 22.04.5 LTS |
| 内核 | 5.15.0-177-generic |
| SSH 端口 | 22 |
| 用户名 | root |
SSH 连接配置
首次连接(密钥认证失败时)
服务器默认只允许密钥登录,首次需要通过密码设置公钥:
# 在本地终端执行(会提示输入密码)ssh root@47.104.152.119
# 登录后执行以下命令设置公钥mkdir -p ~/.ssh && chmod 700 ~/.sshecho 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKELbP3RHgJrLDl7sZEtMWwB6fTYKXfgCwBU5zIDmc0/ 2860556024@qq.com' >> ~/.ssh/authorized_keyschmod 600 ~/.ssh/authorized_keys重要:echo 命令必须在一行内完成,Web 终端容易自动换行导致报错。如果遇到 syntax error near unexpected token 'newline',说明命令被截断了,需要重新输入。
后续连接(密钥认证)
ssh root@47.104.152.119阿里云安全组配置
在 ECS 控制台 → 实例详情 → 安全组 → 入方向添加规则:
| 协议 | 端口范围 | 授权对象 | 用途 |
|---|---|---|---|
| TCP | 22/22 | 0.0.0.0/0 | SSH |
| TCP | 80/80 | 0.0.0.0/0 | HTTP |
| TCP | 443/443 | 0.0.0.0/0 | HTTPS |
服务器目录结构
/opt/├── portfolio-backend.jar # Spring Boot JAR 包 (84MB)└── portfolio/ ├── application.yml # 应用配置文件 └── storage/ ├── data/ │ └── portfolio.db # SQLite 数据库 └── media/ # 上传的媒体文件已安装软件
| 软件 | 版本 | 安装方式 |
|---|---|---|
| Java | OpenJDK 17.0.19 | apt install openjdk-17-jre-headless |
| Nginx | 1.18.0 | apt install nginx |
| Certbot | latest | apt install certbot python3-certbot-nginx |
三、后端部署详细步骤
1. 构建 JAR 包
在本地 Windows 环境:
cd E:/blogspring/backendmvn clean package -DskipTests产物路径:E:/blogspring/backend/target/portfolio-backend-0.1.0-SNAPSHOT.jar
2. 上传到服务器
scp E:/blogspring/backend/target/portfolio-backend-0.1.0-SNAPSHOT.jar root@47.104.152.119:/opt/portfolio-backend.jar3. 创建配置文件
在服务器上创建 /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: WARN4. 创建 systemd 服务
创建 /etc/systemd/system/portfolio.service:
[Unit]Description=Portfolio Backend APIAfter=network.target
[Service]Type=simpleUser=rootWorkingDirectory=/opt/portfolioExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /opt/portfolio-backend.jar --spring.config.additional-location=/opt/portfolio/application.ymlRestart=on-failureRestartSec=10
[Install]WantedBy=multi-user.target启动服务:
systemctl daemon-reloadsystemctl enable portfoliosystemctl start portfoliosystemctl status portfolio5. 配置 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; }}启用配置:
ln -sf /etc/nginx/sites-available/api.f3f3.top /etc/nginx/sites-enabled/nginx -tsystemctl reload nginx6. 配置 SSL 证书
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 部署
cd E:/Astro/FireflyBlogvercel --prod --yes关键配置文件
.env(环境变量)
PUBLIC_API_BASE_URL=https://api.f3f3.topsrc/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.page 为 undefined。
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/projects | GET /api/public/projects | 获取所有项目 |
GET /api/public/blog-posts | GET /api/public/blog-posts | 获取所有文章 |
GET /api/public/blog-posts/:slug | GET /api/public/blog-posts/:slug | 按 slug 获取文章 |
GET /api/public/profile | GET /api/public/profile | 获取个人信息 |
GET /api/public/works | GET /api/public/works | 获取所有作品 |
Admin API 对照表
| 前端调用 | 后端端点 | 说明 |
|---|---|---|
GET /api/admin/projects | GET /api/admin/projects | 获取所有项目(含未发布) |
POST /api/admin/projects | POST /api/admin/projects | 创建项目 |
PUT /api/admin/projects/:id | PUT /api/admin/projects/:id | 更新项目 |
DELETE /api/admin/projects/:id | DELETE /api/admin/projects/:id | 删除项目 |
POST /api/admin/media/upload | POST /api/admin/media/upload | 上传媒体文件 |
后端实体类型
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;};六、常用运维命令
服务器管理
# 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
# 重启 Nginxsystemctl restart nginx
# 测试 Nginx 配置nginx -t
# 查看 Nginx 错误日志tail -f /var/log/nginx/error.log本地构建和部署
# 构建后端 JARcd E:/blogspring/backendmvn 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"
# 部署前端到 Vercelcd E:/Astro/FireflyBlogvercel --prod --yes
# 查看 Vercel 部署状态vercel ls
# 查看 Vercel 部署日志vercel logs f3f3.top --limit 20API 测试
# 测试后端 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/projectscurl https://api.f3f3.top/api/public/blog-posts数据库操作
# 连接到 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
媒体上传失败
现象:管理后台上传文件报错
原因:
X-ADMIN-TOKENheader 未携带- 后端
api.f3f3.top未部署 - Token 不匹配
解决:
- 检查
src/scripts/admin-dashboard.ts中上传请求是否有headers: { "X-ADMIN-TOKEN": state.token } - 确认后端服务运行中:
systemctl status portfolio - 确认 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 请求。
解决:
- 申请 ICP 备案(推荐):阿里云控制台 → 备案管理 → 添加域名
f3f3.top,备案通过后 80 端口正常 - 临时方案:改用 Cloudflare Full 模式(走 443 端口,不受备案拦截),但需修复 Nginx SSL 配置
Cloudflare 525 SSL 握手失败
现象:Cloudflare Full (Strict) 模式下 api.f3f3.top 返回 525
原因:Cloudflare 无法与源站建立 SSL 连接。可能原因:
- 阿里云安全组未放行 443 端口
- Nginx SSL 配置与 Cloudflare 不兼容
- 证书链不完整
解决:
- 确认安全组已放行 443 端口
- 将 Cloudflare SSL 模式改为 Full(不是 Full Strict)
- 检查 Nginx SSL 配置:
openssl s_client -connect 47.104.152.119:443 -servername api.f3f3.top
八、前端注意事项
1. Windows 构建 Symlink 问题
本地 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@Overridepublic 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 缓存问题。在浏览器中:
- 打开 DevTools → Application → Service Workers
- 点击 Unregister
- 或勾选
Update on reload
九、服务器安全注意事项
- SSH 密钥认证:已配置公钥认证,确保
~/.ssh/authorized_keys权限为 600 - 防火墙:建议开启
ufw,只放行 22、80、443 端口 - 定期更新:
apt update && apt upgrade -y - 服务监控:
systemctl status portfolio确认服务正常 - 日志检查:
journalctl -u portfolio -f查看运行日志 - 备份数据库:
cp /opt/portfolio/storage/data/portfolio.db /opt/portfolio/storage/data/backup_$(date +%Y%m%d).db
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!