
从Flask和Django二选一的纠结说起吧。我做网上书店这类项目带过不少人十有八九都会卡在同一道选择题上Python后端到底用Flask还是Django我的回答通常很直接——如果你做的不是几十行代码的小脚本而是一个图书销售商城最好两个都用。Django扛主业务Flask做轻量辅助接口这样既不会被Django的全家桶束缚也不用在Flask里从零拼数据库模型和后台管理。下面这篇文章就是一套完整的基于Python、由Flask与Django联合驱动的网上书店设计与实现思路从项目拆分、表结构设计到下单事务、部署踩坑全流程梳理一遍。想拿去交毕设、练手、或者扩展成简历项目的读者按这个路径走能少走不少弯路。1. 为什么我推荐用FlaskDjango组合做网上书店1.1 网上书店到底在练什么能力选项目先要看它能不能覆盖足够多的典型场景。图书销售商城这套业务天然包含用户注册登录、图书分页展示、分类筛选、关键词搜索、购物车增删改、订单生成、库存扣减、支付回调、订单状态流转、后台图书管理。这些模块几乎把Web系统最常遇到的CRUD、认证、事务、状态机全占齐了。只做博客系统当然也行但博客的业务太单薄很多关键点根本碰不到比如并发扣库存时怎么保证数据不错、订单生成时价格怎么不被后续改价影响、购物车数据怎么跨设备保留。这些才是面试时真正能聊出东西的细节。图书商城项目把这些点一个不少地暴露出来做完之后你对一个系统是怎么被设计出来的会有一个完整的骨架感而不是只学会几个框架API。1.2 两个框架的合理分工谁当主力谁当辅助网上不少文章把Flask和Django做成对立面实际上它们完全可以协同工作。我的拆分逻辑是这样的模块归属框架主要职责服务端口用户认证与资料Django注册、登录、地址管理8000图书展示与管理Django列表、详情、后台维护8000购物车Django加购、改数量、删除8000订单与支付状态Django下单事务、状态流转8000热销推荐接口FlaskTOP10榜单、新书推荐JSON8001模拟支付通知Flask接收支付结果回调8001Django负责的是重活它有内置ORM、Admin后台、用户认证体系、CSRF防护、事务管理。图书管理后台几乎可以直接基于Django Admin二次开发用户和权限也不用自己造轮子省下的时间足够把订单和库存的业务逻辑打磨清楚。Flask负责的是轻活热销榜、新书推荐这类接口数据简单只需要对外返回JSON不需要模板渲染、不需要复杂ORM。用Flask写这类纯API接口十分顺手代码量极小而且天然和Django主服务隔离。万一以后推荐逻辑要调优、要频繁更新算法只动Flask那个服务就够了不会牵扯到整个商城主站。实际部署时两个服务分别跑在8000和8001端口前端页面统一走Django推荐数据要么由前端直接请求Flask、要么由Django侧转发一次。这套结构稍微带了一点微服务拆分的味道但又不至于复杂到需要上消息队列和注册中心最适合课程设计、毕业设计和中小型练手项目。1.3 这套方案适合谁三类读者适合直接抄这份实现路径。第一类是毕业设计选了网上书店系统题目的人这套架构能在答辩时讲出双框架整合的亮点比单纯的CRUD加分不少第二类是学完Python基础、想通过一个完整项目找工作的转行者能展示出你对数据事务和部署流程的理解这在简历里是很能打的点第三类是已经在单独用Django或Flask做过项目、想理解服务拆分的人可以借这个项目体会什么时候该把模块拆出去。2. 项目脚手架搭建环境、目录与底层配置2.1 环境准备与依赖清单我建议直接用Python 3.10以上版本配合虚拟环境安装依赖。虚拟环境的作用是让每个项目的依赖互相隔离避免这台机器上跑着五六个项目时Django版本互相打架。python -m venv venv # Linux/macOS 激活 source venv/bin/activate # Windows 激活 venv\Scripts\activate pip install -r requirements.txtrequirements.txt的内容大致如下django4.2.* flask3.0.* django-cors-headers4.* flask-cors4.* requests2.* gunicorn21.* redis5.* PyMySQL1.*说明一点PyMySQL是生产环境连MySQL时用的开发阶段用SQLite就可以不用装。django-cors-headers解决的是浏览器直接请求Flask推荐接口时的跨域问题requests用于Django侧转发Flask接口gunicorn是生产环境的进程管理器runserver只适合开发调试。2.2 Django的项目初始化与App划分用命令创建项目然后按业务域拆成四个Appdjango-admin startproject store_books cd store_books python manage.py startapp users python manage.py startapp books python manage.py startapp cart python manage.py startapp ordersApp边界的建议是一个App只服务一个业务域。user只管用户资料与地址books只管图书与分类cart只管购物车条目orders只管订单和订单项。有人会把购物车和订单合并成一个App我试过后期会后悔——购物车是临时意向单订单是已确定的交易记录生命周期和状态转换完全不同混在一起你不得不在同一个models.py里来回横跳。分开之后订单事务里操作购物车时用明确的API调用而不是共享一堆状态变量。settings.py里注册App时记得加入INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, corsheaders, users, books, cart, orders, ]注意AUTH_USER_MODEL要在第一次migrate之前设好否则已有user表后再改自定义用户模型迁移会非常烦人这属于Django的一个经典坑下面章节会专门讲。2.3 Flask服务的初始化与蓝图规划Flask部分的目录结构我习惯分成两层最外层一个run_flask.py作为进程入口内部按api和service区分。api目录放路由蓝图service目录放业务逻辑比如读写Redis的代码。from flask import Flask from flask_cors import CORS def create_app(): app Flask(__name__) app.config[JSON_SORT_KEYS] False app.json.ensure_ascii False # Flask 2.3 让JSON输出正常显示中文 CORS(app, resources{r/api/*: {origins: [http://127.0.0.1:8000, http://your_domain.com]}}) from api.recommend import recommend_bp app.register_blueprint(recommend_bp, url_prefix/api) return app app create_app()run_flask.py里直接创建app实例后面gunicorn启动时就是指向这个文件里的app对象。2.4 配置文件的统一约定开发环境和生产环境的最大区别是DEBUG、数据库、密钥这三项。我建议不要在settings.py里写死这些值而是用python-dotenv读取.env文件from dotenv import load_dotenv import os load_dotenv() SECRET_KEY os.getenv(DJANGO_SECRET_KEY, dev-insecure-key) DEBUG os.getenv(DJANGO_DEBUG, True) True ALLOWED_HOSTS os.getenv(DJANGO_ALLOWED_HOSTS, localhost,127.0.0.1).split(,)数据库连接我放在一个if判断里开发默认SQLite生产自动切换到MySQLif os.getenv(DB_ENGINE) mysql: DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: os.getenv(DB_NAME), USER: os.getenv(DB_USER), PASSWORD: os.getenv(DB_PASSWORD), HOST: os.getenv(DB_HOST, 127.0.0.1), PORT: os.getenv(DB_PORT, 3306), } } else: DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } }Flask侧配置同理单独维护一个config.py把Redis地址、服务端口都放进去。把密钥和数据库密码从代码里分离出去无论毕设展示还是将来上线都属于必须养成的习惯。3. 数据库设计图书、用户、购物车、订单四张核心表怎么串起来3.1 图书模型字段设计和类型选择图书是商城的核心资产字段设计直接影响后续所有模块。我的Book模型长这样class Category(models.Model): name models.CharField(max_length50, uniqueTrue) parent models.ForeignKey( self, nullTrue, blankTrue, on_deletemodels.CASCADE, related_namechildren, ) class Book(models.Model): title models.CharField(max_length200, db_indexTrue) author models.CharField(max_length100) publisher models.CharField(max_length100, blankTrue) isbn models.CharField(max_length13, uniqueTrue) category models.ForeignKey(Category, on_deletemodels.PROTECT, related_namebooks) price models.DecimalField(max_digits8, decimal_places2) stock models.PositiveIntegerField(default0) sales_count models.PositiveIntegerField(default0) cover models.ImageField(upload_tocovers/, blankTrue) description models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue)有几个选择必须解释一下。title加db_index是因为图书列表页最常见的操作就是按书名搜索索引能让模糊查询不至于全表扫一遍。isbn用uniqueTrue图书国际标准书号天然唯一如果不设唯一约束后台录入重复书号会导致展示混乱。price必须用DecimalField而不是FloatField——Float在计算0.10.2时会出现浮点误差涉及到金额的事最好一开始就选对类型。cover允许blank是因为不是所有书都有封面图没图时前端展示默认封面。Category做成自关联是为了支持计算机-编程语言-Python这样的多级分类。parent为空就是顶级分类查询时用select_related把父类别一次性带出来避免N1查询。3.2 用户模型扩展Django内置User的做法Django自带的User有username、password、email等字段够用于登录但商城还需要手机号、收货地址。正确的扩展方式不是新建一张UserProfile表去外键关联而是直接继承AbstractUserclass User(AbstractUser): mobile models.CharField(max_length11, blankTrue)然后在settings.py里加上AUTH_USER_MODEL users.User这句必须写在第一次执行makemigrations之前。如果你已经migrate出auth_user表再改这里Django会报一堆依赖错误处理起来相当难受。我刚上手Django时在这上面耗过两三个小时现在都会提醒项目一创建就先配好自定义用户模型。地址信息单独建表class Address(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameaddresses) receiver models.CharField(max_length50) phone models.CharField(max_length20) province models.CharField(max_length30) city models.CharField(max_length30) detail models.CharField(max_length200) is_default models.BooleanField(defaultFalse)related_nameaddresses的含义是拿到一个user对象后可以直接user.addresses.all()读取该用户全部地址。下单时默认选中is_defaultTrue那条没有默认地址就让用户现填。3.3 购物车模型为什么选数据库表而不是Session购物车实现有两条路线。Session方案不建表把购物车数据塞进session简单但致命缺陷是用户换设备、清浏览器缓存后购物车直接消失而且服务端无法统计流失数据。数据库表方案需要登录才能加购但是数据永久保留、跨设备一致管理员还能分析加购转化率。图书商城面对的是一定需要注册下单的真实用户我选数据库方案class CartItem(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namecart_items) book models.ForeignKey(Book, on_deletemodels.CASCADE, related_namecart_items) quantity models.PositiveIntegerField(default1) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (user, book)unique_together保证同一个用户对同一本书只有一条购物车记录。这样加购接口的逻辑就是查得到就增加数量查不到就新建而不是每次点击都插入一行。on_delete参数这里也要慎重。user删除时购物车一并清空所以用CASCADEbook删除时购物车条目也删掉因为商品不存在了。这和订单项里的PROTECT策略形成对比下面会讲。3.4 订单与订单项价格快照为什么如此重要订单是商城系统的交易凭证不能随便变。模型的要点是订单主表和订单项表分开class Order(models.Model): STATUS_CHOICES [ (PENDING, 待支付), (PAID, 已支付), (SHIPPED, 已发货), (COMPLETED, 已完成), (CANCELED, 已取消), ] order_no models.CharField(max_length32, uniqueTrue, db_indexTrue) user models.ForeignKey(User, on_deletemodels.PROTECT, related_nameorders) address models.ForeignKey(Address, on_deletemodels.PROTECT) total_amount models.DecimalField(max_digits10, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultPENDING) created_at models.DateTimeField(auto_now_addTrue) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) book models.ForeignKey(Book, on_deletemodels.PROTECT) book_title models.CharField(max_length200) book_price models.DecimalField(max_digits8, decimal_places2) quantity models.PositiveIntegerField(default1)OrderItem里出现book_title和book_price的快照字段这是很多新手会省略的部分。为什么要冗余这份数据场景是这样的用户今天下单时《Python实战》卖98元明天运营把价格改成128元——订单已经生效总价就应该按98元结算不能因为图书表被改就跟着变。同样图书也可能因为版权原因下架但历史订单的内容必须仍然可查。所以在下单那一刻把书名和价格复制一份到订单项里之后无论图书表怎么变动订单数据都保持稳定。on_delete的设计也有讲究Order对User和Address都用PROTECT意思是只要该用户下过单、该地址被订单引用过就不能被硬删防止历史订单查无此人、查无地址。OrderItem对Order用CASCADE则是因为订单删除时子项自然没有意义跟随主单清理是合理的。4. 商城核心链路编码从搜索到下单的完整逻辑4.1 图书列表、分类过滤与模糊搜索列表页是用户进入商城的第一站。Django ORM是懒加载的所以可以按参数动态拼查询集最后再统一执行不需要写又臭又长的if分支去拼接SQL字符串from django.db.models import Q from django.core.paginator import Paginator def book_list(request): books Book.objects.all().select_related(category) kw request.GET.get(kw, ).strip() if kw: books books.filter( Q(title__icontainskw) | Q(author__icontainskw) | Q(publisher__icontainskw) ) category_id request.GET.get(cat, ) if category_id.isdigit(): books books.filter(category_idint(category_id)) sort request.GET.get(sort, default) if sort price_asc: books books.order_by(price) elif sort sales: books books.order_by(-sales_count) paginator Paginator(books, 12) page_obj paginator.get_page(request.GET.get(page, 1)) return render(request, books/list.html, {page_obj: page_obj})Q对象在这里的作用是把多个搜索条件用OR连接起来。icontains是大小写不敏感的包含匹配对中文虽然没有大小写概念但对英文书名和作者名很关键。select_related(category)能把分类信息一次性join出来列表页渲染分类名不会每条都查一次数据库。4.2 购物车增删改数量更新不要用先读后写加购接口比较简单接收book_id和quantity校验参数后用get_or_create处理。真正容易踩坑的是数量修改。前端传来的通常是一个delta表示增加1还是减少1很多人的第一反应是读出来再加减后save但并发情况下这种先读后写会丢更新。正确做法是用F表达式让数据库在原子操作里完成加减from django.db.models import F def update_quantity(request, item_id, delta): item CartItem.objects.filter( pkitem_id, userrequest.user ).first() if not item: return JsonResponse({code: 404, msg: 购物车条目不存在}) new_qty item.quantity int(delta) if new_qty 0: item.delete() return JsonResponse({code: 0, msg: 已删除}) CartItem.objects.filter(pkitem.pk).update(quantityF(quantity) int(delta)) return JsonResponse({code: 0, msg: ok})F(quantity) 1在数据库内部执行等价于UPDATE cart_item SET quantity quantity 1 WHERE id ...整个过程不经过Python解释器读取旧值两个并发请求不会互相覆盖。订单详情页的加减数量按钮连续快速点击时这个写法能保证数量正确。下单后购物车条目需要清理这就是OrderItem生成后批量删除对应CartItem的原因交易真的落地了临时清单就清空。4.3 下单事务与并发扣库存select_for_update的正确用法图书有库存字段下单时必然要扣减库存。最危险的场景是两个用户同时买最后一本书都先读到stock1都判断库存充足然后各自执行stock-1最终库存变成-1。这是典型的并发写冲突。Django的解决方案是行级锁select_for_update但它必须配合transaction.atomic才能生效。单独拿出来调用时Django会直接忽略锁逻辑这个细节文档里写得不显眼很多人踩坑。from django.db import transaction login_required def create_order(request): selected_items request.POST.getlist(cart_item_ids) address_id request.POST.get(address_id) address Address.objects.filter(pkaddress_id, userrequest.user).first() if not address: return JsonResponse({code: 1, msg: 地址无效}) try: with transaction.atomic(): cart_items CartItem.objects.filter( pk__inselected_items, userrequest.user ).select_related(book) order Order.objects.create( order_nogenerate_order_no(), userrequest.user, addressaddress, total_amount0, statusPENDING, ) total 0 for item in cart_items: book Book.objects.select_for_update().get(pkitem.book_id) if book.stock item.quantity: raise ValueError(f《{book.title}》库存不足) book.stock - item.quantity book.sales_count item.quantity book.save() OrderItem.objects.create( orderorder, bookbook, book_titlebook.title, book_pricebook.price, quantityitem.quantity, ) total book.price * item.quantity item.delete() order.total_amount total order.save(update_fields[total_amount]) except ValueError as e: return JsonResponse({code: 1, msg: str(e)}) return JsonResponse({code: 0, order_id: order.id, total_amount: str(total)})select_for_update的作用是在事务内对那行Book记录加锁事务A先拿到锁读取、扣库存、提交事务B在A提交之前会一直等待等A提交后B再读到的是已经减过的库存接着走库存不足分支。这样就把并发情况下库存变负数的窗口堵死了。整个过程都要包在同一个事务里还有一个原因订单生成、库存扣减、订单项创建、购物车清理这四件事必须全成功或全失败。如果订单建好了、库存没扣成交易就乱了页面用户明明下单成功仓库却还有货反过来库存扣了但订单项插入失败用户付款后发现订单列表是空的。transaction.atomic保证了要么全部落库要么全部回滚。4.4 模拟支付与订单状态机真正接微信/支付宝需要商户号、证书、异步回调验签毕设和练手阶段完全可以用模拟支付代替。但是模拟支付不代表随便写状态流转必须用状态机的方式管理否则时间长了代码里到处是if status PAID的判断乱成麻。class Order(models.Model): def can_transition_to(self, new_status): allowed { PENDING: {PAID, CANCELED}, PAID: {SHIPPED}, SHIPPED: {COMPLETED}, COMPLETED: set(), CANCELED: set(), } return new_status in allowed.get(self.status, set())配上这张流转表当前状态允许跳转到的状态触发动作PENDINGPAID / CANCELED用户支付 / 用户取消PAIDSHIPPED后台发货SHIPPEDCOMPLETED用户确认收货COMPLETED无终态CANCELED无终态支付接口的逻辑就是订单属于当前登录用户、状态是PENDING才允许改成PAID。这样杜绝了还没支付就发货取消后还能支付这类逻辑漏洞。Flask那边可以提供一个支付回调接口模拟第三方支付平台把支付结果推给主站。前端在订单页点立即支付后Flask接口收到通知再通过requests回调Django的一个内部视图去更新状态。这正好呼应了前面提到的Flask辅助模块角色除了推荐榜单支付通知这种非页面型接口也很适合放在Flask侧。5. Flask辅助模块的实现热销榜单与支付回调5.1 热销推荐接口为什么拆出去热销榜的逻辑很简单Book表按sales_count倒序取前十就行。但简单不代表应该塞在Django主进程里。实际收益有三个第一推荐逻辑可能频繁调整比如加本周上升榜同分类推荐如果都在主站里改每次部署都要重启整个商城第二销量数据写入Redis后推荐接口可以不查数据库访问高峰期给数据库减负第三从工程练习角度看这是理解服务拆分的入门级案例——拆出去的Flask服务仍然独立可运行、可单独测试。5.2 基于Redis的销量榜缓存实现思路是Django下单成功后把book_id写入Redis的有序集合score是销量累加值import redis REDIS_CLIENT redis.Redis( hostsettings.REDIS_HOST, portsettings.REDIS_PORT, db0, decode_responsesTrue, ) def incr_book_sales(book_id, quantity1): REDIS_CLIENT.zincrby(book_sales, quantity, str(book_id))Flask推荐接口里直接取前10from flask import Blueprint, jsonify recommend_bp Blueprint(recommend, __name__) recommend_bp.route(/recommend) def recommend(): top_book_ids REDIS_CLIENT.zrevrange(book_sales, 0, 9) books get_book_brief_info(top_book_ids) # 对应书目信息 return jsonify({code: 0, data: books})zrevrange按score从大到小返回前10个元素也就是销量最高的十本书。要注意decode_responsesTrue保证返回的是字符串而非字节串否则还要多做一次decode。榜单数据允许一定程度的偏差。用户下单后Redis加1假设Redis刚重启丢了数据榜单最多暂时不准不影响交易。所以这里不需要强一致性也不需要搞分布式锁。Redis挂了怎么办Flask接口加一个try except降级为查数据库try: top_book_ids REDIS_CLIENT.zrevrange(book_sales, 0, 9) except redis.ConnectionError: top_book_ids []接口降级之后仍然返回结果只是可能不是热销前十用户体验不受影响。这个兜底逻辑在实际部署中很重要。5.3 Django调用Flask接口的两种接线方式拆出去的服务总得和主站对接对接方式可以选前端直连和后端代理两种。方式A前端直接请求Flask。页面里写一段fetch请求http://127.0.0.1:8001/api/recommend然后渲染榜单。这种方式实现最直接但前端要能跨域访问8001端口需要靠Flask-CORS放行。由于榜单是公开数据不涉及登录态前端直连可用。方式B由Django视图代理请求Flask再把结果塞进模板。这种方式对浏览器透明浏览器只认识Django跨域问题从根本上不存在。import requests def home(request): top_books [] try: resp requests.get( f{settings.FLASK_SERVICE_URL}/api/recommend, timeout2, ) if resp.ok: top_books resp.json().get(data, []) except requests.RequestException: pass return render(request, home.html, {top_books: top_books})生产环境我推荐方式B因为Nginx只需要把所有流量都指向DjangoFlask服务地址只存在于后端配置里不会暴露给用户。requests库要设置timeout否则Flask服务挂掉时Django视图会一直等到连接超时页面卡死。5.4 CORS配置与中文JSON输出如果前端直连方式Flask侧必须处理跨域。我第一次做的时候直接在CORS里写origins*结果前端带着Cookie请求时报错。实际原因是跨域场景携带Cookie时服务端不允许使用通配符来源必须明确指定允许的域名。CORS(app, resources{r/api/*: {origins: [http://127.0.0.1:8000, http://your_domain.com]}})还要处理中文显示成\uXXXX的问题。Flask 2.3之后配置项变了新手常在这卡住# Flask 2.3 app.json.ensure_ascii False # 老版本写法 # app.config[JSON_AS_ASCII] False不设置的话jsonify返回的中文书名会变成\u56fe\u4e66这种转义序列虽然前端解析后能还原但接口测试直接看输出就是一堆乱码非常容易误判成编码问题。6. 部署实战与避坑记录6.1 用gunicorn同时托管两个Python服务开发时用python manage.py runserver没问题但上线不能这么干。runserver是Django自带的开发服务器单进程、性能上限低只服务调试场景跑到几十个并发请求就开始吃力。生产环境用gunicorn起真正的多进程服务。Django侧gunicorn store_books.wsgi:application --bind 127.0.0.1:8000 --workers 3 --timeout 30Flask侧gunicorn run_flask:app --bind 127.0.0.1:8001 --workers 2 --timeout 30workers数量不必盲目加大。我习惯先各开2到3个worker压测后发现请求排队再增加。worker太多反而会因频繁上下文切换降低单机吞吐。两个服务分别占8000和8001端口互不干扰。这样改动推荐逻辑时只需要重启Flask进程不影响正在处理订单的Django主进程。6.2 Nginx反向代理与静态资源部署结构一般长这样浏览器请求NginxNginx按路径分发到两个后端服务。server { listen 80; server_name your_domain.com; client_max_body_size 20m; location /static/ { alias /home/deploy/store_books/staticfiles/; } location /media/ { alias /home/deploy/store_books/media/; } location /api/ { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { 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; } }静态文件为什么要交给Nginx而不是Django因为Django处理静态文件需要经过Python的整个WSGI链路效率远低于Nginx直接读磁盘文件。部署时先执行python manage.py collectstatic收集所有App静态文件到STATIC_ROOT指定目录Nginx的alias指到那个目录即可。media目录放的是用户上传的图书封面和头像也要固定持久化目录不然服务器重启或者重新部署代码时图片全部丢失。6.3 部署期踩过的几个坑把整个过程里最典型的问题整理成一张表每个都值得记一下现象根本原因排查与解决登录后提交表单报403 CSRF验证失败模板里没渲染csrf_token或跨域POST未加白名单表单加{% csrf_token %}API接口按实际场景校验或配置CSRF_TRUSTED_ORIGINSSQLite在并发写入时报database is locked多gunicorn worker同时写SQLite文件锁冲突开发期可接受生产切换到MySQL临时做压测可先降为单worker上传的图书封面图片404settings没配置MEDIA_URL/MEDIA_ROOT或Nginx少了media locationsettings里补齐全并save后重启Nginx挂/media/目录页面展示时间比北京时间慢8小时TIME_ZONE与USE_TZ配置不一致统一设TIME_ZONEAsia/Shanghai按需使用django.utils.timezone.localtime转换输出Flask接口返回\u4E66\u7C4DJSON默认ASCII转义Flask 2.3设置app.json.ensure_asciiFalse请求量大时页面偶发超时gunicorn timeout太短或Flask没有兜底降级调大--timeoutRedis挂掉时降级为查数据库每个坑的通用排查顺序是先看服务日志——Django的runserver控制台、gunicorn的stderr、Flask的访问日志都别关再curl对应接口看返回最后才怀疑代码逻辑问题。95%的部署问题都是配置层面的不是代码bug。6.4 部署前必须检查的安全配置上线前过一遍这几个点DEBUG设为False否则报错页面会直接把配置信息、数据库连接串暴露给访问者ALLOWED_HOSTS填实际域名不让陌生Host访问SECRET_KEY用环境变量注入不要出现在git仓库里数据库账号不要用root给应用分配一个最小权限账号。这些步骤花不了十分钟但很多新手项目对外一演示就被挖出后台地址到时再补救就很被动了。Flask侧同理监听地址如果不是需要外部直接访问就绑定127.0.0.1让Nginx做唯一入口。最后说说我做完这个项目最大的体会。网上书店的表结构和业务逻辑并不算难真正拉开差距的地方全在边界上哪些模块放Django、哪些放Flask是高一层的主从边界库存扣减和订单生成之间的事务边界是数据正确性的底线购物车条目和订单项之间的快照关系是业务稳定性的边界。把这些边界想清楚了编码就是往骨架里填肉。如果你正准备拿这个题目交毕设或者练手我建议先别急着敲代码花半天时间把用户、图书、购物车、订单这四张表的关系和订单状态流转画在纸上表结构定稿之后再动手写views整个过程会顺畅非常多。