Turbo Blog 项目学习与上线指南
整理日期:2026-05-06
项目定位:一个轻量个人博客。Django 像发动机,负责数据、接口、登录、评论、上传、RSS 和 sitemap;原生 HTML/CSS/JS 像驾驶舱,负责页面、写作后台和交互。
效果如图所示:

1. 项目总览表
| 前端入口 | 浏览器打开的第一张纸 | index.html | 只挂载 #app,再加载 config.js 和 script.js。 |
| 前端配置 | 决定 API 请求打到哪里 | config.js | 同域部署时留空;前后端分开时填后端公网地址。 |
| 前端逻辑 | 页面渲染、路由、写作、评论、AI 问答 | script.js | 所有前端“大脑”都在这里。 |
| 前端样式 | 颜色、布局、响应式、卡片、表单 | styles.css | 页面长相基本都改这里。 |
| 站点图片 | 头像、上传图、分享图 | assets/ | 固定头像在根图片,后台上传图在 assets/uploads/。 |
| Django 配置 | 环境变量、数据库、上传目录、AI 配置 | turboblog/settings.py | 项目的总电闸。 |
| Django 路由 | URL 到函数的分发 | turboblog/urls.py | 看到接口地址,先来这里找对应函数。 |
| 数据模型 | 数据库表结构 | blog/models.py | 文章、评论、用户、会话、验证码都在这里定义。 |
| 业务接口 | API、RSS、sitemap、静态页面服务 | blog/views.py | 后端主战场,绝大多数行为都在这里。 |
| 跨域处理 | CORS 和 OPTIONS | blog/middleware.py | 允许前端跨域调用后端。 |
| 初始化数据 | 从旧 JSON 导入站点与文章 | blog/management/commands/seed_initial_data.py | 空库第一次启动时喂一点内容。 |
| 默认数据库 | SQLite 数据文件 | backend/data/turbo-blog-django.sqlite | 本地真实数据在这里。 |
| 旧数据种子 | 初始文章、站点信息 | backend/data/db.json | 只影响首次导入或重建库。 |
| 启动脚本 | Windows 本地启动/停止 | start-blog.bat、stop-blog.bat | 双击即可本地跑起来。 |
| 部署配置 | 云平台启动命令 | Procfile | Heroku/Railway 类平台会读它。 |
| 测试脚本 | 后端冒烟检查 | scripts/test_django_backend.py | 改完后跑它,像出门前检查钥匙。 |
当前本地数据库概况:站点名 Turbo Blog,公网地址仍是 http://localhost:5173,共有 4 篇文章,4 篇已发布文章,1 条评论,0 条待审评论。
2. 架构图
#mermaid-svg-P5hGDFqfISYibOJY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-P5hGDFqfISYibOJY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-P5hGDFqfISYibOJY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-P5hGDFqfISYibOJY .error-icon{fill:#552222;}#mermaid-svg-P5hGDFqfISYibOJY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-P5hGDFqfISYibOJY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-P5hGDFqfISYibOJY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-P5hGDFqfISYibOJY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-P5hGDFqfISYibOJY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-P5hGDFqfISYibOJY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-P5hGDFqfISYibOJY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-P5hGDFqfISYibOJY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-P5hGDFqfISYibOJY .marker.cross{stroke:#333333;}#mermaid-svg-P5hGDFqfISYibOJY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-P5hGDFqfISYibOJY p{margin:0;}#mermaid-svg-P5hGDFqfISYibOJY .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-P5hGDFqfISYibOJY .cluster-label text{fill:#333;}#mermaid-svg-P5hGDFqfISYibOJY .cluster-label span{color:#333;}#mermaid-svg-P5hGDFqfISYibOJY .cluster-label span p{background-color:transparent;}#mermaid-svg-P5hGDFqfISYibOJY .label text,#mermaid-svg-P5hGDFqfISYibOJY span{fill:#333;color:#333;}#mermaid-svg-P5hGDFqfISYibOJY .node rect,#mermaid-svg-P5hGDFqfISYibOJY .node circle,#mermaid-svg-P5hGDFqfISYibOJY .node ellipse,#mermaid-svg-P5hGDFqfISYibOJY .node polygon,#mermaid-svg-P5hGDFqfISYibOJY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-P5hGDFqfISYibOJY .rough-node .label text,#mermaid-svg-P5hGDFqfISYibOJY .node .label text,#mermaid-svg-P5hGDFqfISYibOJY .image-shape .label,#mermaid-svg-P5hGDFqfISYibOJY .icon-shape .label{text-anchor:middle;}#mermaid-svg-P5hGDFqfISYibOJY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-P5hGDFqfISYibOJY .rough-node .label,#mermaid-svg-P5hGDFqfISYibOJY .node .label,#mermaid-svg-P5hGDFqfISYibOJY .image-shape .label,#mermaid-svg-P5hGDFqfISYibOJY .icon-shape .label{text-align:center;}#mermaid-svg-P5hGDFqfISYibOJY .node.clickable{cursor:pointer;}#mermaid-svg-P5hGDFqfISYibOJY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-P5hGDFqfISYibOJY .arrowheadPath{fill:#333333;}#mermaid-svg-P5hGDFqfISYibOJY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-P5hGDFqfISYibOJY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-P5hGDFqfISYibOJY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P5hGDFqfISYibOJY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-P5hGDFqfISYibOJY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P5hGDFqfISYibOJY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-P5hGDFqfISYibOJY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-P5hGDFqfISYibOJY .cluster text{fill:#333;}#mermaid-svg-P5hGDFqfISYibOJY .cluster span{color:#333;}#mermaid-svg-P5hGDFqfISYibOJY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-P5hGDFqfISYibOJY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-P5hGDFqfISYibOJY rect.text{fill:none;stroke-width:0;}#mermaid-svg-P5hGDFqfISYibOJY .icon-shape,#mermaid-svg-P5hGDFqfISYibOJY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-P5hGDFqfISYibOJY .icon-shape p,#mermaid-svg-P5hGDFqfISYibOJY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-P5hGDFqfISYibOJY .icon-shape .label rect,#mermaid-svg-P5hGDFqfISYibOJY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-P5hGDFqfISYibOJY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-P5hGDFqfISYibOJY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-P5hGDFqfISYibOJY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
浏览器
index.html
config.js
script.js
/api/… 请求
turboblog/urls.py
blog/views.py
blog/models.py
backend/data/turbo-blog-django.sqlite
assets/uploads/
RSS / sitemap / robots
可选 AI 模型 API
这不是 React/Vue 项目,而是“原生前端 + Django 单体后端”。浏览器加载静态文件,script.js 用 fetch() 调 Django API;Django 再读写 SQLite,必要时保存上传图片。
3. 要改内容时去哪里
| 浏览器标题、SEO 描述、分享图 | index.html | <title>、meta name="description"、og:* |
| API 后端地址 | config.js | window.TURBO_BLOG_CONFIG.apiBase |
| 首页大标题和介绍文案 | script.js | hero() |
| 顶部导航、底部版权 | script.js | layout() |
| AI 小助手区域文案 | script.js | aiAssistantPanel() |
| 首页文章列表、筛选、短札、关于 | script.js | renderHome() |
| 文章详情页 | script.js | renderPost() |
| 写作后台表单 | script.js | renderEditor() |
| 管理员登录弹窗 | script.js | adminLoginModal() |
| 前端按钮点击、提交表单、上传图片 | script.js | bindEvents() |
| 前端 Markdown 渲染规则 | script.js | markdownToHtml()、renderInlineMarkdown() |
| 颜色、字体、间距、移动端布局 | styles.css | :root、对应 class、@media |
| 头像/站点图标 | assets/avatar-blue-anime.png | 替换同名图片最省事 |
| 上传图片存储位置 | assets/uploads/ | 后台上传后自动写入 |
| API 路由增删 | turboblog/urls.py | urlpatterns |
| 文章字段、评论字段、用户字段 | blog/models.py | Post、Comment、BlogUser 等模型 |
| 文章增删改查逻辑 | blog/views.py | posts()、post_detail() |
| 评论提交和审核逻辑 | blog/views.py | post_comments()、admin_comments()、admin_comment_detail() |
| 验证码、限流、敏感词 | blog/views.py、turboblog/settings.py | captcha()、is_rate_limited()、SENSITIVE_WORDS |
| 图片上传大小和格式 | blog/views.py、turboblog/settings.py | upload_image()、IMAGE_UPLOAD_LIMIT |
| AI 检索和 RAG 逻辑 | blog/views.py、turboblog/settings.py | ai_ask()、best_post_passages()、call_rag_model() |
| SQLite 文件位置 | turboblog/settings.py | SQLITE_FILE |
| 生产环境密钥、域名、Debug | turboblog/settings.py | 环境变量读取区 |
| 初始文章内容 | backend/data/db.json | 只在空库导入时生效 |
| 初始化导入规则 | blog/management/commands/seed_initial_data.py | handle() |
| 本地启动逻辑 | start-blog.bat | 端口、迁移、启动命令 |
| 停止本地服务 | stop-blog.bat | 端口 4000,5173,8000 |
| 云平台启动命令 | Procfile | web: 后面的命令 |
manage.py 的作用:它是 Django 的“控制开关”。你运行这些命令时都靠它找到项目配置:
python manage.py runserver python manage.py migrate python manage.py
check python manage.py seed_initial_data
它里面最关键的一行是:
os.environ.setdefault(“DJANGO_SETTINGS_MODULE”, “turboblog.settings”)
意思是:告诉 Django,配置文件在 turboblog/settings.py。
注意:站点标题、描述、公网地址已经存进 SQLite 后,改 backend/data/db.json 不会自动覆盖旧库。已有库要改 public_url,可用:
python manage.py shell -c "from blog.models import SiteSetting; s=SiteSetting.objects.get(pk=1); s.public_url='https://你的域名'; s.save()"
4. 数据模型速记
| SiteSetting | 站点设置 | title、description、public_url |
| BlogUser | 管理员/作者账号 | username、password_hash、role |
| Session | 登录会话 | token_hash、expires_at、revoked_at |
| Post | 博客文章 | title、slug、excerpt、content、tags、status |
| Comment | 评论 | post、author、content、approved、status |
| Captcha | 评论验证码 | answer_hash、expires_at、used、failures |
| RateLimit | 评论频率限制 | key、count、expires_at |
最重要的关系:一篇 Post 可以有多条 Comment;评论默认 pending,管理员通过后才公开展示。
5. API 地图
| /api/health | GET | 健康检查 | 否 |
| /api/storage | GET | 查看存储状态 | 否 |
| /api/site | GET | 获取站点信息 | 否 |
| /api/posts | GET | 文章列表、搜索 | 否;带草稿需登录 |
| /api/posts | POST | 新建文章 | 是 |
| /api/posts/<slug或id> | GET | 文章详情 | 否;草稿需登录 |
| /api/posts/<slug或id> | PUT/DELETE | 修改/删除文章 | 是 |
| /api/posts/<slug或id>/comments | POST | 提交评论 | 否 |
| /api/captcha | GET | 获取评论验证码 | 否 |
| /api/admin/comments | GET | 待审/已审评论列表 | 是 |
| /api/admin/comments/<id> | PUT/DELETE | 审核或删除评论 | 是 |
| /api/uploads | POST | 上传图片 | 是 |
| /api/auth/login | POST | 管理员登录 | 否 |
| /api/auth/me | GET | 检查登录状态 | 否 |
| /api/auth/logout | POST | 退出登录 | 是/有 token 更完整 |
| /api/ai/ask | POST | 博客知识问答 | 否 |
| /sitemap.xml | GET | 搜索引擎站点地图 | 否 |
| /rss.xml | GET | RSS 订阅 | 否 |
| /robots.txt | GET | 爬虫规则 | 否 |
6. 核心流程
6.1 打开首页
浏览器访问 /,Django 的 frontend() 返回 index.html;script.js 先 loadSite(),再 navigate(),最后 loadPosts() 拉文章并 renderHome() 画出首页。
6.2 写文章
点击“写作”后,前端先调用 /api/auth/me 检查管理员。登录通过后进入 renderEditor();保存时提交到 /api/posts 或 /api/posts/<id>,Django 写入 Post 表。
6.3 评论
文章详情页先拉 /api/captcha。访客提交评论时,后端检查验证码、敏感词和频率限制;评论进入 pending,管理员在写作后台通过后才展示。
6.4 图片上传
写作后台选择图片,前端转成 base64 发到 /api/uploads;后端校验格式和大小,然后写入 assets/uploads/,返回 Markdown 图片地址。
6.5 AI 问答
问题进入 /api/ai/ask。后端把问题分词,在已发布文章里找相关片段;没有 AI_API_KEY 时用本地检索回答,有 key 时把片段交给 DeepSeek/OpenAI-compatible 模型生成回答。
7. 本地开发命令
Windows 推荐:
start-blog.bat
手动启动:
python -m pip install -r requirements.txt
python manage.py migrate –noinput
python manage.py seed_initial_data
python manage.py runserver [::]:5173
检查:
python manage.py check
python scripts/test_django_backend.py
本地地址:
http://localhost:5173
http://localhost:5173/api/health
http://localhost:5173/sitemap.xml
本地默认管理员令牌/密码:dev-token。上线必须改。
8. 生产环境变量
| DJANGO_SECRET_KEY | 必填,长随机字符串 | Django 加密密钥 |
| DJANGO_DEBUG | 0 | 关闭调试模式 |
| ALLOWED_HOSTS | 你的域名,www.你的域名 | 允许访问的域名 |
| ADMIN_TOKEN | 强随机令牌 | 后台令牌登录 |
| ADMIN_PASSWORD | 强密码 | 管理员用户名密码登录 |
| SQLITE_FILE | 持久磁盘路径 | SQLite 数据库位置 |
| JSON_BODY_LIMIT | 默认即可 | JSON 请求体大小 |
| IMAGE_UPLOAD_LIMIT | 默认 5MB | 上传图片大小 |
| SENSITIVE_WORDS | 按需加词 | 评论敏感词 |
| AI_PROVIDER | local/deepseek/openai | AI 提供方 |
| AI_API_KEY | 有模型才填 | 模型 API Key |
| AI_MODEL | 如 deepseek-chat | 模型名称 |
9. Turbo Blog Django 公网发布指南
当前项目已经切换为 Django 后端:一个 Python 服务同时提供前端页面、博客 API、图片上传、RSS、sitemap 和 robots.txt。
整体发布流程如下:
部署平台 + 持久磁盘 Volume + SQLite
部署成功如图所示:

9.1Railway部署
Railway 是什么
Railway 是一个“云上自动机房”。你不用买服务器、装 Nginx、配 HTTPS,它会从 GitHub 拉代码,自动构建,运行你的 Django,并给你一个公网网址。
你的链路会变成:
GitHub 仓库
-> Railway 自动拉代码
-> 安装 requirements.txt
-> 执行启动命令
-> Gunicorn 跑 Django
-> Railway 给公网域名和 HTTPS
Volume 是什么
Volume 不是“音量”,是“持久硬盘”。
普通 Railway 容器像临时酒店房间:你可以在里面写文件,但重新部署/重启后,房间可能被打扫干净。
Volume 像你租的储物柜:挂到服务里以后,文件会长期保留。
你的博客长期写文章,最重要的是数据库不能丢,所以要让 SQLite 住进 Volume:
/app/data/turbo-blog-django.sqlite
Railway 官方说明:Volumes 用来给服务保存持久数据,挂载路径会变成服务里可读写的目录;Volume 在服务启动时挂载,不在 build/pre-deploy 阶段挂载。参考:
python manage.py migrate –noinput && python manage.py seed_initial_data && gunicorn turboblog.wsgi:application –bind 0.0.0.0:$PORT
项目也带了 Procfile,支持会读取 Procfile 的平台自动部署。
9.2新建项目
New Project
-> Deploy from GitHub repo
-> 选择你的博客仓库
Railway 会生成一个项目,里面会有一个服务。这个服务就是你的 Django 博客容器
9.3设置启动命令
进入你的服务:
Service
-> Settings
-> Deploy
-> Start Command

python manage.py migrate –noinput && python manage.py seed_initial_data && gunicorn turboblog.wsgi:application –bind 0.0.0.0:$PORT
9.4必填环境变量
操作如下图所示:

之后直接复制下方代码:
DJANGO_DEBUG=0
DJANGO_SECRET_KEY=一串很长的随机字符
ADMIN_TOKEN=你的后台强令牌
ADMIN_PASSWORD=你的后台强密码
ALLOWED_HOSTS=*
SQLITE_FILE=/app/data/turbo-blog-django.sqlite
UPLOAD_DIR=/app/data/uploads
密钥可以本地生成:
python -c "import secrets; print(secrets.token_urlsafe(50))"
9.5Volume 创建
先ctrl+k
然后在这个搜索框里输入 volume
接下来一直点
最后Mount Path: /app/data
结束
9.6访问
1、开全局才能访问的到网站
2、如图所示,这才是你个人的网站

10. 上线前检查清单
| DJANGO_DEBUG=0 | 不暴露调试信息 |
| DJANGO_SECRET_KEY 已更换 | 不用本地默认密钥 |
| ADMIN_TOKEN/ADMIN_PASSWORD 已更换 | 不用 dev-token |
| ALLOWED_HOSTS 是真实域名 | 不长期使用 * |
| SQLITE_FILE 在持久磁盘 | 重启/重建不丢文章 |
| assets/uploads/ 可持久保存 | 上传图片不丢 |
| 已备份 SQLite | 至少定期备份数据库文件 |
| SiteSetting.public_url 是 HTTPS 域名 | sitemap/RSS 链接正确 |
| /sitemap.xml 可访问 | 搜索引擎可发现文章 |
| /robots.txt 可访问 | 爬虫规则正常 |
| 页脚有 ICP/公安备案号 | 国内长期运行需要 |
| 评论审核开启 | 访客评论不直接公开 |
| python scripts/test_django_backend.py 通过 | 核心接口没坏 |
11. 快速学习路线
按这个顺序学,像从地图边缘走到城市中心:
练手建议:
| 把首页标题改成自己的话 | script.js 的 hero() | 熟悉渲染函数 |
| 改主题色 | styles.css 的 :root | 熟悉样式变量 |
| 新增一个文章标签筛选 | script.js 的 categoryOf() 和 renderHome() | 熟悉前端状态和筛选 |
| 给文章加字段 | blog/models.py、迁移、blog/views.py、script.js | 走完整前后端链路 |
| 改 AI 提示词 | blog/views.py 的 rag_messages() | 理解 RAG 边界 |
| 加一个新 API | turboblog/urls.py + blog/views.py | 理解 Django 路由 |


