Django+uni-app实战:构建完整电商购物系统与部署方案
1. 为一个“购物系统”做技术选型我为什么最后落在了 django uni-app我陆续做过几个信息管理类系统之后决定挑战一个交易链路完整的在线购物平台。市面上现成商城源码很多但大多绑定特定云厂商或者前端框架改起来束手束脚。这次我给自己定的目标是前后端完全分离后端能扛住真实订单流转前端一套代码同时覆盖小程序和移动端浏览器。折腾一圈最终方案锁定为 django uni-app。先说结论这套组合尤其适合“需要一个能完整演示、能真实上线、能继续扩展”的课程设计或个人项目。如果你也在犹豫技术栈先别急着抄代码我把我选型时怎么想的讲清楚。选 django 的故事很简单Python 生态里django 自带 Admin 后台、ORM、迁移机制和认证体系加上 DRFDjango REST Framework之后写接口非常顺手。购物系统比普通 CRUD 复杂在订单、库存、支付这三件事的联动django 的 ORM 支持事务和锁能在这一层保证数据一致性。对比过 flask 和 fastapiflask 过于自由所有模块都要自己拼fastapi 异步性能确实强但配套生态和后台管理要额外搭。我的场景是“系统设计与实现”的完整交付不是纯接口压测django 的“全家桶”属性帮助最大。选 uni-app 的理由更实际。我要同时交付微信小程序端和 H5 端如果两端分开写前端工作量直接翻倍。uni-app 基于 Vue 语法一套代码编译到多端虽然做不到 100% 零差异但页面结构、业务逻辑、状态管理都能复用。社区里关于商城类项目的踩坑记录也多真遇到问题不至于两眼一抹黑。这里也对比过 Taro它基于 React本身也很好但我更熟 Vue 生态从熟悉度出发选了 uni-app。技术方向定了之后我先把整体架构画在纸上django 提供 RESTful APIuni-app 负责展示和交互两者通过 HTTP JSON 通信登录态用 Token 维持订单和支付走单独的服务逻辑模块。没有引入消息队列也没有拆微服务单体架构足够支撑中小流量同时把代码结构拆干净方便后续换成微服务也不至于重写。这一节末尾我想强调一件事选型别追新。项目的本质是解决“购物平台怎么做”不是“什么技术最时髦”。django 在服务端开发里成熟了十几年uni-app 在多端开发里积累了大量的生产案例这两个工具足够把你从零带到上线。2. 数据库设计是购物系统的一半用户、商品、购物车、订单、库存的模型关系很多第一次做商城的人上来就写商品表和订单表结果联调时发现购物车和订单对不上、库存扣了但订单没生成。我的经验是数据库设计阶段花两个小时后面调试少花两天。2.1 用户体系不自己造轮子但必须扩展 Profiledjango 自带的 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(idsku_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_deletemodels.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(days1), REFRESH_TOKEN_LIFETIME: timedelta(days7), }小程序端登录调/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(idaddress_id, useruser) # 计算金额并校验库存 total_amount Decimal(0.00) order_items [] for item in items: sku ProductSku.objects.select_for_update().get(iditem[sku_id]) if sku.stock item[quantity]: raise ValueError(商品库存不足) amount sku.price * item[quantity] total_amount amount order_items.append(OrderItem(skusku, pricesku.price, quantityitem[quantity])) # 生成订单 order Order.objects.create( useruser, address_snapshotformat_address(address), total_amounttotal_amount, statusOrder.STATUS_PENDING, order_nogenerate_order_no() ) OrderItem.objects.bulk_create(order_items) # 扣库存 for item in order_items: ProductSku.objects.filter(iditem.sku_id).update(stockF(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(idorder_id, userrequest.user) # 校验金额前端可以传金额但必须以服务端计算为准 if order.status ! Order.STATUS_PENDING: return Response({detail: 订单状态异常}, status400) Payment.objects.create( orderorder, amountorder.total_amount, channelmock, transaction_idgenerate_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.jsconst 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_saletrueordering-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 IDvalue 是布尔值。提交订单的时候先从后端确认这些 CartItem 属于当前用户再组装下单参数。为什么不直接提交购物车全部内容因为用户勾选了哪几项是明确的意图直接全提交会把没选中的也买了。这样一个用户同时只能有一个 Cart 实例CartItem 通过 cart 关联。前端使用“当前用户 当前商品 SKU”来定位购物车里是否已有同款已有则数量累加没有则新建。这个逻辑前后端各做一次前端做了体验好后端做了兜底防恶意调用。5.3 地址选择和快照的时序问题用户在订单确认页选择地址后后端创建订单时会把地址信息复制一份到 Order.address_snapshot。这里我用的是序列化后的 JSON 字符串包含收件人、手机号、省市区、详细地址。为什么要快照因为用户可能在下单后修改默认地址但订单必须按下单时的地址发货。这里引发的思考是地址表本身要维护多地址才是正常的。如果设计成“一个用户一个地址”每次买的东西都发到同一个地方体验太差了。所以我的 UserAddress 表iduserreceiverphoneprovince/city/districtdetailis_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(minutes15) expired_orders Order.objects.filter( statusOrder.STATUS_PENDING, created_at__ltexpire_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(iditem.sku_id).update(stockF(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-headersINSTALLED_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/api7.3 部署之后的性能优化清单上线之后我看了一下接口耗时发现几个地方值得优化商品列表接口加缓存QPS 提升立竿见影首页轮播图走静态 JSON 配置不走数据库查询图片压缩上传时统一缩到合适宽度体积直接小一半数据库索引订单表按 user status 建联合索引查询订单列表快很多class Order(models.Model): class Meta: indexes [ models.Index(fields[user, status]), ]还有一个容易被忽视的点n1 查询问题。商品列表序列化时每件商品都要查分类名、封面图 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 这部分更像“体力活”把页面忠实还原给用户即可真正花时间的地方全在后端的数据一致性和边界处理。希望这篇分享对正在折腾同类型项目的朋友有用。