
做游泳用品商城这个项目说实话一开始是有点被需求推着走的。朋友开了家线下游泳用品专卖店夏天生意不错但一到淡季就只能靠老客带新客他一直想搞个线上商城把泳衣、泳镜、泳帽、泳圈这些SKU挂到网上去。我接手时的第一反应不是这项目多简单而是——这类商品属性太碎了泳镜有度数、泳衣有尺码和花色、泳帽有材质之分和卖标准化的3C数码完全不是一个建模思路。所以这篇文章不打算给你堆一堆高深理论就实实在在复盘一下我用Python基于Django框架从零搭建这套游泳用品专卖店系统的全过程包括业务模型怎么拆、下单库存怎么处理并发、后台怎么管理以及最后如何用waitress nginx把它部署到生产环境。如果你正准备用Django做类似的中小型商城系统这篇应该能让你少踩不少坑。1. 先想清楚再动手游泳用品商城的业务模型与技术选型1.1 游泳用品品类的特殊性在哪里很多初学者拿到游泳用品专卖店这个需求第一反应就是照搬通用的电商Demo把商品、分类、购物车、订单一摆就完事。但真正做过垂直品类商城的人会告诉你垂直品类的核心价值恰恰在于垂直二字。游泳用品的商品属性非常不规整泳衣分男士、女士、儿童每个性别下面又分连体、分体、平角、三角尺码从XS到XXL还有花色和款式的区分。泳镜有平光、近视度数之分近视度数从150度到800度不等还有成人款和儿童款。泳帽硅胶、布料、PU涂层材质直接影响价格和使用场景。泳圈与浮板要看适用年龄段、承重、直径尺寸属于规格差异大的商品。这意味着商品表如果只设计一个简单的Product模型后面做购物车、订单的时候会非常痛苦。用户在详情页选的不是一件泳衣而是这件泳衣的藏蓝色M码——这个具体到某个属性组合的库存单位就是SKUStock Keeping Unit。所以我在建模时商品Product和规格SKU是严格分离的两张表这也是后面整个交易链路能够跑通的基础。1.2 为什么选择Django而不是Flask或Spring Boot技术选型没有绝对的对错只有适不适合当前场景。这个项目我选Django理由其实很朴素。先说MTV模式。Django的MTVModel-Template-View经常被拿来和MVC做对比很多人纠结两者区别实际开发中你只需要理解一句话Model管数据Template管展示View管业务逻辑。Template作为一个独立的层可以让前端模板和后端业务解耦商品详情页、购物车页、结算页各用各的模板互不干扰。对于商城这种页面结构相对标准化的项目Django的模板系统开箱即用比Flask需要自己选Jinja2搭配方案要省心。再说它的全家桶特性。做商城绕不开用户认证、后台管理、表单校验、CSRF防护、Session管理等。这些Django全部自带尤其是Admin后台——虽然生产环境不建议直接用默认Admin给运营用但它对于快速搭建管理端原型、给店主先看效果价值太大了。我第一版后台就是基于Django Admin二次开发的商品上架、订单查看、库存修改几乎零成本实现。另外Django的ORM非常成熟对于后续要讲的事务处理、行锁select_for_update、预取关联数据prefetch_related写法都比裸SQL更安全、更不容易出错。如果换成Spring Boot从Java环境搭建到MyBatis或JPA配置起步成本高一大截对这样一个中小型垂直商城来说属于杀鸡用牛刀。1.3 项目功能边界的划分一个完整的游泳用品商城系统我把它拆成两部分用户端和管理端。用户端需要做到用户注册、登录、个人资料管理商品分类浏览、关键词搜索、按销量/价格/新品排序商品详情页展示多图、SKU规格选择、库存显示购物车添加/修改/删除支持未登录状态下使用订单创建、模拟支付、订单状态查看、确认收货收货地址管理管理端则围绕运营需求商品管理上架、下架、修改价格/库存、设置主图和详情图订单管理查看订单、发货、处理退款类目管理用户管理查看注册用户、禁用恶意账号功能边界想清楚以后我不会一上来就写代码而是先把数据库模型设计出来。这一步省不了商城系统的数据模型一旦建错后期改起来是牵一发动全身。2. 环境配置到工程初始化每一步都值得认真对待2.1 开发环境版本的选择逻辑Django项目最怕的就是版本隐患。我本地用的是Python 3.10Django选择了4.2 LTS版本。为什么不追新用Django 5.x因为4.2是长期支持版本第三方库兼容性更好尤其是MySQL驱动、图片处理库这些在LTS版本上踩坑的概率低很多。数据库方面开发环境用了SQLite生产环境切成MySQL 8.0。为什么开发和生产用不同的库SQLite是文件型数据库零配置本地调试非常方便但生产环境并发一上来SQLite的写锁问题就会暴露。所以我会在开发阶段就尽量用ORM的标准写法避免用到SQLite特有语法切换数据库时只需要改settings.py里的配置和安装对应的驱动。依赖管理我建议从一开始就用虚拟环境python -m venv venv source venv/bin/activate # Windows环境执行 venv\Scripts\activate pip install django4.2.* mysqlclient # 如果连MySQL pip freeze requirements.txt有个小习惯值得养成每次安装新依赖后第一时间更新requirements.txt并且把虚拟环境目录加入.gitignore。项目开发到一半换机器、加人的时候你会感谢这个习惯。2.2 创建Django工程与应用创建工程和应用是再基础不过的操作但应用拆分这事很多人没想清楚就动手了。django-admin startproject swim_shop cd swim_shop python manage.py startapp goods python manage.py startapp users python manage.py startapp cart python manage.py startapp orders我个人按业务域拆分应用而不是按前端、后端这种技术层拆分users用户模型、注册登录、收货地址goods商品、分类、SKU、商品图片cart购物车逻辑orders订单、订单明细、支付状态拆分的好处是每个应用的内聚性高职责清楚。商城项目后期一定会加功能比如优惠券、评论系统那时候再加独立应用即可不会把某个应用撑成几千行的上帝模块。应用创建后别忘了两件事一是在settings.py的INSTALLED_APPS里注册二是把应用的urls.py用include挂到主路由。初次接触Django的人经常在这两个地方漏掉配置结果页面404了还不知道原因。2.3 VSCode中的Django调试配置日常开发我习惯用VSCode但要打断点调试Django请求必须手动配置一下调试器。很多人不知道Django在VSCode里可以直接用Python Debugger附加到开发服务器上。推荐在项目根目录建.vscode/launch.json{ version: 0.2.0, configurations: [ { name: Django Server, type: debugpy, request: launch, program: ${workspaceFolder}/manage.py, args: [runserver, 127.0.0.1:8000], django: true, justMyCode: false } ] }这里django: true是关键它让调试器识别Django的模板和ORM上下文遇到异常时可以直接在调试控制台里查看SQL语句。justMyCode: false表示可以进入第三方库内部代码调试排查Django源码问题时非常有用。另外如果你的VSCode里Python解释器选错了会直接导致cannot be resolved against python helper roots这类报错。处理方法很简单CtrlShiftP输入Python: Select Interpreter选择虚拟环境里的python.exe问题立刻消失。3. 数据库建模游泳用品商城的关键是SKU与状态管理3.1 商品模型设计从类目到SKU这是整个项目的核心我花了最多时间设计。先看最终落地的模型代码再逐个解释为什么这么建from django.db import models class Category(models.Model): name models.CharField(max_length50, verbose_name类目名称) parent models.ForeignKey( self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name父级类目 ) sort_order models.IntegerField(default0, verbose_name排序权重) class Meta: verbose_name 商品类目 verbose_name_plural verbose_name def __str__(self): return self.name class Product(models.Model): category models.ForeignKey( Category, on_deletemodels.PROTECT, verbose_name所属类目 ) title models.CharField(max_length200, verbose_name商品标题) subtitle models.CharField(max_length255, blankTrue, verbose_name副标题) main_image models.ImageField(upload_toproducts/%Y/%m/, verbose_name主图) detail models.TextField(verbose_name商品详情) is_active models.BooleanField(defaultTrue, verbose_name是否上架) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) updated_at models.DateTimeField(auto_nowTrue, verbose_name更新时间) class Meta: verbose_name 商品 verbose_name_plural verbose_name indexes [ models.Index(fields[category, is_active]), ] class SKU(models.Model): product models.ForeignKey( Product, related_nameskus, on_deletemodels.CASCADE, verbose_name所属商品 ) spec_values models.CharField(max_length255, verbose_name规格值如藏蓝色/M) price models.DecimalField(max_digits10, decimal_places2, verbose_name售价) original_price models.DecimalField( max_digits10, decimal_places2, nullTrue, blankTrue, verbose_name原价 ) stock models.PositiveIntegerField(default0, verbose_name库存) sku_code models.CharField(max_length64, uniqueTrue, verbose_nameSKU编码) is_active models.BooleanField(defaultTrue, verbose_name是否启用) class Meta: verbose_name SKU verbose_name_plural verbose_name这里面有几个关键设计决策第一个是Category的自关联。游泳用品可以先分泳衣、泳镜、泳帽、配件几大类每个大类下还能再分小类比如配件下面有泳圈、浮板、耳塞鼻夹。自关联字段用一个parent指向自己就能实现无限级类目前端展示树形结构也很方便。第二个是商品和SKU分离。Product管商品的公共信息比如标题、主图、详情SKU管具体可下单的规格组合比如藏蓝色/M码。每个SKU有自己的价格和独立库存。为什么不把价格直接放在Product上因为泳衣不同尺码价格一样但像泳镜带度数和不带度数价格就差很多如果价格绑在商品上就无法支持这种差异化定价。第三个是on_deletemodels.PROTECT。当我们尝试删除一个还被商品引用的类目时数据库会拒绝防止误操作导致商品变成无类目的孤儿数据。3.2 用户、地址、购物车与订单模型用户模型这里我推荐用一个实际开发中很常见的做法——扩展Django自带的User而不是自己另建一张用户表from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length20, blankTrue, verbose_name手机号) avatar models.ImageField(upload_toavatars/%Y/%m/, blankTrue, verbose_name头像) class Meta: verbose_name 用户 verbose_name_plural verbose_name扩展AbstractUser的好处是直接继承Django完整的认证体系登录、会话、权限这些不用重写只需要加你自己的业务字段。记得在settings.py里设置AUTH_USER_MODEL users.User这一行必须在第一次执行migrate之前配置好否则后面换用户模型会非常麻烦。收货地址、购物车、订单这三张表的设计逻辑class Address(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameaddresses, verbose_name所属用户) receiver models.CharField(max_length50, verbose_name收货人) phone models.CharField(max_length20, verbose_name联系电话) province models.CharField(max_length50, verbose_name省) city models.CharField(max_length50, verbose_name市) district models.CharField(max_length50, verbose_name区) detail models.CharField(max_length255, verbose_name详细地址) is_default models.BooleanField(defaultFalse, verbose_name是否默认) class CartItem(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namecart_items, verbose_name所属用户) sku models.ForeignKey(goods.SKU, on_deletemodels.CASCADE, verbose_nameSKU) quantity models.PositiveIntegerField(default1, verbose_name数量) created_at models.DateTimeField(auto_now_addTrue, verbose_name加入时间) class Order(models.Model): STATUS_PENDING pending STATUS_PAID paid STATUS_SHIPPED shipped STATUS_COMPLETED completed STATUS_CANCELLED cancelled STATUS_CHOICES [ (STATUS_PENDING, 待支付), (STATUS_PAID, 已支付), (STATUS_SHIPPED, 已发货), (STATUS_COMPLETED, 已完成), (STATUS_CANCELLED, 已取消), ] order_no models.CharField(max_length64, uniqueTrue, verbose_name订单编号) user models.ForeignKey(User, on_deletemodels.PROTECT, related_nameorders, verbose_name下单用户) address_snapshot models.TextField(verbose_name地址快照) total_amount models.DecimalField(max_digits10, decimal_places2, verbose_name订单总额) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultSTATUS_PENDING, verbose_name订单状态) created_at models.DateTimeField(auto_now_addTrue, verbose_name下单时间) paid_at models.DateTimeField(nullTrue, blankTrue, verbose_name支付时间) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems, verbose_name所属订单) sku models.ForeignKey(goods.SKU, on_deletemodels.PROTECT, verbose_nameSKU) product_title models.CharField(max_length200, verbose_name商品标题快照) spec_values models.CharField(max_length255, verbose_name规格快照) price models.DecimalField(max_digits10, decimal_places2, verbose_name成交单价) quantity models.PositiveIntegerField(default1, verbose_name成交数量)订单表一定要加快照字段address_snapshot、product_title、spec_values。为什么要冗余这些数据因为下单之后用户可能改了收货地址商品可能下架或改标题但历史订单必须保留下单那一刻的信息。这种以空间换正确性的设计在交易系统里是必须的绝对不要通过关联查询去动态获取下单时的商品标题和价格。3.3 逻辑删除为什么我不轻易物理删除商品Django的ORM执行delete()时默认是物理删除就是真的从数据库里把这条记录删掉。在商城系统中这很危险。想象一个场景用户A在5月买了一件泳衣6月管理员觉得这款泳衣不想卖了直接在后台删除了商品记录。这时候用户A的个人中心再查看历史订单关联查询商品就会报错——即使订单表里存了标题快照商品详情页也无法访问。更严重的是如果删除操作没有处理好关联数据可能连带把订单明细、购物车记录一起删掉。我的方案是给Product和SKU增加is_active布尔字段下架操作只是把is_active设为False而不是调用delete()。查询商品列表时统一过滤products Product.objects.filter(is_activeTrue)如果确实需要物理删除我会重写模型的delete方法class Product(models.Model): # ... def delete(self, *args, **kwargs): self.is_active False self.save(update_fields[is_active])这样外部调用product.delete()时实际执行的是逻辑删除防止管理层误操作。4. 核心交易链路实现购物车、下单并发与库存扣减4.1 购物车方案选型Session还是数据库购物车是商城绕不开的模块设计上有两种主流方案纯Session购物车用户未登录也能加购数据存在服务端Session里实现简单。缺点是用户换设备购物车就丢了而且Session存在服务端内存/数据库量大了有压力。纯数据库购物车数据落库用户登录后任何设备都能看到购物车。缺点是未登录用户没法用或需要先强制登录。我实际用的是数据库优先 Session兜底的混合方案。用户在未登录状态下加购购物车数据写入Session登录后如果Session里有购物车数据就合并到数据库的CartItem表然后清空Session。这样既照顾了用户体验又保证了购物车的持久性。合并购物车是很容易出错的地方核心逻辑如下def merge_cart(request, user): session_cart request.session.get(cart, {}) if not session_cart: return for sku_id, quantity in session_cart.items(): cart_item, created CartItem.objects.get_or_create( useruser, sku_idsku_id, defaults{quantity: quantity} ) if not created: cart_item.quantity quantity cart_item.save() request.session[cart] {}4.2 下单流程的事务控制阻止库存超卖商城系统最怕的Bug就是超卖——页面显示库存10件结果卖出去了15单。Django的ORM要解决这个问题必须正确使用事务和行锁。看这段创建订单的核心代码from django.db import transaction from django.db.models import F def create_order(user, address, sku_items): # sku_items: [{sku_id: 1, quantity: 2}, ...] with transaction.atomic(): order Order.objects.create( order_nogenerate_order_no(), useruser, address_snapshotformat_address(address), total_amountDecimal(0), statusOrder.STATUS_PENDING ) total Decimal(0) order_items [] for item in sku_items: sku SKU.objects.select_for_update().get(iditem[sku_id]) if sku.stock item[quantity]: raise StockNotEnough(f{sku.sku_code} 库存不足) sku.stock F(stock) - item[quantity] sku.save(update_fields[stock]) order_items.append(OrderItem( orderorder, skusku, product_titlesku.product.title, spec_valuessku.spec_values, pricesku.price, quantityitem[quantity] )) total sku.price * item[quantity] OrderItem.objects.bulk_create(order_items) order.total_amount total order.save(update_fields[total_amount]) return order这里的灵魂是select_for_update()。它在数据库层面给选中的SKU行加了排他锁事务提交前其他事务无法修改同一行只能在锁释放后继续执行。两个用户同时购买同一件库存仅剩1件的泳镜时第一个事务锁住SKU行并扣减库存第二个事务会等待锁释放然后重新读取到库存为0触发StockNotEnough异常从源头杜绝了超卖。用F(stock) - item[quantity]而不是sku.stock - item[quantity]也很关键。F表达式把读值-计算-写回操作转换成数据库端的原子操作避免并发时读到脏数据。另外注意我是在事务内先创建Order再创建OrderItem最后回填total_amount。初始订单总金额为0等明细都算清楚了再更新这样即使中间出错回滚也不会出现订单金额为Null或0的不一致状态。4.3 订单状态的流转与超时关闭订单状态我用的是字符串常量加choices而不是用状态机库因为5种状态足够简单不需要引入额外依赖。状态流转规则待支付 → 用户点击支付 → 已支付待支付 → 超过30分钟系统自动关闭 → 已取消已支付 → 商家后台发货 → 已发货已发货 → 用户确认收货 → 已完成超时关闭订单我用Django的manage.py自定义命令实现配合cron定时执行# orders/management/commands/close_expired_orders.py python manage.py close_expired_orders命令里查询待支付且创建时间早于当前时间30分钟以上的订单批量改为已取消同时把锁定的库存释放回SKU。这类后台任务用Django自定义命令比Celery更轻量适合中小型项目。4.4 商品查询视图过滤、分页、排序用户端的商品列表页我实现了按类目浏览、关键词搜索、按价格/销量/新品排序。这里要提醒一个ORM性能问题列表页使用select_related(category)避免每行商品都查询一次类目名称。Django内置分页器Paginator已经够用但要注意传给模板的每页数据最好是Page对象而不是整个Page.object_list这样才能在模板里正确展示页码。实际项目中我还会记录request.GET的原始参数保证翻页时筛选条件不丢失。5. 后台管理基于Django Admin的二次开发与权限控制5.1 让Admin后台真正可用列表定制与搜索默认的Django Admin注册一个模型之后虽然能用但体验很粗糙。商品管理列表我会在admin.py里定制from django.contrib import admin from .models import Product, SKU, Category admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display [id, title, category, is_active, created_at] list_filter [category, is_active] search_fields [title, subtitle] list_per_page 20 ordering [-created_at] actions [batch_on_shelf, batch_off_shelf] def batch_on_shelf(self, request, queryset): queryset.update(is_activeTrue) batch_on_shelf.short_description 批量上架 def batch_off_shelf(self, request, queryset): queryset.update(is_activeFalse) batch_off_shelf.short_description 批量下架list_display控制列list_filter提供侧边栏筛选search_fields支持关键词搜索actions提供批量操作。这三板斧下来运营日常维护商品的工作效率会高很多。5.2 RBAC权限用Django自带的Group和Permission后台不能所有登录用户都有全部操作权限。比如运营只能管商品客服只能看订单财务只能看订单金额。这就是RBAC基于角色的访问控制的典型需求。Django自带的权限体系包含User、Group、Permission三个核心模型。我创建了两个Group运营组赋予商品和类目的增加、修改、删除权限客服组赋予订单的查看和发货权限在Admin后台Django会为每个注册的模型自动生成 add/change/delete/view 四种权限。给组分配权限后再把用户加入对应的组即可。对于视图中需要额外控制的按钮可以用装饰器from django.contrib.auth.decorators import permission_required permission_required(orders.change_order) def ship_order(request, order_id): ...这样权限控制的粒度更细后端接口和Admin操作都能覆盖。5.3 库存预警功能游泳用品有季节性旺季库存消耗特别快所以我在Admin后台加了一个自定义页面显示库存低于安全阈值比如10件的SKU列表admin.register(SKU) class SKUAdmin(admin.ModelAdmin): list_display [sku_code, product, spec_values, price, stock] list_filter [is_active] def changelist_view(self, request, extra_contextNone): low_stock_skus SKU.objects.filter(stock__lte10, is_activeTrue).select_related(product) extra_context extra_context or {} extra_context[low_stock_skus] low_stock_skus return super().changelist_view(request, extra_contextextra_context)列表页顶部渲染一个低库存提醒卡片让管理员一进后台就知道哪些商品需要补货。这种做法比做复杂的可观测系统成本低得多但对小团队实际帮助极大。6. 生产部署waitress nginx 的完整配置实践6.1 为什么生产环境不能继续用runserver很多新手把项目跑起来之后直接在服务器上用python manage.py runserver对外提供服务。这在开发阶段没毛病生产环境绝对不行。Django的runserver是开发服务器单进程、单线程性能差而且没有经过安全加固不适合暴露在公网。我选择waitress作为WSGI服务器。它是纯Python实现的跨平台Windows和Linux都能跑不像gunicorn在Windows上支持不好。对于中小型商城waitress的性能足够稳定配置也简单。安装和启动命令pip install waitress waitress-serve --listen127.0.0.1:8000 swim_shop.wsgi:application生产环境我建议用进程管理工具托管Linux用supervisorWindows可以用NSSM或者任务计划程序保证进程挂了能自动重启。6.2 收集静态文件与媒体文件配置部署中最高频的坑就是静态文件404。开发环境下Django自动处理静态文件生产环境必须自己配。先在settings.py里明确几个路径DEBUG False ALLOWED_HOSTS [www.yourdomain.com, yourdomain.com] STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfiles MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media执行python manage.py collectstatic这个命令会把所有应用里的静态资源复制到STATIC_ROOT目录交给nginx统一托管。图片上传的媒体文件则放在MEDIA_ROOT也需要配置到nginx。之前有朋友在VSCode里写img标签发现Django的static目录下图片显示不出来多半是STATICFILES_DIRS没配置。开发环境下除了每个应用自带的static目录还可以在项目根目录建一个全局static目录并在settings.py里声明STATICFILES_DIRS [BASE_DIR / static]然后在模板里用模板标签引用{% load static %} img src{% static images/banner.jpg %} altbanner不要把路径硬编码写死这是静态文件引用最基本也最容易忽略的规则。6.3 nginx反向代理配置nginx在这里承担两个任务一是监听80/443端口将请求转发给waitress二是托管静态文件和媒体文件。一个精简但完整的server配置server { listen 80; server_name yourdomain.com; client_max_body_size 10m; location /static/ { alias /path/to/swim_shop/staticfiles/; expires 7d; } location /media/ { alias /path/to/swim_shop/media/; expires 7d; } 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; proxy_set_header X-Forwarded-Proto $scheme; } }X-Forwarded-Proto头很重要。如果Django启用HTTPS而用户实际通过HTTP访问缺少这个头会导致Django在生成绝对URL时误判协议产生安全警告。严格来说还应该在settings.py里加SECURE_PROXY_SSL_HEADER让Django正确识别经过代理的HTTPS请求。6.4 部署阶段容易踩的坑部署过程中我踩过几个印象深刻的坑单独列出来坑一ALLOWED_HOSTS没配好。部署后访问页面提示Invalid HTTP_HOST header。这其实是Django的安全机制在起作用它不接受未知域名的请求。解决方案就是在ALLOWED_HOSTS里把你实际访问的域名写进去。坑二DEBUGFalse后静态文件丢失。开发时DEBUGTrue一切正常一改成False样式就全崩了。原因就是生产环境Django默认不再处理静态文件必须靠collectstatic nginx。如果临时不想配nginx可以通过django.contrib.staticfiles的runserver --insecure应急但这个不能用于真实生产。坑三数据库连接报错。从SQLite切换MySQL时如果之前模型里有某些字段类型依赖SQLite的特性迁移可能失败。所以还是那句话开发阶段就用ORM标准字段不要用方言特性。7. 开发复盘几个值得记住的实操经验7.1 时间处理时区问题的隐形坑Django项目默认开启USE_TZ True数据库存储的是UTC时间模板渲染时自动转换成本地时间。这个机制本身没问题但如果你用datetime.datetime.now()而不是django.utils.timezone.now()存时间就会产生时区错乱——后台看到的下单时间比实际差了8个小时。我的习惯是代码中一律用timezone.now()获取当前时间创建时间、更新时间字段全部使用auto_now_add和auto_now计算订单超时时间时用UTC时间做比较展示层才转本地时区7.2 ORM的N1查询问题商城商品列表页有个经典性能问题查询了10个商品然后每个商品又要查一次类目名称总共执行11条SQL。这就是N1查询。解决办法是使用select_related和prefetch_related。对于外键这种单值关系用select_related它会用JOIN一次性查出来products Product.objects.select_related(category).filter(is_activeTrue)对于多值关系比如一个商品的多个SKU用prefetch_related它会先查商品列表再批量查SKU总共2条SQLproducts Product.objects.prefetch_related(skus).filter(is_activeTrue)在实际项目里这个优化能把列表页的响应时间从几百毫秒降到几十毫秒。写完代码之后建议用django-debug-toolbar观察SQL执行次数凡是看到1N模式一律用预取解决。7.3 图片上传与MEDIA路径的细节商品主图上传后马上在页面显示不出来是很常见的现象。检查三件事MEDIA_ROOT是否正确、MEDIA_URL是否和nginx配置一致、模板里是否用了{{ product.main_image.url }}而不是拼接字符串。有一点容易被忽略开发环境下Django不处理MEDIA文件的URL映射除非在urls.py里手动加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这段代码只在settings.DEBUG为True时生效生产环境交给nginx。很多新手在本地能显示图片一部署就白屏多半就是这个原因。7.4 版本管理与数据备份意识代码这块我强烈建议项目第一天就初始化Git仓库每完成一个功能模块提交一次commit。数据库备份则用定时任务导出SQL文件商城系统的订单数据是资产丢了恢复成本极高。我在部署服务器上写了一个简单的shell脚本每天凌晨用mysqldump备份数据库保留最近30天的备份文件曾不止一次在测试环境误操作后靠备份恢复数据。说实话做完这个游泳用品商城项目我最深的体会是垂直品类商城真正的工作量不在展示层而在商品模型的灵活性和交易链路的可靠性。SKU设计够不够好决定了后面所有功能的开发效率事务和锁用对了才能保证库存和订单数据不出大问题。Django这个框架在快速搭建这类系统时确实省力它把用户认证、Admin后台、ORM这些基础设施都给你备好了省下的时间足够你好好打磨业务逻辑。现在这套系统已经跑起来了商品可以上架下架、用户在手机上能下单购物、后台订单和库存都管得明明白白。如果后面要继续扩展直播带货、优惠券、多门店库存这些方向都能基于现在的模型往上加。我的建议是如果你也想做类似的商城系统不妨先从商品建模和订单状态这两块入手想清楚再动手后面会顺很多。