ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flask + Vue3 全栈开发:读书分享评论平台实战解析

Flask + Vue3 全栈开发:读书分享评论平台实战解析 最近整理完一个读书分享评论平台后端用的 Python Flask前端用的 Vue 3。这个项目从我自己的角度来说算是把“内容分享 互动评论”这类社交产品的核心链路完整走了一遍用户注册登录、发布书评、点赞、回复评论、按书籍聚合内容。整个过程踩了不少坑也有很多值得记录的经验这里把整个项目的设计思路、核心代码、联调部署和问题排查完整分享一下。适合看这篇文章的人有两类一类是正在准备毕业设计或者找工作的作品集需要一套拿得出手的全栈项目另一类是刚学完 Flask 或者 Vue 基础想做点真实业务练手又不想把时间浪费在不停试错上的朋友。这套技术栈的特点是清晰、可控、社区资料多用来做书评这种 CURD 密集型业务非常合适。1. 项目整体设计与技术选型1.1 这个项目的核心需求是什么做项目之前得先想清楚所谓“读书分享评论”到底要解决什么真实问题。从用户角度来看核心场景是我读完一本书想记录一下自己的感受也想看看别人对同一本书的评价。从平台角度来看需要承载的核心功能有四个书籍信息管理、书评发布与展示、评论互动点赞和回复、用户账号体系。这些需求拆开之后并不复杂但难点在于它们之间的关联关系。一本书对应多条书评一条书评下面有多条评论一条评论还可能回复另一条评论。这种树状和一对多的关系正是后端数据库设计、前端组件拆分的核心主线。我的做法是先把这个关系图画清楚User用户一对多 Review书评Book书籍一对多 ReviewReview 一对多 Comment评论关键字段是 parent_id 用于评论的嵌套回复。整张业务表其实就四张但把关系理清楚之后前后端的接口设计、数据处理逻辑都有了依据。1.2 技术选型的对比与取舍这里我重点说一下为什么是 Flask 而不是 Django。Django 确实自带 Admin 后台和完整的 ORM做 CURD 类项目开发速度很快但对这个项目场景来说Flask 有更舒服的地方第一Flask 轻量路由和请求处理逻辑非常直观学习成本低第二前后端分离架构下后端只需要提供 JSON APIDjango 自带的模板、表单等能力基本用不上反而增加理解负担第三Flask 的扩展机制很成熟SQLAlchemy 管数据库、JWT 做认证、Flask-CORS 解决跨域按需加载项目结构干净。前端选择 Vue 3 Vite理由也很直接。Vue 的响应式数据和单文件组件SFC形式非常适合这种内容展示型项目props 往下传书评数据、emit 往上报点赞事件数据流非常清晰。Vite 作为构建工具开发环境热更新速度比 Vue CLI基于 Webpack快非常多实测大项目里改一个组件基本秒级刷新。有朋友会问那为什么不直接用 Node 全栈或者前后端都用 Python这个问题我项目里实际验证过Flask 负责数据接口Vue 负责界面交互职责清晰部署也简单——后端用 gunicorn 起服务前端打包成静态文件交给 Nginx 托管两者各干各的排错定位非常方便。1.3 项目目录结构与规划思路项目采用前后端分离的单仓库结构根目录下分 backend 和 frontend 两个子目录各自独立维护依赖book-review-platform/ ├── backend/ │ ├── app.py # Flask 入口与蓝图注册 │ ├── models.py # SQLAlchemy 数据模型 │ ├── auth.py # JWT 认证相关 │ ├── api/ │ │ ├── books.py # 书籍接口 │ │ ├── reviews.py # 书评接口 │ │ └── comments.py # 评论接口 │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── api/ # axios 请求封装 │ │ ├── components/ # 通用组件 │ │ ├── views/ # 页面级组件 │ │ ├── router/ # 路由配置 │ │ └── store/ # Pinia 状态管理 │ └── package.json └── README.md前端把页面级组件和通用组件分开views 里放 BookList书籍列表页、BookDetail书籍详情页、LoginView登录注册页components 里放 BookCard书籍卡片、ReviewItem书评条目、CommentTree评论树这类可复用单元。这样拆分的好处是任何一个页面需要复用书评展示直接引 ReviewItem 就行不用重复写模板。后端把接口按业务模块拆成蓝图Blueprintbooks、reviews、comments 各自独立所有接口统一挂在 /api 前缀下。后端不关心前端怎么渲染只负责把结构化的 JSON 数据吐出去。这套设计对后期扩展也很友好比如将来想加一个“我的收藏”功能只需要新开一个收藏蓝图不动现有代码。2. 后端设计Flask API 与书评业务核心2.1 数据库模型设计四张表搞定核心业务数据模型是整个项目的根基我花了不少时间在这里。先看 models.py 的核心部分from datetime import datetime from werkzeug.security import generate_password_hash, check_password_hash from flask_sqlalchemy import SQLAlchemy from flask_jwt_extended import create_access_token db SQLAlchemy() class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse, indexTrue) password_hash db.Column(db.String(256), nullableFalse) avatar db.Column(db.String(256), default) created_at db.Column(db.DateTime, defaultdatetime.utcnow) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) def to_dict(self): return { id: self.id, username: self.username, avatar: self.avatar, created_at: self.created_at.strftime(%Y-%m-%d %H:%M:%S) } class Book(db.Model): __tablename__ books id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(200), nullableFalse, indexTrue) author db.Column(db.String(100), nullableFalse) cover db.Column(db.String(256), default) summary db.Column(db.Text, default) created_by db.Column(db.Integer, db.ForeignKey(users.id)) created_at db.Column(db.DateTime, defaultdatetime.utcnow) class Review(db.Model): __tablename__ reviews id db.Column(db.Integer, primary_keyTrue) book_id db.Column(db.Integer, db.ForeignKey(books.id), indexTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id), indexTrue) rating db.Column(db.Integer, default5) # 1-5 分 content db.Column(db.Text, nullableFalse) likes_count db.Column(db.Integer, default0) created_at db.Column(db.DateTime, defaultdatetime.utcnow) book db.relationship(Book, backrefreviews) user db.relationship(User, backrefreviews) class Comment(db.Model): __tablename__ comments id db.Column(db.Integer, primary_keyTrue) review_id db.Column(db.Integer, db.ForeignKey(reviews.id), indexTrue) user_id db.Column(db.Integer, db.ForeignKey(users.id)) parent_id db.Column(db.Integer, db.ForeignKey(comments.id), defaultNone) content db.Column(db.Text, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow)设计这张表的时候有几个细节值得说。第一User 表里存的不是明文密码而是 password_hash用 werkzeug 自带的 hash 算法加密这是安全底线我在项目里坚决不做明文存储。第二Review 里加了 rating 评分字段从 1 到 5这个设计让书评除了文字内容还能聚合出书籍的平均评分首页可以做“高分好书”推荐。第三Comment 的 parent_id 指向自身主键实现多级回复逻辑。如果你只想做一层评论用 review_id 关联就够了但考虑到回复场景这个 parent_id 字段后面会让前端渲染评论树方便很多。2.2 认证方案JWT 无状态登录实战用户要发布书评、点赞、评论这些敏感操作需要验证身份。我选了 JWTJSON Web Token方案它在前后端分离项目里是最常见的做法。登录成功之后后端签发一个包含用户 ID 的 token前端存在 localStorage每次请求带着走后端只需要校验 token 是否有效不需要在服务端保存 session。from flask import Blueprint, request from flask_jwt_extended import jwt_required, get_jwt_identity auth_bp Blueprint(auth, __name__) auth_bp.route(/api/auth/register, methods[POST]) def register(): data request.get_json() if not data or not data.get(username) or not data.get(password): return {msg: 用户名和密码不能为空}, 400 if User.query.filter_by(usernamedata[username]).first(): return {msg: 用户名已存在}, 409 user User(usernamedata[username]) user.set_password(data[password]) db.session.add(user) db.session.commit() return {msg: 注册成功}, 201 auth_bp.route(/api/auth/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username, )).first() if not user or not user.check_password(data.get(password, )): return {msg: 用户名或密码错误}, 401 token create_access_token(identitystr(user.id)) return {token: token, user: user.to_dict()}, 200 auth_bp.route(/api/auth/me, methods[GET]) jwt_required def me(): user User.query.get(int(get_jwt_identity())) if not user: return {msg: 用户不存在}, 404 return user.to_dict(), 200这里有个实际踩过的坑create_access_token 的 identity 参数必须传字符串如果直接传整数后续从 token 里解析出来的也是字符串查询数据库时要先做类型转换。我在项目里用 str(user.id) 做了处理后面 get_jwt_identity() 回来再 int()避免类型不一致引发的查询 bug。客户端在拿到 token 后axios 请求拦截器里统一塞进 Authorization 头。后端通过 jwt_required 装饰器保护需要登录的接口不需要登录的接口不做限制。这种方式的优点是水平扩展容易多台后端服务器共用一套校验逻辑不依赖共享 session 存储对部署非常友好。2.3 书评与评论接口的核心实现书评接口是整个项目的核心。设计书评列表接口时我特意做了分页和嵌套返回一次查询把书评携带的书籍信息和用户信息一起返回前端不用再发额外请求books_bp.route(/api/books/int:book_id/reviews, methods[GET]) def get_book_reviews(book_id): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) pagination Review.query.filter_by(book_idbook_id)\ .order_by(Review.created_at.desc())\ .paginate(pagepage, per_pageper_page, error_outFalse) items [] for r in pagination.items: items.append({ id: r.id, rating: r.rating, content: r.content, likes_count: r.likes_count, created_at: r.created_at.strftime(%Y-%m-%d %H:%M:%S), user: r.user.to_dict(), comments_count: Comment.query.filter_by(review_idr.id).count() }) return { total: pagination.total, page: page, per_page: per_page, items: items }, 200写评论接口我使用了事务保护确保点赞数修改的原子性。发布书评、删除书评这几个操作逻辑类似但有几个容易忽略的点发布评论前先校验当前用户登录身份删除书评时必须校验该书评属于当前用户否则任意用户都能删别人的内容这是严重越权漏洞封面图片判断前端可能传空字符串后端需要校验 URL 合法性避免意外数据入库。评论系统为了支持嵌套回复我设计了 parent_id 字段并特意做了一层递归处理。前端的评论树渲染依赖这个字段如果评论是顶级节点parent_id 为 None如果是回复某条评论parent_id 指向父评论 ID。后端的评论树查询我建议一次性把该书评下所有评论取出来评论量本身不会特别大在 Python 层组树结构避免前端发大量请求。3. 前端实战Vue3 组件化与交互细节3.1 项目初始化与工程结构搭建前端初始化我直接用 Vite 官方脚手架npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 pinia axios npm run dev这里想提醒一个新手常犯的问题不要在已有项目目录里再嵌套一个 vue 项目Vite 会检测到当前目录已有内容初始化会报错或者覆盖文件。我吃过这个亏实际操作时先把空目录建好再进目录执行脚手架命令。Vite 开发环境的端口默认是 5173后端 Flask 默认跑在 5000。这两个端口不同浏览器直接发请求必然跨域所以我在 Vite 配置里加了代理// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } } })这样前端代码里的请求路径直接写 /api/books开发时 Vite 自动转发到后端。因为浏览器看到的请求是同源的所以开发环境下根本不需要后端配置 CORS——当然如果遇到前后端分离部署或者跨域测试后端还是要配 CORS这个我在后面的章节专门讲。3.2 核心组件拆解书籍卡片与书评列表前端界面设计我遵循“一切皆组件”的思路。书籍卡片组件 BookCard 负责展示封面、书名、作者、平均评分点击卡片跳转到书籍详情页。这个组件的 props 设计如下template div classbook-card clickgoDetail img :srcbook.cover || defaultCover :altbook.title / h3{{ book.title }}/h3 p{{ book.author }}/p span classrating{{ avgRating }}分/span /div /template script setup import { computed } from vue import { useRouter } from vue-router const props defineProps({ book: { type: Object, required: true } }) const router useRouter() const avgRating computed(() props.book.avg_rating || 0) function goDetail() { router.push(/books/${props.book.id}) } /script这里有两个细节值得说明。第一封面图片偶尔会加载失败所以我加了 defaultCover 兜底图不然页面上一堆裂图非常难看。第二在 script setup 里通过 computed 计算展示值不在模板里写复杂逻辑取不到评分时显示 0 分保证页面不会因 undefined 报错。书评列表组件 ReviewItem 是复用度最高的组件详情页、个人主页都需要它。单个书评展示用户头像、用户名、评分星标、内容、点赞数和评论数template div classreview-item div classreview-header img :srcreview.user.avatar || defaultAvatar classavatar / span classusername{{ review.user.username }}/span span classdate{{ review.created_at }}/span /div div classrating-stars {{ ★.repeat(review.rating) }}{{ ☆.repeat(5 - review.rating) }} /div p classcontent{{ review.content }}/p div classactions button clickhandleLike :class{ liked: isLiked }点赞 {{ review.likes_count }}/button button clickhandleReply回复/button /div /div /template script setup import { ref } from vue import { likeReview } from ../api/reviews const props defineProps({ review: { type: Object, required: true } }) const emit defineEmits([like, reply]) const isLiked ref(false) async function handleLike() { try { await likeReview(props.review.id) props.review.likes_count 1 isLiked.value true emit(like, props.review.id) } catch (e) { alert(请先登录后再点赞) } } function handleReply() { emit(reply, props.review) } /script组件内部不直接调全局状态通过 emit 向父组件抛事件父组件决定后续逻辑比如弹出评论框、刷新列表。这种受控组件模式让 ReviewItem 变成纯展示 交互要素放到任何页面都能正常工作。点赞按钮的 isLiked 状态我用的是组件内部 ref没有状态管理也能实现简单场景真的不需要把每个按钮状态都塞进 Pinia。3.3 axios 拦截器与收藏、登录状态管理所有请求统一走封装好的 request.js这层封装非常关键至少解决三个问题token 自动携带、响应错误统一提示、401 时自动跳到登录页。// src/api/request.js import axios from axios import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) localStorage.removeItem(user) router.push(/login) } const msg error.response?.data?.msg || 请求失败请稍后重试 alert(msg) return Promise.reject(error) } ) export default request登录状态的全局管理我用 Pinia 存了一个 user 对象和登录状态标识。刷新页面后从 localStorage 恢复 user 信息同时根据是否存在 token 来判断是否已经登录。模板里通过 v-ifisAuthenticated 控制“写书评/点赞”按钮的显隐没有登录的用户看到提示后会被引导到登录页。这里我特意没有把 token 放进 Pinia——本地存储 拦截器的方案更简单页面刷新后 token 不会丢而 Pinia 刷新即清空反而要重新恢复多此一举。3.4 评论树组件与插槽的合理运用评论区是 Vue 组件递归使用的典型场景。我写了一个 CommentTree 组件自身递归渲染子评论用插槽slot把“回复”操作暴露给父组件template div classcomment-item div classcomment-meta span{{ comment.user.username }}/span span{{ comment.created_at }}/span /div p{{ comment.content }}/p slot nameactions :commentcomment button clicktoggleReply回复/button /slot div v-ifcomment.children comment.children.length classcomment-children CommentTree v-forchild in comment.children :keychild.id :commentchild reply(c) $emit(reply, c) / /div /div /template script setup import { ref } from vue defineProps({ comment: Object }) defineEmits([reply]) const showReply ref(false) function toggleReply() { showReply.value !showReply.value } /script这种递归组件写法要注意两点一是必须有一个终止条件否则 v-for 遍历没有 children 的节点时会无限循环渲染最终白屏二是插槽默认内容要能覆盖到我在插槽里放了默认回复按钮这样在不同页面复用时可定制。评论实现层级过深的需要控制我后端组树时限制了最大深度避免恶意构造超长嵌套链打爆前端渲染。4. 前后端联调与部署落地4.1 跨域问题根治方案开发模式下有 Vite 代理感觉不到跨域但一旦前后端分开部署比如 Flask 跑在云服务器 8000 端口Nginx 托管 Vue 静态页面浏览器就会报跨域。后端的跨域解决很简单Flask-CORS 扩展一句话配置from flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})生产环境建议把 origins 换成自己的域名白名单不要用星号全放行。这里要说清楚一个概念跨域是浏览器的同源策略不是后端拒绝请求。如果你用 Postman 测试接口永远没有跨域问题但浏览器就严格要求——理解这个机制排查问题会少走很多弯路。如果项目里将来要接入实时功能比如新评论出现时不刷新页面就能看到可以引入 SSEServer-Sent Events或 WebSocket。Flask 里有现成的扩展但要注意 SSE 连接会同时占用一个 gunicorn worker如果服务器资源紧张建议小流量使用或改走轮询。4.2 Flask 生产部署gunicorn Nginx开发用的 flask run 自带服务器只适合本地调试绝不能直接拿去生产并发一上来就崩。生产环境我用 gunicorn 作为 WSGI 服务器配上 Nginx 做反向代理和静态文件服务。部署命令pip install gunicorn gunicorn -w 4 -b 0.0.0.0:8000 app:app-w 4 表示启动 4 个 worker 进程具体数量一般建议 CPU 核心数乘以 2 加 1。比如 2 核服务器就开 5 个 worker这算是比较稳的经验值开少了并发上不去开多了内存不够会频繁 OOM。Nginx 配置核心思路是/ 路径指向 Vue 打包后的 dist 静态目录/api/ 路径反向代理到 gunicorn 服务。还要注意一个非常关键的细节Vue Router 如果使用了 history 模式URL 里没有 # 号刷新页面时 Nginx 会因为找不到对应的文件路径返回 404。解决办法是在 server 块里加 try_files把所有请求统统回退到 index.htmlserver { listen 80; server_name your_domain.com; root /var/www/book-review/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; 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_pass 后面如果加了路径比如 http://127.0.0.1:8000/Nginx 会把 /api 前缀去掉把请求转发给后端。我最初没搞清楚这个细节导致 /api/books 被转发成 /books后端 404 查了半天。实际上不加斜杠是原样转发加了斜杠会有路径拼写变化按自己的后端路由规则来选择。我的做法是不加斜杠保持 /api 前缀原样转发后端蓝图前缀也是 /api前后保持统一。4.3 Vue 打包与部署细节处理前端部署前执行 npm run build生成 dist 目录。有几个容易忽略的点第一打包前检查环境变量。如果代码里有直接请求后端地址的地方建议通过环境变量区分开发环境和生产环境否则开发时写的 http://127.0.0.1:5000/api 打到线上就废了。第二图片资源的路径统一用相对路径或者绝对 CDN 地址。Vue 默认 assetsDir 会把图片打包到 dist/assets 下如果部署到子路径比如 http://domain/app/要用绝对路径或找到对应的 publicPath 配置。第三打包后一定先在本地用 Nginx 或者其他静态服务器起一个预览环境测试不要只跑 npm run dev 觉得没问题就上传。很多问题只有打包产物才会暴露比如路由模式导致刷新 404、静态资源 404、接口地址写死等。整个部署流程我整理下来大概是这样# 后端 cd backend pip install -r requirements.txt gunicorn -w 4 -b 0.0.0.0:8000 app:app # 前端 cd frontend npm run build # 把 dist 目录传到服务器 /var/www/book-review/ scp -r dist userserver:/var/www/book-review/ # 配置 Nginx 并 reload nginx -t nginx -s reload5. 常见问题排查与避坑手册5.1 高频问题速查问题现象可能原因解决方案后端接口 Postman 正常浏览器访问报 CORS 错误后端未配置 CORSFlask 加 Flask-CORS生产配置域名白名单登录成功跳转后刷新页面又变未登录token 未持久化登录成功后把 token 存 localStorage拦截器自动携带Vue 打包部署刷新子路由 404Nginx 未配置 try_files在 location / 加 try_files $uri $uri/ /index.htmlgunicorn 启动后访问很慢并发很低worker 数太少按 CPU 核数调整 -w 参数2 核机器建议 5评论发布后页面不显示组件数据未刷新发布成功后重新调评论列表接口或在父组件统一维护评论数组书评内容中文乱码数据库连接未指定 utf8mb4SQLAlchemy 连接串加 charsetutf8mb4建库时指定 utf8时间字段显示为 UTC和本地差 8 小时datetime.utcnow 存储的是 UTC展示层做格式化统一转换为本地时区这里面最让我记忆深刻的还是数据库编码问题。第一次部署到 Linux 服务器时所有中文书评内容都变成问号折腾了半天发现是 MySQL 连接字符串没加 charsetutf8mb4。SQLAlchemy 连接串写法app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://user:passlocalhost/book_db?charsetutf8mb4后面建表时把表的默认字符集也指定为 utf8mb4彻底解决了中文乱码问题。5.2 实操心得与避坑经验整个项目做下来我最想分享几条经验。第一条关于密码安全。网上很多教程登录注册直接明文存密码我强烈建议不要学。哪怕只是个人项目也要用 werkzeug 的 generate_password_hash 加密存储这个轮子不用自己造但能避免很多不必要的风险。万一数据库泄露用户密码不至于直接暴露。第二条关于列表页性能。书评列表如果一次性把所有数据查出来数据量大了之后接口越来越慢。我用了 SQLAlchemy 自带的 paginate 分页同时前端接 page 和 total 渲染分页按钮。这个经验的通用性很强任何列表接口都应该做分页不然上线之后早晚要回头补。第三条关于前后端联调。建议接口文档在写代码之前就定好字段名、状态码、错误信息格式保持一致。我在做的过程中吃过字段名不统一的亏前端以为后端返回的是 content后端字段叫 text改起来很痛苦。最简单的做法是后端所有接口统一返回格式——成功时 return {data: ...}失败时 return {msg: ...}前端封装层集中处理项目规范统一不少。第四条关于 Vue 组件通信。不要动不动就上 Pinia像书评列表这类数据父组件统一管理就好。常见的数据流是书籍详情页父在 created 中请求书评 → 把列表传给 ReviewItem 子组件展示 → 子组件点赞后 emit 事件父组件更新对应的书评点赞数。这个数据流足够清晰加状态管理反而是过度设计。第五条评论数这里有个经典问题显示评论数时如果直接用评论表 count 没问题但评论数据量大的时候频繁 count 会影响性能。实际项目中可以用冗余字段在 Review 表里直接加一个 comments_count 字段发布评论时 update 加 1删除时减 1。这个优化在项目初期可能不需要但如果预计评论量很大可以在架构层面预留这个设计。6. 还可以这样扩展让项目变得更有竞争力如果你打算用这个项目作为作品集或者毕业设计这里有一些扩展方向按投入产出比排序第一增加标签系统和内容推荐。书评里提取标签小说、科幻、传记首页做“热门标签”入口再配合评分排序做成一个简单的推荐页。这个功能技术难度不大但视觉效果和产品完成度会明显提升。第二做用户个人主页。展示用户发布过的所有书评、获赞总数、评论数配合简单的统计图表可以用 ECharts 画个柱状图展示最近一个月的书评热度。这个功能同时用到了“按用户聚合数据”和“可视化”面试时拿出来讲比单纯说“我做了个书评网站”有说服力得多。第三引入全文搜索。Flask 端用 SQL LIKE 查询勉强够用但体验一般。可以接 Elasticsearch或者轻量一点用 SQLite FTS5 或者 MySQL 全文索引。搜索关键词“书名/作者/书评内容”都能命中体验提升非常大。第四管理后台。新增一个简单的 Vue 管理界面管理员可以审核书评、删除违规评论、管理书籍信息用 Flask 的 JWT 里加一个角色字段区分普通用户和管理员。这类后台功能是面试官比较认可的实际业务场景。这些扩展方向都不是空谈做的时候在现有架构上加蓝图、加路由、加组件就行不会推翻重来。这也是当初选 Flask Vue 这套组合最大的好处——边界清楚、扩展成本低每一步改动都能落在实处。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进