从静态页面到全栈博客:Furina 后端开发与宝塔部署踩坑记录

最初的 Furina 是一个以展示为主的个人网站:页面已经做好,文章和作品内容放在本地文件中,更新内容需要修改源码、重新构建,再上传到服务器。

这次改造希望实现一个更方便的流程:

在后台编辑文章、上传图片,点击发布后,访客刷新页面就能看到新内容。

围绕这个目标,项目接入了 Java 后端和 MySQL,增加独立管理后台,并部署到 Linux 服务器上的宝塔面板。

整个过程中,CRUD 只是其中一部分。真正花时间的地方,是前后端的数据同步、登录安全校验、数据库迁移,以及从本地运行到公网访问之间的一系列配置问题。

一、先弄清原项目,再设计后端

开始之前,先梳理了原项目的页面和数据流。

实际项目使用 Nuxt 4、Vue 3 和 TypeScript,博客与作品数据来自本地文件。文章搜索、分类统计和详情查找都在前端完成,网站通过静态生成部署。

这意味着,接入后端不能只替换文章列表的数据来源。文章详情、分类数量、作品统计、加载状态和错误提示,都需要一起调整。

尤其是文章详情页:后台新增的文章,不存在于旧的静态构建结果中。如果继续沿用原来的部署方式,可能出现列表里能看到文章,直接访问详情地址或刷新却返回 404 的问题。

最终采用的访问链路是:

浏览器
  ↓
Nginx:域名入口、HTTPS、反向代理
  ↓
Nuxt Node.js 服务:页面与同源 API 代理
  ↓
Spring Boot:业务接口、鉴权、内容管理
  ↓
MySQL:文章、作品和管理员数据

前端改为通过 Node.js 服务运行,Java 负责业务接口,内容保存在数据库中。

这次最先明确的一点是:静态网站改造成动态网站,需要一起调整数据来源、页面行为和部署方式。

二、公开展示和后台管理,需要分别考虑权限

这是个人博客,访客应该可以直接阅读,不需要注册或登录。只有管理文章和作品时,才需要管理员身份。

因此,系统分成两部分:

  • 公开页面:展示已发布文章和公开作品。
  • 管理后台:管理员登录后进行新增、编辑、删除、预览和发布。

这里一个容易遗漏的问题是草稿权限。

如果只在前端列表中隐藏草稿,但公开详情接口仍然能根据 ID 返回正文,那么别人知道地址后,依然可能访问到未发布内容。

所以,文章是否公开需要由后端判断。公开列表和详情接口都要限制发布状态,管理员预览则使用受保护的接口。

除此之外,还考虑了多窗口编辑的问题。即使只有一个管理员,也可能同时打开两个编辑页面。保存和删除时携带版本号,发生冲突就返回提示,避免旧页面覆盖较新的修改。

三、登录成功,不代表后续操作一定成功

本地联调时,遇到了一个比较典型的问题:

后台能够登录,但文章预览和保存失败。

排查发现,安全校验令牌在后续请求过程中被清除,导致写入请求无法通过 CSRF 校验。

这个问题说明,只测试登录接口还不够。登录成功后,浏览器是否保留认证信息、后续请求是否携带有效的校验令牌、退出后会话是否失效,都属于同一条完整流程。

修正令牌和写入请求的处理后,又验证了连续操作:

登录 → 打开文章 → 编辑 → 预览 → 保存 → 发布 → 退出

相比单独调用几个接口,这种连续操作更容易发现状态管理问题。

四、图片上传涉及的不只是“保存一个文件”

文字管理完成后,又增加了封面和正文图片上传。

最终操作方式是:

  • 上传封面后,自动回填图片地址和尺寸。
  • 上传正文图片后,插入对应的 Markdown 内容。
  • 点击“保存并发布”后,文章公开展示。

这里需要处理几个实际问题。

首先,上传接口需要管理员权限和请求校验,不能成为任何人都能写入服务器的入口。

其次,文件扩展名不能作为唯一判断依据。把其他文件改名为 .jpg,并不会让它变成真正的图片。因此需要校验实际文件内容,并验证伪装文件会被拒绝。

这次限定支持 JPG、PNG,单张不超过 5 MB。

另外,图片要与程序部署文件分开保存。否则更新部署包时,很容易覆盖或遗漏已经上传的图片。

最终图片使用独立目录存储,程序更新时保留该目录,备份时同时考虑数据库和图片文件。

还需要注意:草稿文章不可见,不代表它引用的图片地址也有访问控制。 当前图片地址可以公开访问,这与文章的发布权限是两件事。

五、运行方式改变后,原有音乐功能也受到影响

项目原本带有音乐播放器。改为 Node.js 服务运行后,回归测试发现静态音频没有正常返回分段响应,可能影响拖动播放进度。

音乐能够开始播放,并不能证明播放器的所有行为都正常。

浏览器在跳转播放位置时,可能通过 Range 请求获取文件的一部分。因此,调整资源服务后,还需要检查分段响应、进度拖动和跨页面连续播放。

这个问题也提醒我:后端改造影响的不只是新增功能。原有音乐、图片和页面路由,同样需要重新验证。

六、目录规范化,也会遇到 Windows 路径问题

开发过程中,项目统一放在一个根目录下:

Furina/
├─ frontend/
├─ backend/
├─ docs/
├─ scripts/
├─ deployment/
└─ artifacts/

前端原先叫 nuxt-app,后来改为 frontend。

看起来只是改名,实际遇到了目录被进程占用,以及依赖缓存中存在旧绝对路径链接的问题。

处理时,先停止相关开发进程、关闭占用目录的窗口,再完成目录调整,并重新安装依赖。脚本、CI、部署配置和文档中的路径也同步更新。

修改后重新运行类型检查、生产构建和浏览器回归,确认改名没有影响功能。

另外还发现,项目根目录和前端内部各有一个 Git 仓库。目录统一之后,版本管理边界仍然需要单独处理,不能认为文件放在一起,就已经成为一个完整仓库。

七、部署第一关:配置文件存在,程序却找不到

宝塔部署时,Java 服务首先遇到外置配置加载失败。

启动参数指定的是:

application-production.properties

实际文件名却仍然是:

application-production.properties.example

文件在目录中,但名称与启动参数不一致,程序就无法加载。

去掉 .example 后,程序进入下一阶段,又出现数据库连接错误:

using password: NO

这条信息说明,本次连接没有传入密码。它与“传了密码,但密码错误”是不同的问题。

随后补齐数据库密码,继续排查日志目录和运行用户的写入权限。

这一阶段最有效的做法,是按启动顺序逐层判断:

配置文件是否加载
  ↓
数据库能否连接
  ↓
迁移和初始化是否完成
  ↓
应用是否启动成功

每次重启后,还要确认查看的是最新日志。旧错误记录不会因为文件改名或配置保存而自动消失,很容易让人误以为修改没有生效。

八、数据库“导入成功”,还需要验收内容

数据库迁移是这次部署中比较关键的一处问题。

目标是保留本机已有的文章、作品和后台账号,但后端启动时进入了首次创建管理员的流程。

查询后发现,目标库中的管理员数量为 0,文章数量也与本次迁移文件的预期不一致。

这说明,数据库能连接、表已经存在,并不能证明原数据已经完整迁入。

为了保留现场,先停止后端,避免初始化逻辑继续写入;随后新建数据库,重新导入完整迁移文件,再核对关键表。

本次迁移文件核对通过的结果是:

内容 实际条数
管理员 1
文章 2
作品 7
成功的迁移记录 2

确认后,再切换后端连接配置并启动服务。

这次的经验是:迁移验收应该检查具体数据,而不只是看面板有没有显示“成功”。

另外,管理员初始密码配置只用于首次创建账号。数据库中已有账号后,修改初始化配置,不会自动修改现有账号的密码。

九、宝塔中的站点、Java 和 Node 项目各有职责

部署时还遇到一个操作层面的难点:宝塔不同项目类型的入口并不完全一样。

现有 HTML 项目页面里,没有预期中的独立反向代理按钮,因此最后直接修改了站点的 Nginx 配置。

最终分工是:

服务 职责
HTML 站点中的 Nginx 配置 接收域名请求、处理 HTTPS、转发请求
Node.js 项目 运行 Nuxt 前端
Java 项目 运行 Spring Boot 后端
MySQL 保存业务数据

Nginx 将请求交给本机 3000 端口,Nuxt 再通过同源接口代理访问 Java 服务。

图片上传还需要检查代理层的请求大小限制。应用允许上传 5 MB 图片,代理层需要给 multipart 请求中的附加数据留出空间,因此本次配置了 6m。

应用限制和代理限制需要配合,否则可能出现后端支持上传,文件却在到达后端前就被拒绝的情况。

十、后台返回 403,问题发生在密码校验之前

部署后,后台登录返回:

403 请求验证失败

最初看起来像账号密码问题,但这次请求还没有进入密码校验阶段,就被安全检查拦截了。

当时使用 HTTP 访问,生产配置却开启了:

app.secure-cookie=true

浏览器无法在这个 HTTP 站点上正常使用所需的 Secure Cookie,登录流程因此受阻。

处理方向是为正式域名配置 HTTPS,然后使用 HTTPS 地址继续验证。

这个问题让我记住了:登录失败时,要先确认失败发生在哪一层,再决定是否检查密码。

请求验证失败、账号密码错误、数据库连接失败,虽然最终都可能表现为“登不上”,排查方向却完全不同。

十一、服务器内访问正常,公网仍然打不开

配置证书后,又遇到了网站从公网打不开的问题。

这时采用分层检查,先确认应用本身:

curl -I --max-time 10 http://127.0.0.1:3000/blog

再从服务器内部检查域名对应的 HTTPS 站点:

curl -I --max-time 10 \
  --resolve duck-live.top:443:127.0.0.1 \
  https://duck-live.top/blog

两条请求都返回 200,说明当时 Nuxt 服务,以及服务器内部的 HTTPS 转发链路都能正常响应。

随后检查云服务器安全组,发现公网 HTTPS 所需的 443 端口没有放行。调整规则后,网站恢复访问。

这里还有一个容易混淆的问题:duck-live.top 与 www.duck-live.top 是两个不同的域名,需要分别考虑解析、站点绑定和证书覆盖范围。

排查时应该先固定一个地址,避免一会儿访问主域名,一会儿访问 www,把不同问题混在一起。

十二、这次改造完成了什么,还需要继续验证什么

这次完成了公开博客与管理后台的接口接入,支持文章和作品管理、草稿与发布、Markdown 展示,以及封面和正文图片上传。

加入图片上传后的本地验证记录包括:

  • 12 项后端测试。
  • 前端类型检查与生产构建。
  • 34 项浏览器检查。
  • 5 个后台页面的无障碍检查。
  • 真实 MySQL 下的登录和内容管理联调。

部署方面,Java 健康检查、Nuxt 页面和接口转发均已验证,公网 HTTPS 访问也在放行 443 端口后恢复。

不过,公网网站能打开,还不能代表所有线上流程都已经验收完成。 截至这次记录结束,仍需要继续验证生产环境中的后台登录、文章发布和图片上传。

后续还准备完善账号修改与应急重置、数据库和图片备份恢复,以及服务监控。同时,检查中发现的公开接口错误码问题、反向代理下的登录限流识别,也需要继续处理。

这次从静态展示页走到全栈部署,让我对“项目能运行”有了更具体的理解:页面、接口、数据库、安全配置、文件存储和网络入口,需要在同一条链路上正常配合。真正有效的排查方式,是用日志和请求结果逐层缩小范围,每解决一层,就继续验证下一层。