1. 为一个“购物系统”做技术选型,我为什么最后落在了 django + uni-app
我陆续做过几个信息管理类系统之后,决定挑战一个交易链路完整的在线购物平台。市面上现成商城源码很多,但大多绑定特定云厂商或者前端框架,改起来束手束脚。这次我给自己定的目标是:前后端完全分离,后端能扛住真实订单流转,前端一套代码同时覆盖小程序和移动端浏览器。折腾一圈,最终方案锁定为 django + uni-app。
先说结论:这套组合尤其适合“需要一个能完整演示、能真实上线、能继续扩展”的课程设计或个人项目。如果你也在犹豫技术栈,先别急着抄代码,我把我选型时怎么想的讲清楚。
选 django 的故事很简单:Python 生态里,django 自带 Admin 后台、ORM、迁移机制和认证体系,加上 DRF(Django REST Framework)之后写接口非常顺手。购物系统比普通 CRUD 复杂在订单、库存、支付这三件事的联动,django 的 ORM 支持事务和锁,能在这一层保证数据一致性。对比过 flask 和 fastapi,flask 过于自由,所有模块都要自己拼;fastapi 异步性能确实强,但配套生态和后台管理要额外搭。我的场景是“系统设计与实现”的完整交付,不是纯接口压测,django 的“全家桶”属性帮助最大。
选 uni-app 的理由更实际。我要同时交付微信小程序端和 H5 端,如果两端分开写,前端工作量直接翻倍。uni-app 基于 Vue 语法,一套代码编译到多端,虽然做不到 100% 零差异,但页面结构、业务逻辑、状态管理都能复用。社区里关于商城类项目的踩坑记录也多,真遇到问题不至于两眼一抹黑。这里也对比过 Taro,它基于 React,本身也很好,但我更熟 Vue 生态,从熟悉度出发选了 uni-app。
技术方向定了之后,我先把整体架构画在纸上:django 提供 RESTful API,uni-app 负责展示和交互,两者通过 HTTP + JSON 通信,登录态用 Token 维持,订单和支付走单独的服务逻辑模块。没有引入消息队列,也没有拆微服务,单体架构足够支撑中小流量,同时把代码结构拆干净,方便后续换成微服务也不至于重写。
这一节末尾我想强调一件事:选型别追新。项目的本质是解决“购物平台怎么做”,不是“什么技术最时髦”。django 在服务端开发里成熟了十几年,uni-app 在多端开发里积累了大量的生产案例,这两个工具足够把你从零带到上线。
2. 数据库设计是购物系统的一半:用户、商品、购物车、订单、库存的模型关系
很多第一次做商城的人上来就写商品表和订单表,结果联调时发现购物车和订单对不上、库存扣了但订单没生成。我的经验是,数据库设计阶段花两个小时,后面调试少花两天。
2.1 用户体系:不自己造轮子,但必须扩展 Profile
django 自带的 User 模型负责账号、密码、权限,这部分直接复用。购物系统需要手机号、收货地址、会员等级这些附加信息,所以另建一张 UserProfile 表,用 OneToOne 关联到 User。
为什么不用自定义 User 模型?如果你是在项目初期直接替换 AUTH_USER_MODEL,那没问题;但我这套是基于已有 django 项目扩展,中途换用户模型影响面太大,容易把迁移搞乱。用拓展表的方式反而简单:认证走原生 User,业务数据走 UserProfile。地址表单独建,一个用户可以有多条收货地址,下单时选择其中一条作为收货信息。
2.2 商品模型的层级与 SKU 设计
商品不能只是一张表,否则“红色 M 码”和“黑色 L 码”这种规格就会让表结构变得极其难维护。我按“商品 SPU + 商品规格 SKU”的经典结构来建:
- Product:商品主体,存标题、主图、详情描述、所属分类、上下架状态
- ProductSku:具体规格,存颜色、尺寸、价格、库存、SKU 编码
价格和库存放在 SKU 上,而不是 Product 上。比如一个商品有三个规格,每个规格价格可以不同,库存独立计算,下单时锁定的是 SKU 的库存,不是商品的库存。
订单明细里也要冗余一份商品标题、图片、价格快照。为什么?因为用户下单之后,商家可能修改商品价格或删除商品,订单里必须保留下单那一刻的信息。很多商城项目在这上面偷懒,订单详情直接关联商品表,最后对账时全是坑。
2.3 购物车与订单:临时态和持久态要分开
购物车是临时态,我设计了 Cart 和 CartItem 两张表,Cart 关联用户,CartItem 关联 Cart 和 ProductSku,记录数量和勾选状态。购物车不是订单的必需前置条件,允许用户直接点击“立即购买”,这时订单直接由商品 SKU 生成。
订单我用了 Order 和 OrderItem。Order 存订单号、用户、总金额、运费、订单状态、收货信息快照、支付时间;OrderItem 存每个商品条目。为什么要拆两张表?因为一个订单里往往有多个商品,如果全塞进一张表里,查询订单列表时要合并多条记录,订单状态还得在每条上重复维护,完全不合理。
订单状态我用 IntegerField 而不是 CharField。用数字存状态再配一层 Choices 类,代码里写 Order.STATUS_PENDING、Order.STATUS_PAID 这种语义化常量,比直接存“待支付”字符串更安全,改状态文案不用动数据库,查询也更快。
2.4 库存扣减:数据库锁与事务的第一次实战
库存是购物系统里最容易出并发问题的地方。我的方案是使用数据库的“乐观锁 + 事务重试”:
from django.db import transaction from django.db.models import F @transaction.atomic def deduct_stock(sku_id, quantity): sku = ProductSku.objects.select_for_update().get(id=sku_id) if sku.stock < quantity: raise ValueError("库存不足") sku.stock = F('stock') - quantity sku.save(update_fields=['stock'])这里关键点是select_for_update()和F()。select_for_update对行加锁,防止两个请求同时读到同一个库存值;F在数据库层面完成减法,避免先读后写的竞态。这两个配合下来,秒杀场景小流量实测不会出现超卖。
用事务包裹还有一个目的:如果后续创建订单失败,库存扣减要自动回滚。一旦把“扣库存”和“生成订单”写进同一个事务,数据库保证要么全成功,要么全失败。我不会一开始就引入 Redis 分布式锁,因为单体 django 加数据库行锁已经完全够用,锁的复杂度留给真正有高并发需求时再加。
2.5 关于表关系的一个血泪经验
不要轻易在模型里用外键的on_delete=models.CASCADE。用户删了账号就顺着删掉所有订单,听起来合理,但订单里还关联着支付记录、物流信息,这些属于不可丢失的审计数据。我全部改成了PROTECT或SET_NULL并保留字段内容,线上环境很少物理删除记录,项目里更多是软删除,加一个is_active或者deleted_at字段就好。新人最容易忽略这一点,等到要和支付对账的时候才会意识到订单数据多重要。
3. Django 后端接口开发:登录鉴权、商品浏览、下单支付的完整链路
3.1 DRF + SimpleJWT 实现登录鉴权
购物系统里用户要维护购物车、下单、查看订单,每个接口都必须知道“当前是谁”。我采用 JWT 方案,配合 django-rest-framework-simplejwt 这个库。
JWT 的设计是服务端不保存 Session,而是签发一个包含用户 ID 和过期时间的 Token 给前端,前端每次请求都带上。它适合前后端分离的项目,小程序端和 H5 端都能轻松存储和携带。
配置起来很舒服:
# settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), } from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), }小程序端登录调/api/token/接口换取 Token,之后所有需要用户身份的接口在 Header 里带Authorization: Bearer <token>。我额外写了一个登录注册一体接口:手机号加验证码登录,如果用户不存在就先创建再返回 Token。用 django 的信号机制,在用户创建的时候自动生成 UserProfile,省去手动初始化的麻烦。
3.2 商品列表与详情:缓存、过滤、序列化
商品列表是高频接口,我直接用 DRF 的ListAPIView加django-filter做分类筛选、价格区间、关键字搜索。第一次做的时候我把所有商品字段都序列化出去,前端拿到一大坨 JSON,小程序端渲染倒是没压力,但流量白费了。后来优化成列表接口只返回 id、标题、主图、最低价格、销量,详情接口再返回完整字段。
class ProductListView(generics.ListAPIView): serializer_class = ProductListSerializer filter_backends = [DjangoFilterBackend, OrderingFilter] filterset_fields = ['category', 'is_on_sale'] ordering_fields = ['created_at', 'sales_count']商品详情里要注意“规格联动”:前端选了“红色”和“M 码”,要能查出对应 SKU 的价格和库存。我在商品详情接口里把skus嵌套返回,每个 SKU 自带规格选项值和价格库存,前端本地做矩阵匹配。接口设计上,商品详情只返回 SKU 列表,规格的联动计算放在前端,这样后端不用处理复杂的规格组合查询,压力小很多。
由于商品数据变化频率低,我在详情接口上加了 django 的缓存装饰器,缓存 5 分钟。商品修改后主动清除缓存,避免用户看到旧价格。
3.3 下单接口:事务、库存、订单状态机
下单是这系统里最复杂的接口。我把它拆成几个步骤:
- 接收前端传来的地址 ID 和商品条目列表(SKU ID + 数量)
- 校验地址归属当前用户
- 遍历商品条目,查询 SKU,校验库存
- 计算总金额(从服务端取价格,绝不相信前端传的价格)
- 生成订单号和 Order/OrderItem 记录
- 扣减库存
- 返回订单 ID 和应付金额
所有步骤放在一个transaction.atomic()块里。
@transaction.atomic def create_order(user, address_id, items): # 校验地址 address = UserAddress.objects.select_for_update().get(id=address_id, user=user) # 计算金额并校验库存 total_amount = Decimal('0.00') order_items = [] for item in items: sku = ProductSku.objects.select_for_update().get(id=item['sku_id']) if sku.stock < item['quantity']: raise ValueError('商品库存不足') amount = sku.price * item['quantity'] total_amount += amount order_items.append(OrderItem(sku=sku, price=sku.price, quantity=item['quantity'])) # 生成订单 order = Order.objects.create( user=user, address_snapshot=format_address(address), total_amount=total_amount, status=Order.STATUS_PENDING, order_no=generate_order_no() ) OrderItem.objects.bulk_create(order_items) # 扣库存 for item in order_items: ProductSku.objects.filter(id=item.sku_id).update(stock=F('stock') - item.quantity) return order订单号生成我用的方案是时间戳 + 用户ID + 随机数,直接用 datetime 的微秒加随机字符串。这里不搞雪花算法,因为单机应用不需要全局唯一分布式 ID。微秒级时间戳同一个人同一秒下两单的概率几乎为零,再用随机数兜底,足够保证唯一性。
状态机上,订单从待支付到已支付再到已发货、已完成,我把状态流转方法封装在 Order 模型里:
class Order(models.Model): def mark_paid(self): if self.status != self.STATUS_PENDING: raise ValueError('订单状态不允许支付') self.status = self.STATUS_PAID self.paid_at = timezone.now() self.save(update_fields=['status', 'paid_at'])这样所有状态变更都走模型方法,不会出现业务代码里乱改状态的问题。
3.4 支付模块的对接与模拟支付
真实对接微信支付需要商户号、证书、回调域名一堆东西,课程设计和个人项目我推荐先做“模拟支付”。模拟支付的意思是:前端调起支付页,点击“确认支付”后,后端直接把这个订单标记为已支付,同时记录一条支付流水。这样整个购物流程可以完整跑通,演示、答辩、验收都不会卡在支付环节。
class MockPayView(APIView): def post(self, request, order_id): order = Order.objects.get(id=order_id, user=request.user) # 校验金额:前端可以传金额,但必须以服务端计算为准 if order.status != Order.STATUS_PENDING: return Response({'detail': '订单状态异常'}, status=400) Payment.objects.create( order=order, amount=order.total_amount, channel='mock', transaction_id=generate_transaction_id() ) order.mark_paid() return Response({'detail': '支付成功'})支付记录单独一张表,哪怕以后接真实支付,也只需要在这个 mock 接口里替换成调用官方 SDK,逻辑不动。这里的设计思想是:把“支付行为本身”和“支付渠道”解耦,让系统核心不依赖某一家第三方。
4. uni-app 前端工程:从商城首页到支付页,一套代码两套端
4.1 项目初始化与目录结构
uni-app 官方推荐用 HBuilderX 创建项目,也可以用 CLI 方式。我用的 CLI 方式,因为可以方便地用 VS Code 写代码,Git 管理也更顺手。
创建命令:
npx degit dcloudio/uni-preset-vue#vite my-mall项目结构里我把 pages 拆成:首页、分类、购物车、订单确认、支付、用户中心、订单列表、商品详情。tabBar 只有四个页面:首页、分类、购物车、我的。其他页面通过uni.navigateTo跳转,不占 tabBar 位置。
静态资源方面,图片直接放 static 目录,接口地址写在config.js里统一管理。开发环境用本地局域网地址,小程序端调试时要在微信开发者工具里关闭“不校验合法域名”,否则请求会被拦截。这个坑我每次都要提醒自己,特别是换电脑或者换网络之后。
4.2 请求封装与登录态管理
小程序端拿 Token 通常存在 storage 里,H5 端我喜欢用本地 localStorage。uni-app 提供了uni.setStorageSync和uni.getStorageSync,跨端统一,不用区分平台。
我封装了一个request.js:
const BASE_URL = 'http://192.168.1.100:8000/api' export function request(path, options = {}) { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') uni.request({ url: BASE_URL + path, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success: (res) => { // 401 表示 Token 过期,跳转登录 if (res.statusCode === 401) { uni.navigateTo({ url: '/pages/login/login' }) reject(res) } else { resolve(res.data) } }, fail: reject }) }) }这里注意一个问题:uni.request拿到的 res 是整个响应对象,业务代码需要的是res.data。我在封装层直接把 data 解析出来返回,页面里调用时就不用再套一层了。拦截 401 是必须的一步,否则 Token 过期的时候接口报错表现得很奇怪,用户完全不知道发生了什么。
4.3 商城首页与商品列表页的实现
商城首页我用的是“轮播图 + 金刚区图标 + 商品瀑布流”的常见布局。轮播图和后端接口绑定,运营可以在后台配置。金刚区图标写死在前端,跳转到对应分类页。商品瀑布流用scroll-view+ 左右两列实现,没有用uni-list,因为两列布局更接近主流电商 App 的视觉。
首页的数据加载我拆了两个接口:/api/home/banners/返回轮播图,/api/products/?is_on_sale=true&ordering=-sales_count返回热销商品。页面 onLoad 里用Promise.all并行请求,减少白屏等待时间。
商品详情页有个容易忽略的细节:SKU 弹出层。用户在详情页选择规格后会看到实时价格和库存,这个交互我用 popup 弹层实现。每个 SKU 选项做成本地二维数组,通过判断规格组合能否匹配到具体 SKU 来决定该选项是否可点。
// 规格组合匹配:options 是用户选中的规格集合 findSku() { return this.skus.find(sku => { return this.options.every((value, index) => sku.specs[index] === value) }) }这个函数看似简单,但它是整个 SKU 联动交互的核心。新增规格维度的时候,只需保证 sku.specs 的顺序和 options 一致,不需要改逻辑。
4.4 购物车页面:单选全选、数量加减、金额计算
购物车最核心的是金额联动计算。我维护一个selectedItems数组,用户点击单选或者全选的时候重新计算总金额。金额计算放在前端,但下单接口提交时后端会重新校验,前端金额只用来展示。
购物车页面数量加减要防抖,防止用户狂点加减的时候发出大量网络请求。我用的方案是:本地先更新数量,等用户停止操作 500 毫秒后统一提交后端。这比每点一次就请求一次体验好很多,也减轻了后端压力。
4.5 订单确认页与支付页的联动
订单确认页做三件事:展示商品列表、选择收货地址、显示订单总金额。页面从购物车进入时,把选中的购物车条目通过参数传过来;从商品详情“立即购买”进入时,则传单个 SKU 和数量。
支付页面更简单,订单创建成功后跳转过来,显示订单号、应付金额,一个“确认支付”按钮。点击支付后请求 mock 支付接口,成功后把订单状态改成已支付并跳转订单详情。这里我额外做了一个倒计时显示,“如果 15 分钟内不支付订单会自动关闭”,配合后端的定时关单任务,模拟真实电商的支付超时机制。
5. 购物车到订单的数据流转:前端几个容易出问题的细节
5.1 商品数据快照:为什么订单不能只存 ID
我在前面的数据库设计里提过快照,这里再从接口联调的角度讲一次。前端把购物车的商品条目提交到后端后,后端生成订单明细,OrderItem 里存的是下单那一刻的标题、图片 URL、单价。如果用户在下单后半小时再看订单,商家改了价格,订单详情仍然显示旧价格,这才是正常行为。如果订单关联的是实时商品表,价格就跟着变了,订单和支付金额对不上,这就是线上事故。
前端下单时传什么?只用传 SKU ID 和数量。其他一切数据后端自己查,价格也由后端计算。千万不要前端算好总价传过来,那样用户可以篡改金额。有一次我在测试接口的时候用抓包工具改了请求体重的大字段,后端直接存了篡改后的金额,这让我意识到服务端金额校验有多重要。
5.2 购物车勾选状态与下单条目的映射
购物车页面的勾选状态,我存在checkMap里,key 是 CartItem ID,value 是布尔值。提交订单的时候,先从后端确认这些 CartItem 属于当前用户,再组装下单参数。为什么不直接提交购物车全部内容?因为用户勾选了哪几项是明确的意图,直接全提交会把没选中的也买了。
这样一个用户同时只能有一个 Cart 实例,CartItem 通过 cart 关联。前端使用“当前用户 + 当前商品 SKU”来定位购物车里是否已有同款,已有则数量累加,没有则新建。这个逻辑前后端各做一次,前端做了体验好,后端做了兜底防恶意调用。
5.3 地址选择和快照的时序问题
用户在订单确认页选择地址后,后端创建订单时会把地址信息复制一份到 Order.address_snapshot。这里我用的是序列化后的 JSON 字符串,包含收件人、手机号、省市区、详细地址。为什么要快照?因为用户可能在下单后修改默认地址,但订单必须按下单时的地址发货。
这里引发的思考是:地址表本身要维护多地址,才是正常的。如果设计成“一个用户一个地址”,每次买的东西都发到同一个地方,体验太差了。所以我的 UserAddress 表:
- id
- user
- receiver
- phone
- province/city/district
- detail
- is_default
下单时接受 address_id,创建订单时读取对应记录生成快照。默认地址的切换逻辑也简单:先把该用户所有地址 is_default 设为 False,再把选中的那条设 True。
5.4 购物车在订单创建后的清理
下单成功后,购物车里对应条目要删除。我在后端 create_order 的接口里同步做了:订单创建成功后,根据 items 中的 sku_id 删除购物车条目。前端在跳转支付页前也主动调用一次轻量接口更新购物车数量角标,保持 UI 同步。
有同事问过为什么不在前端清理完购物车再调下单接口?因为如果前端删了但后端下单失败(比如库存不足),用户购物车里的商品就凭空消失了,这体验很糟。顺序必须是“下单成功 → 后端删购物车条目”,前端删除只是辅助更新。
6. 订单超时与状态流转:用 django 定时任务撑起“自动关单”
6.1 定时任务方案对比:Celery 还是 django-crontab
购物平台里有个常见需求:用户下单后 15 分钟未支付,订单自动关闭并释放库存。这在系统里是一个典型的“延迟任务”。主流方案有三种:
| 方案 | 特点 | 适配场景 |
|---|---|---|
| Celery + Redis | 功能强,支持定时/周期任务,但引入额外组件 | 项目流量大、任务复杂 |
| django-crontab | 轻量,基于系统 cron,适合简单周期任务 | 单机部署、任务简单 |
| 线程后台任务 | 简单粗暴,进程内执行 | 只做演示,不推荐生产 |
我的项目用 django-crontab 是最合适的。这类“设计实现”类项目没必要为了一个定时任务搭一套消息队列。django-crontab 的配置方式:
# settings.py CRONJOBS = [ ('*/1 * * * *', 'orders.cron.close_expired_orders', '>> /tmp/order_cron.log') ]注意,crontab 里每个 job 每分钟执行一次。命令执行的任务频率太低,订单超时状态就不准时;太高又浪费资源。我的close_expired_orders函数内部只查询“创建时间超过 15 分钟且状态为待支付”的订单,然后批量关闭并回滚库存。
6.2 自动关单与库存回滚的实现
from datetime import timedelta from django.utils import timezone from django.db.models import F def close_expired_orders(): expire_time = timezone.now() - timedelta(minutes=15) expired_orders = Order.objects.filter( status=Order.STATUS_PENDING, created_at__lt=expire_time ) for order in expired_orders: with transaction.atomic(): order.status = Order.STATUS_CLOSED order.save(update_fields=['status', 'updated_at']) # 回滚库存 for item in order.items.all(): ProductSku.objects.filter(id=item.sku_id).update(stock=F('stock') + item.quantity)这里回滚库存要注意用F()原子上加,不要先读后写。我在写这个函数的时候踩过一个坑:第一次直接用.stock += item.quantity,结果在高频关单时出现库存不准,因为多个进程同时读到同一个库存值。后来全部改成F表达式才稳定。
自动关单和用户手动取消的区别是:自动关单是系统行为,所以不需要用户操作;用户手动取消则要校验订单状态可是待支付,并且同样回滚库存。两个场景共用了同一个“关闭订单并回滚库存”的方法,避免逻辑重复。
6.3 支付回调与状态竞争
真实场景里,支付回调可能和自动关单同时发生。比如用户卡在支付页,超时任务先把它关了,然后支付回调到了。这时如果回调里不做状态检查,订单状态可能会被错误地改成已支付,但库存已经回滚了,钱货两清出问题。
我的处理是:支付回调先查询订单,再用状态机的mark_paid方法,这个方法内部检查状态是否待支付:
def mark_paid(self): if self.status != self.STATUS_PENDING: # 订单已关闭或已取消,不能支付 raise ValueError('订单状态不允许支付') self.status = self.STATUS_PAID self.paid_at = timezone.now() self.save(update_fields=['status', 'paid_at'])自动关单和支付回调的先后顺序由数据库行锁保证。select_for_update()锁行之后,另一个事务再查同一订单会等锁释放,然后发现状态不是待支付,直接拒绝。我在 mock 支付接口中本来就用了select_for_update(),实测并发的脏状态能被有效拦截。
7. 部署与踩坑实录:从小程序真机预览到云服务器上线
7.1 Django 部署:Nginx + uWSGI 的基本配置
本地开发用runserver很爽,但上线必须换 WSGI 服务器。我用的组合是 Nginx + uWSGI,这也是 django 生态里最经典的部署方式。
uWSGI 的配置文件:
[uwsgi] chdir = /data/www/mall module = mall.wsgi:application master = true processes = 2 threads = 2 socket = 127.0.0.1:8000 vacuum = true harakiri = 60 max-requests = 5000Nginx 配置要注意两个点:静态文件路径和 API 反代。
server { listen 80; server_name your_domain; location /static/ { alias /data/www/mall/static/; } location /media/ { alias /data/www/mall/media/; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }上传的商品图片存在 media 目录,跑collectstatic后静态资源由 Nginx 直接服务,API 请求才转发给 uWSGI。这样动态和静态分离,压力都在可接受范围。
7.2 跨域问题与小程序真机预览的两个差异化配置
H5 端在浏览器里访问 django 接口会碰到跨域,解决方案是django-cors-headers:
INSTALLED_APPS = [ 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ] CORS_ALLOW_ALL_ORIGINS = True # 开发环境先全部放行生产环境我会把CORS_ALLOW_ALL_ORIGINS关掉,替换成CORS_ALLOWED_ORIGINS白名单列表。小程序端不存在跨域概念,因为它不是浏览器环境,但它在真机预览时必须用局域网 IP 或域名访问接口,不能用localhost。我第一次真机调试时小程序请求一直失败,就是因为代码里写死了http://localhost:8000,在手机上 localhost 指向手机自己,不是电脑。
开发接口地址我做成环境变量,从config.js中读取:
const API_BASE = process.env.NODE_ENV === 'development' ? 'http://192.168.1.100:8000/api' : 'https://your_domain/api'7.3 部署之后的性能优化清单
上线之后我看了一下接口耗时,发现几个地方值得优化:
- 商品列表接口加缓存,QPS 提升立竿见影
- 首页轮播图走静态 JSON 配置,不走数据库查询
- 图片压缩:上传时统一缩到合适宽度,体积直接小一半
- 数据库索引:订单表按 user + status 建联合索引,查询订单列表快很多
class Order(models.Model): class Meta: indexes = [ models.Index(fields=['user', 'status']), ]还有一个容易被忽视的点:n+1 查询问题。商品列表序列化时每件商品都要查分类名、封面图 URL,如果不优化会产生大量 SQL。用select_related('category')或者prefetch_related('skus')一次性查出来,接口耗时从几百毫秒降到几十毫秒。
8. 从“能跑”到“能交付”:代码组织、后台管理与验收文档
很多人的期末项目停在“能跑”的程度,演示的时候各种愣神。把项目从“能跑”到“能稳定演示、能交付答辩”,需要做三件事:完善 admin 后台、补充初始数据、整理接口文档。
8.1 用 django admin 做运营后台
django admin 是这个框架最吸引人的地方之一。我注册了所有核心模型,并配置了 list_display、list_filter、search_fields,让运营人员可以直接在后台管理商品、订单、用户。商品上下架直接在后台改 is_on_sale 字段,订单状态变更也在后台操作。
Admin 里我为订单加了自定义 action,比如选中多个订单批量导出 CSV。这在演示时很加分,证明系统有运营支撑能力而不只是前端页面。
8.2 造数据与初始化脚本
项目验收要演示商品列表、搜索、分类筛选,空数据库没法展示。我写了一个init_data.py脚本,用 faker 库生成几十个商品、几百个 SKU、若干测试用户和订单。跑一次脚本数据库就有真实感,领导和评委浏览页面时不会觉得是空壳。
# management/commands/init_data.py from django.core.management.base import BaseCommand from faker import Faker class Command(BaseCommand): help = '初始化演示数据' def handle(self, *args, **options): fake = Faker('zh_CN') # 生成分类 # 生成商品和 SKU # 生成测试用户 self.stdout.write(self.style.SUCCESS('初始化完成'))管理命令被我跑了很多次,每次跑完还要清理旧数据,所以我又加了一个“清空并重建”的选项,方便反复演示。
8.3 接口文档:drf-spectacular 自动生成 OpenAPI
给接口写文档很痛苦,但 drf-spectacular 可以根据 DRF 代码自动生成 OpenAPI 文档,还能带出一个 Swagger UI 页面。配置好之后,打开/api/docs/就能看到所有接口的参数和返回结构。答辩时把这个页面一展示,技术上明显比纯口头描述有说服力。
{动手写一个真实项目的最大收获,不是学会了某个框架的某个 API,而是理解了数据在整个系统里如何流转:从用户点击购买,到订单落库,到库存扣减,到支付状态回调,每一步都有一致性和可靠性的考量。这套购物平台做完之后,我再去看电商开源项目,很多设计一看就明白了,因为底层关心的问题就那么多。}
写在最后给我自己的总结其实很简单:先把 MySQL 表设计清楚,再把交易链路的状态机定义明白,最后用 django admin 把运营后台捡起来,一个购物系统就能支撑完整演示了。uni-app 这部分更像“体力活”,把页面忠实还原给用户即可,真正花时间的地方全在后端的数据一致性和边界处理。希望这篇分享对正在折腾同类型项目的朋友有用。