码上问学问代码之理,学编程之道

阶段 7 · 答辩与复盘

做完了,不代表讲得清;讲得清,才算真的掌握。这一阶段把整个项目收口成两样东西:一份能讲 10 分钟的答辩 PPT 和 一份写给自己的项目总结。答辩不是复述代码,而是回答评委真正关心的问题——你做了什么、为什么这么做、遇到什么问题、怎么解决的。走完这一步,你手里就有了一件「可以拿出去说」的完整作品。

答辩讲什么

不是复述代码,而是讲清取舍
先讲为什么很多同学的答辩是这样的:打开代码文件,从第一行念到最后一行——「这里我定义了一个函数,这里我写了一个循环……」评委听三分钟就走神了。因为「代码长什么样」评委自己能看,他想听的是「你为什么这么写」。
同样一个功能,能写出「我导入了一个库」和能写出「我对比了两种方案,选了更合适的那一种,因为它在这个场景下……」——这是「会敲代码」和「懂工程」的分水岭。答辩考的就是后者。
讲透:答辩的三条铁律
  • 讲取舍,不讲语法。「我用了 Flask」是没信息的;「我没用 Django,因为这是个 API 项目不需要重型后台,Flask 更轻、依赖更少、部署更简单」才是答辩。
  • 讲证据,不讲感觉。「性能还行」不如「我给 article 表的 title/summary 加了 FULLTEXT 索引,搜索速度明显改善」。
  • 讲真话,不讲大话。「测试覆盖了主要功能,移动端筛选栏还有一个布局缺陷待修」比「我的项目没有任何 bug」可信一百倍——说自己零缺陷,评委第一反应就是去找 bug。

答辩 PPT 建议结构

10–12 页讲完,别堆字
先讲为什么PPT 的作用是「给评委一个跟随你的路线图」,不是「把你脑子里的东西全塞进去」。一页讲一件事,一页不超过三行字——字越多,评委看得越少。下面的结构按「背景 → 需求 → 架构 → 难点 → 演示 → 总结」走,正好是一个听众能跟得上的顺序。
动手做先列大纲,再往每页填要点。建议 11 页,控制在 10–12 分钟内讲完。
答辩 PPT 大纲text
P1  封面      项目名 · 姓名 · 学号 · 指导老师 · 日期
P2  选题背景   为什么要做古典文学网(1 张图 + 1 句话)
P3  需求分析   3 类角色 · 功能清单(用户/作品/书评/收藏四大模块)
P4  技术架构   一张架构图:Vue 3 → Nginx → Gunicorn → Flask → MySQL 8
P5  数据库设计 ER 图 + 核心表(article / comment / favorite)
P6  接口设计   18 个接口总览 + 统一响应格式 {code, msg, data}
P7  技术难点 1 前后端分离与跨域(代理 + Nginx 反代)
P8  技术难点 2 JWT 鉴权 + 越权的后端校验
P9  技术难点 3 部署上线(Gunicorn + systemd + Nginx)
P10 项目演示   现场走一遍主流程(另附演示脚本)
P11 总结与展望 收获 · 不足 · 后续计划
效果预览运行结果
运行结果: # 每页的「要点 + 讲法」示例(以 P7 技术难点为例) 标题:前后端分离下的跨域与部署 要点(PPT 上只写这三行): · 问题:5173 前端 → 5000 后端,浏览器同源策略拦截 · 开发:Vite 代理 /api 转发,绕开跨域 · 生产:Nginx 同时托管前端与反代 /api,同源自然成立 讲稿(口头补充,不进 PPT): 「一开始我直接在前端写了 5000 的绝对地址,Console 一直报 CORS。 后来我明白了同源策略是浏览器在拦,让后端加通配符只是掩盖问题, 正确做法是让请求看起来是同源的 —— 开发用代理、生产用 Nginx。」
讲透:PPT 的四条排版纪律
  • 一页一件事。别把「需求 + 架构 + 数据库」挤在一页;宁可 12 页每页清爽,也不要 6 页每页塞满。
  • 能画图就别写字。架构图、ER 图、流程图一律用图;文字只做标注。
  • 代码只放「关键几行」。要展示代码时只放 5–8 行核心片段(比如 login_required 装饰器),别整屏贴代码——没人能在 10 秒内读完 40 行。
  • 统一风格:同一套配色、同一种字体、同一位置放页码。PPT 的完成度本身就是项目完成度的一部分。
避坑PPT 里绝不能出现真实的密钥、密码、服务器 IP、数据库连接串。截图代码时特别要检查 .env 文件——很多同学的截图里就明明白白写着数据库密码。用 ****** 打码,或者干脆只截函数逻辑那几行。另外,答辩是公开场合,别在 PPT 里放学号以外的个人敏感信息。

现场演示脚本

按顺序演,别乱点
答辩 10 分钟怎么分:演示 4 分钟 > 讲设计 4 分钟 > 讲收获 2 分钟开场与选题1 分钟系统演示4 分钟设计与实现4 分钟收获与改进2 分钟演示是重头戏:评委最想看「真的能跑」——提前把演示数据备好,别现场造演示脚本(按顺序演,别乱点)打开首页(轮播/分类/推荐)→ 列表筛选搜索 → 进详情发书评 → 点赞收藏 → 退出登录 → 再登录看收藏中途出错的兜底:准备一组截图/录屏,现场环境崩了立刻切换大忌:从第一页代码讲到最后一页——评委想听的是「为什么这么设计」,不是代码朗读
先讲为什么演示是答辩里唯一「能出错且无法辩解」的环节:网络断了、数据被删了、点错按钮进了 404……台下几百只眼睛盯着。把演示当成一段有台词的戏:先写脚本,练三遍,准备好备用方案。脚本的作用是让你「不用临场思考下一步点哪」,从而腾出脑子讲解说词。
动手做按「浏览 → 详情 → 写书评 → 点赞 → 收藏 → 个人中心」的顺序写脚本。顺序设计的原则是:每一步都为下一步做铺垫,最后在个人中心把所有动作的结果一次性展示出来。
现场演示脚本(约 3 分钟)text
【0. 准备】提前打开浏览器:标签1 = 网站首页;标签2 = journalctl 日志(可选)

【1. 浏览首页】约 30 秒
  「这是首页,轮播图、五个文学分类和六篇推荐作品,数据都来自后端接口。」
  动作:指向轮播图 → 点「宋词」分类
  「点分类可以直接筛选,这是查询参数传给后端、由数据库筛出来的。」

【2. 打开详情】约 30 秒
  动作:点「念奴娇·赤壁怀古」
  「详情页有正文、浏览量、点赞数。注意浏览量每次访问 +1,是后端做的。」
  动作:切到 Network 面板,指一下 /api/article/detail/3 这条请求
  「这样评委能直观看到前端是真的在调后端,不是写死的假数据。」

【3. 写书评】约 40 秒
  「我现在没有登录,先点发表书评看看会发生什么。」
  动作:在书评框输入内容 → 点「发表」
  「它提示我要先登录,并跳到了登录页 —— 这是前端的登录守卫
   加后端的 @login_required 双重保护。我登录一下。」
  动作:登录 tester / 123456 → 回到详情页 → 发表书评
  「书评立刻出现在列表首位,总数也同步变了。」

【4. 点赞 + 收藏】约 40 秒
  动作:点作品的「赞」,数字 +1;再点一次,撤回(说明是 toggle)
  动作:点「收藏」,按钮变「已收藏」
  「点赞和收藏都是 toggle 语义:同一个人再点一次就是取消。
   后端用唯一键挡重复插入,连点两次不会产生两条记录。」

【5. 个人中心】约 40 秒
  动作:点顶栏昵称 → 用户中心
  「刚才的操作结果都在这里:我的收藏里能看到刚收藏的那篇,
   个人中心还有我发表过的书评。」
  动作:刷新页面(F5)
  「刷新也不会掉登录态,因为 token 存在 localStorage,
   进页面时用 /api/user/profile 恢复了一次用户信息。」

【6. 收尾】约 20 秒
  「最后,这个网站已经部署上线,地址是 http://example.com,
   大家现在用手机就能打开。」(指向屏幕上已打开的网址)
效果预览运行结果
运行结果: # 演示前 15 分钟的检查清单(逐条打勾) [ ] 网站能打开(自己先刷一遍,确认服务器没睡) [ ] 演示账号能登录(密码记在便签上,别现场想) [ ] 数据是「演示友好」的:书评不要太少、收藏别为空 —— 如果收藏是空的,先手动收藏两篇 [ ] 浏览器缩放调到 110%–125%(投影后字更大) [ ] 关掉所有无关标签页、关掉微信/QQ 弹窗 [ ] 手机热点备用:笔记本开热点,别指望教室 WiFi [ ] 录屏备份:提前把整个流程录一遍,网络挂了就放录屏
讲透:为什么一定要准备备用方案
  • 网络不可控:教室 WiFi 断一次,你的公网网站就打不开。「我有录屏」和「我的网断了」的区别,就是「演示完成」和「演示失败」。
  • 数据不可控:站点可能正好被别人(或你自己)删了数据,导致演示时收藏为空、书评为零。演示前手动造几条「好看」的数据。
  • 本地兜底:除了公网地址,也把本地环境准备好并开着(npm run dev + flask run),公网挂了立刻切本地。
  • 讲解词比操作重要:操作卡住时,只要你还在讲「这一步在验证什么」,评委就不会觉得是事故。演示的容错率,取决于你的嘴,不是你的手。
避坑三件绝对不能在现场做的事:① 现场改代码 / 现场装依赖 / 现场重启服务——十有八九会失败,而且一旦失败就无法挽回;② 用生产库做破坏性操作(测试「删除书评」时删掉演示用的核心数据);③ 把密码、token 打在投影上——演示账号的密码提前写在便签上,不要点开 .env 或数据库客户端。演示只演「已经排练过的路径」,不演「我试试看」。

常见提问准备

每题给出答题要点
先讲为什么提问环节是真正拉开分差的地方。评委的问题往往只有几个套路,但答案的深度能立刻分出「背了知识点」和「真的想过」。下面这六道题,是这门课里最可能被问到的——不要背答案,要理解后半段那句「为什么」。
动手做每道题先用一句话正面回答,再补一句「为什么这么选」,最后如果有对比方案就点一句「为什么不选它」——三段式答题,稳。
高频提问与答题要点text
Q1 为什么做前后端分离?
   答:前端是 Vue 3 的 SPA、后端只提供 JSON 接口,两者通过 HTTP 通信。
   为什么:① 前后端可并行开发,靠接口文档先约定契约,互不等待;
          ② 前端能独立打包上线,后端能独立扩容升级,改一端不影响另一端;
          ③ 体验更好,页面切换不用整页刷新。
   对比:不分离(Jinja2 直出模板)适合小站点、SEO 要求高的场景,
        但前后端耦合,一个人改模板另一个人改接口容易互相踩。

Q2 为什么用 JWT,不用 Session?
   答:登录成功后后端签发 token,前端存起来,之后每个请求带在
       Authorization 头里,后端验签取出 user_id。
   为什么:① 前后端分离下,跨域携带 Session Cookie 很麻烦
              (要配 withCredentials 和 Cookie 的跨域策略);
          ② JWT 是无状态的,服务端不用存用户会话,
              多台后端服务器扩展时不用做会话共享;
          ③ 手机 App、小程序也能用同一套鉴权。
   代价:JWT 签发后服务端难以主动作废(这就是「退出登录」由前端
        清 token 完成的原因),需要黑名单才能彻底解决。

Q3 为什么给 article 表加索引?
   答:① 外键列 category_id、user_id 加索引,加速关联查询;
      ② 搜索用的 title/summary 加 FULLTEXT 全文索引;
      ③ favorite 表加联合唯一键 (user_id, article_id)。
   为什么:索引把「全表扫描」变成「按结构定位」,数据量越大差距越明显。
        联合唯一键不只是性能问题,它还从数据层杜绝了重复收藏。
   代价:索引占空间、拖慢写入 —— 所以不要给从不用于查询的列乱加索引。

Q4 怎么防 SQL 注入?
   答:用 SQLAlchemy 的查询构造器(filter_by / filter),条件值作为参数
      交给数据库驱动做参数化处理。例如 Article.query.filter_by(title=keyword),
      keyword 里写 ' OR 1=1 -- 也只会被当成一个普通字符串。
   为什么:参数化查询让「数据」永远不会被当成「SQL 语句的一部分」。
   注意:图省事用 text() 拼接字符串(f-string 拼 SQL),
        就等于亲手打开了注入的口子 —— 课程里明确禁止这么写。

Q5 密码是怎么存的?
   答:只存哈希,不存明文。注册时 user.set_password(pwd) 生成哈希入库,
      登录时 user.check_password(pwd) 用同样的算法比对。
   为什么:数据库被拖走时,攻击者拿到的是一堆不可逆的哈希值,
        无法直接还原出密码。哈希算法必须是为密码专门设计的
        (werkzeug 的 generate_password_hash),带随机盐,防彩虹表。
   注意:响应里永远不返回 password 字段(测试用例里有专门一条断言)。

Q6 项目里最难的地方是什么?
   答:(讲一个真实故事,别讲「都挺难的」)
   示例:最难的是部署时的 502 —— 页面能开但接口全挂。
        我的排查顺序是:先 systemctl status 看后端 → 发现是 failed →
        再 journalctl 看堆栈 → 发现 ModuleNotFoundError: No module named 'app'
        → 顺着「gunicorn 在哪找模块」想到 WorkingDirectory 配错了。
        改完 daemon-reload + restart 就恢复了。
   为什么这样答:评委想听的是【排查思路】,不是结论。
        能讲清「我先看什么、为什么看它、看到了什么」,比「我修好了」高分得多。
效果预览运行结果
运行结果: # 答题时的三条小技巧 1)不会的题别硬编:「这块我还没深入,我的理解是……,回去我会查证」, 比胡说一个错误答案安全得多。 2)被追问细节时,用「具体到我们的代码是……」把话题拉回自己的项目, 讲你真正写过的东西。 3)主动带出数据或例子:「我们的 article 表有 6 条种子数据, 测试搜索『明月』时命中 1 条」——具体数字最有说服力。
讲透:所有技术问题都能套这个四层模板
  • 是什么:一句话定义(「JWT 是一段带签名的 token」)。
  • 为什么用:解决了我项目里的什么具体问题(「解决前后端分离下跨域带 Session 的麻烦」)。
  • 怎么用:落到自己的代码(「登录时 create_access_token,@login_required 里 verify_jwt_in_request」)。
  • 代价 / 对比:它有什么局限、另一个方案什么场景更合适(「JWT 难主动作废;Session 能立刻踢人但需要服务端存储」)。
  • 能说出第四层的人,基本就赢了一半——因为这说明你是在做技术决策,而不只是在用工具。
避坑别把「我用过」当成「我懂」。评委只要多问一句「那它为什么不……」就能识破。比如你说「用了 JWT 因为它很快」,评委问「它和 Session 的差别在哪」,你答不上来——反而比一开始就说「这个我了解得还不够深」更糟。准备时把每个技术点都问自己一遍「它有什么缺点 / 什么时候不该用它」,能答上来,才算真的掌握。

项目总结怎么写

技术收获 / 工程收获 / 待改进
先讲为什么项目总结不是写给老师看的作业,是写给三个月后的自己看的笔记。你还记得上个月踩过的那个坑是因为什么吗?多数人记不清。而一份好的总结,能让你在下一个项目里少走一遍弯路。「技术收获」记录你学会了什么工具,「工程收获」记录你学会了什么方法——后者才是能迁移到任何项目里的东西。
动手做按三段写:技术收获 / 工程收获 / 待改进项。每一条都写成「原来我以为……,后来发现……,所以现在我会……」,比单纯罗列知识点有用得多。
项目总结模板.mdmarkdown
# 古典文学网 · 项目总结

## 一、项目概况
用 Vue 3 + Flask 3 + MySQL 8 实现的前后端分离古典文学网站,
含用户、作品、书评、收藏四大模块,共 18 个接口,已部署上线。
开发周期:2026-09-20 ~ 2026-10-16(约 4 周)

## 二、技术收获
1. 数据库:原来我以为建完表就完事,后来发现索引才是查询性能的关键。
   现在我会在写查询前先看执行计划,给外键和搜索列加索引。
2. 后端:原来我以为 @app.route 一行就能写接口,后来发现
   「统一响应格式 + 全局异常处理」能让前端少写一半判断。
3. 前端:原来我以为 axios.get 到处写就行,后来发现
   放在拦截器里统一加 token 和拆包,18 个接口的代码省了三分之一。
4. 部署:原来我以为 python app.py 就是上线,后来发现
   生产环境要用 Gunicorn + systemd + Nginx,这是完全不同的三件事。

## 三、工程收获(可迁移到任何项目)
1. 接口先行:先写接口文档,前后端并行开发,联调时几乎没有理解偏差。
2. 分层思维:request.js(通用)→ api(目录)→ 组件(消费)→ Pinia(共享),
   每一层只干一件事,出问题时能快速定位在哪一层。
3. 证据意识:报错先看 Network 和日志,不靠猜;
   测试要有用例编号、有可复现步骤,而不是「我点过了」。
4. 自下而上排查:502 先查后端活没活,别一上来就改 Nginx 配置。

## 四、待改进项
1. 收藏和书评列表没有做加载态与空状态,弱网下体验不好。
2. 移动端筛选栏在 375px 下会重叠(BUG-03),尚未修复。
3. 评论还没做分页缓存,作品多了之后需要引入 Redis。
4. 没有做后台管理,内容只能通过 SQL 维护。
5. 测试只覆盖了用户/书评/收藏三个模块,作品模块靠手工走查。
效果预览运行结果
运行结果: # 写完后自查:差的总结 vs 好的总结 差的:「本项目使用了 Vue3、Flask、MySQL 等技术,实现了增删改查功能。」 → 全是名词,没有你的思考 好的:「原来我以为 X,后来发现 Y,所以现在我会 Z。」 → 有转变、有过程、能迁移 # 检查每一条收获是不是都符合这个句式 共 4 条技术收获 + 4 条工程收获 —— 4 条工程收获都能迁移到别的项目, 这一步最关键。
讲透:为什么「待改进项」最重要
  • 它证明你真的用了一遍自己的作品——没用过的人,写不出「弱网下体验不好」这种具体问题。
  • 它证明你有自我评估能力——能判断出哪些是真正的短板,说明你知道一个「合格的产品」该长什么样。
  • 它给下一个阶段留了接口——想继续做,直接照着「待改进项」往下排就行。
  • 写不出来?说明你只是「把它做出来了」,没有「把它用过一遍」。去用它一整天,待改进项自然就来了。

复盘方法

回顾目标—对比结果—分析原因—提炼经验
复盘四步法:回顾目标 → 对比结果 → 分析原因 → 提炼经验(循环使用)① 回顾目标当初计划做什么?验收标准是什么?② 对比结果实际完成了多少?差在哪里?③ 分析原因成/败的根本原因?客观条件 vs 主观决策④ 提炼经验下次继续做的 /下次要改的做法复盘写进《项目总结》:技术收获 · 工程收获 · 待改进三条线,各举实例
先讲为什么「复盘」不是「总结」,两者有个关键区别:总结是「发生了什么」,复盘是「为什么会这样,下次怎么改」。真实项目里做复盘有固定套路,它能让一次经历变成可复用的经验——而不是过两天就忘的一段忙碌。
动手做用经典的四步法复盘整个综合实战。关键在第三步——只有「分析原因」才能得出「提炼经验」,跳过它,复盘就退化成写流水账。
复盘四步法(以本项目为例)text
【第一步:回顾目标】当初打算做到什么?
  · 完成一个前后端分离的古典文学网,覆盖用户/作品/书评/收藏
  · 18 个接口全部实现并自测通过
  · 部署到公网,手机能打开
  · 交出一份测试报告

【第二步:对比结果】实际做到了什么?(实事求是,别美化)
  · 功能:四大模块全部完成,18 个接口全部实现 —— 达成
  · 部署:已上线 http://example.com —— 达成
  · 测试:26 条用例通过 26 条,遗留 1 个移动端布局缺陷 —— 基本达成
  · 进度:比计划晚了 3 天 —— 未达成
  · 一个意外:原以为「部署是最简单的收尾」,结果卡了整整两天

【第三步:分析原因】为什么会这样?(每个差异都追到根因)
  · 为什么部署卡了两天?
      → 直接原因:502 报错,我不知道从哪查
      → 根本原因:我对「请求经过 Nginx → Gunicorn → Flask」这条链路
        没有整体认识,出问题时不知道先查哪一层
      → 还有一个原因:我开始直接用 root 跑、没配 systemd,
        环境混乱,报错信息互相干扰
  · 为什么进度晚了 3 天?
      → 接口联调的时间被低估了,CORS、代理、token 透传这些小问题
        每个都要查半小时
  · 为什么本该简单的「上传头像」也要花半天?
      → 忘了改 Nginx 的 client_max_body_size,一直返回 413,
        而 Flask 日志里什么都没有,方向找错了半天

【第四步:提炼经验】下次怎么做?(要写成可执行的规则)
  · 部署前先「画一遍请求链路图」,出错时按链路自下而上查
  · 联调阶段给自己预留 3 天缓冲,不要按「写代码的时间」估算
  · 遇到「前端有响应但后端无日志」→ 先怀疑 Nginx(请求没转发过去)
  · 所有环境配置(.env、systemd、Nginx)在本地先演练一遍再上服务器
  · 结论:部署不是「收尾」,它是一个独立的、需要专门学习的环节
效果预览运行结果
运行结果: # 复盘产出的「经验清单」(这些才是真正带走的东西) [ ] 画一张自己的请求链路图,出问题从最底层往上查 [ ] 估算工期时,把「联调 + 部署」按写代码时间的 50% 单独预留 [ ] 记住口诀:后端无日志 + 前端有响应 = 先查 Nginx [ ] 生产环境的每一项配置,先在本地或测试环境演练过一次 # 对照一下:只写「总结」,你只会得到一句「部署花了两天」; # 做了复盘,你得到的是四条下次能直接用的规则。
讲透:复盘的三条原则
  • 对事不对人。写「我没有提前认识部署的复杂度」,而不是「我太笨了」。复盘的目的是改进流程,不是自我批评。
  • 追根因,不追责任人。连问几次「为什么」,一直问到一个「可以改的具体东西」为止——上面的例子就追到了「缺少链路图」这个可改的点。
  • 产出要可执行。「以后要注意」不是经验,「部署前先画请求链路图」才是。判断标准:这条经验,明天就能照做吗?
避坑最没用的复盘是「这次做得挺好的,下次继续努力」——全是好话等于没复盘。一定要找出「和预期不一样的地方」,那才是信息量所在。另一个极端是变成批斗会,把所有问题都归到「我能力不行」——这也没用,因为「能力」不是一个可以立刻改的东西。好的复盘,最后一定落在「具体动作」上。

后续可拓展方向

后台管理 / Docker / 缓存 / 监控
先讲为什么综合实战把「完整流程」走完了,但它刻意控制了规模——真实项目还有后台管理、容器化部署、缓存、监控这些环节。知道「下一步该往哪走」,比「以为自己做完了」重要得多。下面四个方向,挑一个就能作为课程设计 / 毕业设计的起点。
动手做(方向一:后台管理)现在维护内容只能靠 SQL,加一个后台管理是最自然的下一步。不需要重学技术——Vue 3 + Element Plus 已经够用,只是多一层「管理员角色」的权限校验。
后台管理:权限校验(后端)python
from functools import wraps

from flask import g

from utils.response import fail
from utils.auth import login_required


def admin_required(view_func):
    """在 login_required 之上再叠一层「角色校验」。

    用法:
        @admin_bp.post("/article/add")
        @admin_required
        def article_add(): ...
    """

    @wraps(view_func)
    @login_required                     # 先确认登录,再确认角色
    def wrapper(*args, **kwargs):
        # user 表里需要新增一个 role 字段(1=普通用户,9=管理员)
        if g.current_user.role != 9:
            return fail("无权访问,仅管理员可操作", code=403)
        return view_func(*args, **kwargs)

    return wrapper
运行输出Python 运行结果
运行结果: # 前端只需要多一个「路由守卫判断角色」的分支 if (to.meta.requiresAdmin && user.info?.role !== 9) { ElMessage.error('无权访问') return next('/') } next() # 注意:前端守卫只是体验层 # 真正的权限必须由后端的 @admin_required 兜底 # 否则直接 curl 就能绕过前端,随意调用管理接口
动手做(方向二:Docker 部署)阶段 6 的部署方式是「在服务器上手工装一遍环境」,好处是看得清楚,缺点是换一台服务器要重来一遍。Docker 把「环境」也打包成文件,一条命令就复现出整套运行环境。
Dockerfile(后端)docker
# 后端镜像:把「Python 环境 + 依赖 + 代码」一起打包
FROM python:3.10-slim

# 换国内源加速(可选),并装编译 psycopg2 / cryptography 需要的系统库
RUN apt-get update && apt-get install -y --no-install-recommends \
        default-libmysqlclient-dev build-essential \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 先装依赖:依赖没变时这层会被缓存,构建更快
COPY hiflask/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt gunicorn

# 再拷代码
COPY hiflask/ .

EXPOSE 5000

# 容器里可以监听 0.0.0.0(外面还有 compose 网络隔离),
# 但优先级最高的做法仍是只暴露给同一 compose 网络里的 Nginx 容器
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]
效果预览运行结果
运行结果: # 构建镜像并查看 $ docker build -t hiwen-backend . [+] Building 42.3s (10/10) FINISHED => [1/5] FROM docker.io/library/python:3.10-slim => [3/5] RUN pip install --no-cache-dir -r requirements.txt gunicorn => => naming to docker.io/library/hiwen-backend:latest $ docker images hiwen-backend REPOSITORY TAG IMAGE ID CREATED SIZE hiwen-backend latest a1b2c3d4e5f6 10 seconds ago 312MB # 与阶段 6 的对比: # 手工部署 —— 照着文档敲 20 条命令,一步错了排查半小时 # Docker —— docker compose up -d,一条命令起全套(含 Nginx、MySQL)
docker-compose.ymlyaml
services:
  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: 换成强密码
      MYSQL_DATABASE: hiwen
    volumes:
      - db_data:/var/lib/mysql
      - ./hiwen.sql:/docker-entrypoint-initdb.d/hiwen.sql:ro
    # 不写 ports —— 数据库只在 compose 内网可达,不对外暴露
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 5

  backend:
    build: .
    environment:
      FLASK_CONFIG: prod
      DB_HOST: db                # 用服务名当主机名,compose 自动解析
      DB_NAME: hiwen
      DB_USER: hiwen
      DB_PASSWORD: 换成应用账号密码
    depends_on:
      db:
        condition: service_healthy

  nginx:
    image: nginx:1.24
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./dist:/usr/share/nginx/html:ro
      - ./default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - backend

volumes:
  db_data:
效果预览运行结果
运行结果: $ docker compose up -d [+] Running 4/4 ✔ Network hiwen_default Created ✔ Container hiwen-db-1 Healthy ✔ Container hiwen-backend-1 Started ✔ Container hiwen-nginx-1 Started $ docker compose ps NAME IMAGE STATUS hiwen-backend-1 hiwen-backend Up 12 seconds hiwen-db-1 mysql:8 Up 20 seconds (healthy) hiwen-nginx-1 nginx:1.24 Up 12 seconds # 与阶段 6 的架构完全一致,只是「Nginx 服务器」换成了「Nginx 容器」 # 学习建议:先用阶段 6 的手工方式走通一遍理解原理,再用 Docker 提高效率
讲透:四个拓展方向各自的「入口」与价值
  • 后台管理:入口是「加一个 @admin_required 装饰器 + 一个管理端路由表」。价值——把「只有研发能改数据」变成「运营能自助改数据」,这是产品化的第一步。
  • Docker 部署:入口是「Dockerfile + docker-compose.yml」。价值——环境可复现、迁移成本极低,也解决了「我这台能跑你那台报错」的经典问题。
  • 书评分页与缓存:入口是「给评论列表加 Redis 缓存 + 缓存失效策略」。价值——从「功能对不对」走向「性能好不好」,这是后端进阶的必经之路。
  • 部署监控:入口是「Prometheus + Grafana 或云厂商的监控面板」。价值——从「等着用户来报错」变成「主动发现异常」,能监控 CPU、内存、接口耗时、错误率。
  • 选一个深挖,比四个都浅尝更有价值——答辩时被问「下一步做什么」,你能讲出细节,就是加分项。
避坑别把「拓展方向」写成「已实现功能」——PPT 里写了 Docker 部署,就一定会被问到「那你讲讲容器里 Nginx 怎么反代后端」。要么真的做了,要么就老老实实标成「后续计划」。另外进 Docker 之前,先把阶段 6 的手工部署走通一遍:不懂 Gunicorn、systemd、Nginx 各自的职责,直接上 Docker 只是把不懂的东西糊在一起,出问题时更难查。
动手练习
1按本节的 11 页结构做一份答辩 PPT,并写成一份「讲稿」:每页分配多少秒、开头第一句说什么、讲到难点时用什么话术收尾。对着镜子或同学完整讲一遍,掐表看在 12 分钟内能不能讲完。
2挑出你项目里最难的 3 个问题,用「现象 → 排查过程 → 根因 → 修复 → 经验」五项写清楚,作为答辩 Q6 的素材,同时写进项目总结的「工程收获」。
3迁移练习:「红影赏析」电影网站也做完了,主题从「古典文学」换成了「电影赏析」,技术栈完全一致。请说明:有哪些答辩内容可以直接沿用(哪些不用改)、哪些必须重新准备,并为它写 2 个「贴合电影主题」的答辩提问与答题要点(提示:电影有评分、时长、导演、上映年份这些文学网站没有的字段)。
参考答案
第 2 题 · 难点素材模板text
【难点 1】连点两次收藏报 500
  现象:快速双击收藏按钮,接口返回 500,页面无提示
  排查:看 Network → 状态 500;看 Flask 日志 → IntegrityError:
        Duplicate entry '3-1' for key 'uk_fav_user_article'
  根因:两个请求几乎同时到达,都判断「没收藏过」走了 INSERT 分支,
        第二个撞上唯一键
  修复:favorite_toggle 里 try/except IntegrityError → rollback,
        并发下第二次当作「已收藏」处理,不报错
  经验:唯一键既是数据保障,也会成为并发下的异常源 ——
        凡是「先查再插」的写法,都要考虑并发

【难点 2】刷新详情页 404
  现象:从首页点进详情正常,但在详情页按 F5 就 404
  排查:curl -I http://example.com/article/1 → 404;
        看 Nginx 配置 → location / 里没写 try_files
  根因:Nginx 把 /article/1 当成磁盘上的文件去找,找不到就 404,
        根本轮不到 Vue Router
  修复:location / { try_files $uri $uri/ /index.html; }
  经验:只要用了 history 模式的路由,就必须配 try_files 兜底

【难点 3】上传头像一直失败
  现象:本地能上传,线上点上传就报错
  排查:Network 显示 413 Request Entity Too Large;
        但 Flask 日志里一条记录都没有
  根因:Nginx 的 client_max_body_size 默认只有 1m,
        请求被 Nginx 直接拒绝,压根没转发给 Flask
  修复:client_max_body_size 4m; 并 reload nginx
  经验:「前端有响应、后端无日志」→ 先怀疑 Nginx,别去改 Python
效果预览运行结果
运行结果: # 这三个难点能覆盖答辩的多个提问角度 难点 1 → 并发、唯一键、异常处理(后端深度) 难点 2 → SPA 部署、Nginx 配置(部署理解) 难点 3 → 分层排查、Nginx 与 Flask 的边界(工程方法) # 准备 3 个,够你答完「最难的地方是什么」以及追加上来的两三个问题
第 3 题 · 红影赏析的答辩迁移对照text
【可以直接沿用(不用改)】
  · 技术架构页:Vue 3 → Nginx → Gunicorn → Flask → MySQL 8,一张图照搬
  · 接口设计页:{code, msg, data} 统一格式、分页约定、RESTful 风格照搬
  · 技术难点:前后端分离与跨域、JWT 鉴权、部署上线 —— 完全一致
  · 测试方法:用例表结构、pytest 写法、兼容性清单 —— 完全一致
  · 复盘四步法与项目总结模板 —— 换几个名词即可

【必须重新准备】
  · 选题背景:为什么做电影赏析(文学 → 电影,动机完全不同)
  · 需求分析:新增「评分」「想看/看过」「按类型/年份筛选」等需求
  · 数据库设计:ER 图重画 —— movie 表多出 director / duration / release_year,
    还有专门的 rating 表(一个用户对一部电影只能有一个评分)
  · 演示脚本:演示内容换成「找电影 → 看详情 → 打分 → 写短评 → 我的想看」
  · 数据:种子数据、演示数据全部要造(不能再讲「静夜思」)

【2 个贴合电影主题的答辩提问】
Q 你的评分功能,一个用户对同一部电影重复打分怎么处理?
  答:rating 表加了联合唯一键 (user_id, movie_id),重复提交就是更新
      已有记录(而不是新增一条)。做法是先查有没有、有就 UPDATE、
      没有就 INSERT,并 try/except 捕获并发下的唯一键冲突。
      为什么要有唯一键:否则同一个用户可以给自己喜欢的电影打 100 次
      10 分,把平均分刷上去 —— 这是业务规则,靠数据层保证最可靠。

Q 平均分是实时算还是存字段?为什么?
  答:存冗余字段。movie 表加一个 rating_avg 和 rating_count,
      每次打分后重算并写回。
      为什么:电影列表要显示每部电影的评分,如果每次列表都去
      rating 表 JOIN + AVG,数据量一大就慢;存成冗余字段后列表查询
      只是一次简单读取。
      代价:要保证一致性 —— 打分、取消打分、删除评分这几个入口
      都要记得更新它,这是典型的「用写入复杂度换查询性能」,
      和 article 表里存 likes 冗余计数是同一个思路。
效果预览运行结果
运行结果: # 迁移判断的通用原则 能沿用的:与「业务主题」无关的 —— 架构、接口规范、部署方案、测试方法 必须重做的:与「业务主题」强相关的 —— 背景、需求、ER 图、演示、数据 # 一句话:技术骨架可以整块搬走,业务内容必须重做 # 能把这句话说清楚,说明你分得清「通用能力」和「项目细节」

第 3 题的关键:把「知识」和「知识在具体项目里的用法」分开看。架构、部署、测试方法这些是可迁移的通用能力;选题、需求、数据这些是项目专属内容。能一眼分清这两类,你在下一个项目里就能复用一半的经验、只花一半的时间——这正是整个综合实战想教给你的东西。

本阶段小结