从静态页面到全栈博客: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 端口后恢复。
不过,公网网站能打开,还不能代表所有线上流程都已经验收完成。 截至这次记录结束,仍需要继续验证生产环境中的后台登录、文章发布和图片上传。
后续还准备完善账号修改与应急重置、数据库和图片备份恢复,以及服务监控。同时,检查中发现的公开接口错误码问题、反向代理下的登录限流识别,也需要继续处理。
这次从静态展示页走到全栈部署,让我对“项目能运行”有了更具体的理解:页面、接口、数据库、安全配置、文件存储和网络入口,需要在同一条链路上正常配合。真正有效的排查方式,是用日志和请求结果逐层缩小范围,每解决一层,就继续验证下一层。
