恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Django的特产销售平台开发:电商系统设计与实现
首页
资讯中心
/
基于Django的特产销售平台开发:电商系统设计与实现
基于Django的特产销售平台开发:电商系统设计与实现
发布时间:2026/10/12 9:59:23
做特产销售平台这个项目本质上是在解决一个很现实的行业问题很多地方的优质农产品、手工艺品品质好但销路窄线下档口只能辐射周边几公里线上又不知道从哪下手。我花了大约三周时间完整做了一套基于 Python 和 Django 的特产销售平台前台购物、后台管理、订单流转、数据统计全部跑通。这个项目很适合拿来当 Web 开发入门到进阶的练手项目也适合真有小团队想做特产电商的人直接参考技术栈不复杂业务逻辑却足够完整该踩的坑一个都不少。先说一下这套系统最终做出来的样子访客可以按分类浏览特产、搜索商品、查看详情、加入购物车、下单购买下单后能看到订单状态实时变化商户端或者说后台管理员可以维护商品信息、调整库存、处理订单发货还能看到简单的销售统计。前后台共用一套 Django 项目通过权限控制区分访问范围。下面我从设计思路开始把每一块细节和实现过程全部拆开讲包括代码结构和那些文档里不会写的经验。1. 项目定位与整体设计思路1.1 特产销售的核心痛点做特产销售平台首先要搞清楚一件事它和普通电商平台不一样在哪。普通标品电商比如卖手机壳、卖标准件供应链稳定、规格统一、物流成本低而特产类目的特点非常鲜明非标品多。一份农家腊肉可能这个批次偏咸、下个批次偏淡无法像工业品那样精准描述规格参数。地域性强。很多特产离开原产地就失去意义消费者买的就是原产地直发这个信任感。商家规模小。真正的特产供给端大多是农户、小作坊、合作社他们没有技术团队甚至没有专职运营。时效性敏感。生鲜类特产对物流和库存准确度的要求远高于普通商品。这些痛点直接决定了系统设计方向前台必须突出产地信息和信任背书后台必须足够简单好用商品和库存管理必须支持灵活的规格设置。这也是我最终确定功能清单的依据而不是照搬一个通用电商模板。1.2 为什么选 Django 而不是 Flask 或 Node.js选型这个问题我在动手前认真对比过。如果是做纯 API 后端Flask 确实更轻便配合数据库扩展也能满足需求。但特产销售平台本质上是一个业务逻辑重、后台管理需求强、数据关系复杂的系统Django 的几个特性几乎是为这个场景量身定做的自带 Admin 后台。开发初期还没写前端页面前Admin 就能直接管理商品和订单省掉一大半后台开发时间。内置用户认证体系。登录、登出、密码哈希、权限分组都是现成的电商系统必须的注册登录功能一个小时就能搭好。ORM 能力扎实。商品、订单、用户之间的多表关联查询Django ORM 的查询表达式能写得很干净比手写 SQL 拼接容易维护太多。安全问题考虑周全。CSRF 防护、SQL 注入防护、XSS 转义都属于默认配置对没有专职安全的团队来说非常友好。Node.js 我也考虑过Express 生态确实灵活但做这种强管理后台的系统框架约束少反而不是优点什么都要自己选型、自己组装开发周期肉眼可见地拉长。Django 的约定优于配置在这里是加分项。1.3 分层架构与数据流设计整个项目采用 Django 标准的 MVT 架构但内部做了模块化拆分。项目根目录下按功能域拆成四个 appusers用户注册、登录、个人资料、收货地址管理。products商品分类、商品信息、商品图片、库存管理。orders购物车、订单生成、订单状态流转、支付回调处理。stats销售数据统计。这么拆的核心原因是避免单 app 过大。很多初学者喜欢把所有 models 写在一个 app 里项目到后期 models 文件几千行改一个字段牵一发动全身。分 app 之后每个模块的边界很清晰products 不关心订单怎么流转orders 只通过外键引用 products 的模型模块间的耦合度降到了最低。数据流方面用户的前台操作会产生三条核心数据链路浏览记录可选的冷数据、购物车临时数据、订单核心数据。购物车数据我选择存在 session 里而不是数据库理由是购物车的生命周期短、临时性强、不需要跨设备同步存 session 可以显著减轻数据库压力也少写好几张表和一堆增删改查接口。订单数据则必须入库而且要保证强一致这是整个系统的数据核心。2. 核心功能模块与数据库设计2.1 功能清单不是越多越好我在规划功能的时候刻意砍掉了一些看起来酷炫但对初期落地不实用的功能比如直播带货、社区种草、复杂会员等级体系。最终保留的功能以能跑通交易闭环为准前台商品列表按分类筛选关键词搜索、商品详情、购物车、结算下单、订单列表与详情、个人中心收货地址管理。后台仪表盘销售概览、商品管理增删改查上下架、订单管理发货、备注、分类管理、用户管理。公共注册登录、图片上传、分页、操作日志。这个功能清单的商业逻辑是先保证一单生意能顺利做完再考虑营销和增长。特产销售平台的第一版如果连下单、支付、发货都做不到流程顺畅加再多社交功能也没用。如果你自己也在规划类似项目我建议也用同样的标准去砍功能。2.2 数据库表结构的关键设计决策数据库设计是整个项目中我花心思最多的地方几张核心表的字段定义直接影响后续编码的复杂程度。先展示核心模型代码再逐个讲解为什么这么设计# products/models.py from django.db import models class Category(models.Model): name models.CharField(分类名称, max_length50) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name父级分类) sort_order models.IntegerField(排序, default0) class Meta: ordering [sort_order, id] class Product(models.Model): STATUS_CHOICES ( (0, 下架), (1, 上架), ) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name所属分类) name models.CharField(商品名称, max_length200) subtitle models.CharField(副标题, max_length255, blankTrue) cover_image models.ImageField(封面图, upload_toproducts/%Y/%m/) price models.DecimalField(销售价, max_digits10, decimal_places2) origin_price models.DecimalField(原价, max_digits10, decimal_places2) stock models.IntegerField(库存, default0) sales_count models.IntegerField(销量, default0) origin_place models.CharField(产地, max_length100) detail models.TextField(商品详情) status models.SmallIntegerField(状态, choicesSTATUS_CHOICES, default0) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: ordering [-created_at] indexes [ models.Index(fields[status, category]), ] def __str__(self): return self.name几个关键决策点金额字段用 DecimalField 不用 FloatField。这是电商系统的一条铁律。浮点数在计算机里是二进制近似存储0.1 0.2 不等于 0.3涉及钱的运算一旦出现精度误差就是事故。DecimalField 底层是定点数运算精度可控。我见过不止一个项目因为用 FloatField 存金额最后对账差几分钱查了一整天。删除策略用 PROTECT 不用 CASCADE。外键关联的商品分类如果商品表里还有商品引用了它CASCADE 会把商品连带删掉这种删一个分类导致所有商品消失的操作在生产环境是灾难。PROTECT 会在有引用时阻止删除并抛出异常代码里捕获异常提示用户该分类下有商品无法删除这个设计倒逼着系统必须保留操作完整性。冗余设计Product 里的 sales_count 销量字段是冗余的它完全可以通过订单表聚合查询得出。但我还是保留了这个字段道理和所有电商系统一样——列表页排序时如果实时去聚合订单表一个几千单的商品就能把查询拖慢几百毫秒。用空间换时间在这个场景下完全值得。# orders/models.py class Order(models.Model): STATUS_CHOICES ( (0, 待付款), (1, 待发货), (2, 待收货), (3, 已完成), (4, 已取消), ) order_no models.CharField(订单号, max_length32, uniqueTrue) user models.ForeignKey(users.UserProfile, on_deletemodels.PROTECT, verbose_name下单用户) total_amount models.DecimalField(订单总额, max_digits10, decimal_places2) receiver_name models.CharField(收货人, max_length50) receiver_phone models.CharField(联系电话, max_length20) receiver_address models.CharField(收货地址, max_length255) status models.SmallIntegerField(订单状态, choicesSTATUS_CHOICES, default0) remark models.CharField(订单备注, max_length255, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) paid_at models.DateTimeField(支付时间, nullTrue, blankTrue) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems, verbose_name所属订单) product models.ForeignKey(products.Product, on_deletemodels.PROTECT, verbose_name商品) product_name models.CharField(商品名称快照, max_length200) product_image models.CharField(商品图片快照, max_length255) price models.DecimalField(成交单价, max_digits10, decimal_places2) quantity models.IntegerField(购买数量, default1)订单主表 订单明细表的拆分是标准设计但有两处细节值得说订单里冗余了 product_name 和 product_image。这个字段看起来是无用功实际却是必须的。如果订单表只存一个 product 外键商家后续修改了商品名称或换了图片历史订单里显示的就会是修改后的内容。给用户打印订单详情时他买的时候叫高山云雾茶现在商品改名叫精品云雾茶用户体验就很奇怪。所以订单明细必须做数据快照把下单时刻的商品信息固化成独立字段。订单号要唯一且非自增。自增 ID 会暴露平台的订单量而且多表联查时容易混淆。我用的生成规则是时间戳14位 用户ID后四位 随机数最终得到一个 32 位以内的唯一字符串。同时给 order_no 加了 unique 约束数据库层面兜底防止并发下订单号重复。2.3 用户模型扩展Django 自带的 User 模型能覆盖登录认证需求但电商系统还需要手机号、收货地址这些额外资料。我采用的方式是新建一个 UserProfile 模型通过 OneToOneField 关联内置 User# users/models.py from django.contrib.auth.models import User class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) phone models.CharField(手机号, max_length20, blankTrue) avatar models.ImageField(头像, upload_toavatars/, blankTrue) class Address(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameaddresses, verbose_name所属用户) receiver_name models.CharField(收货人, max_length50) receiver_phone models.CharField(联系电话, max_length20) province models.CharField(省份, max_length30) city models.CharField(城市, max_length30) district models.CharField(区县, max_length30) detail models.CharField(详细地址, max_length255) is_default models.BooleanField(默认地址, defaultFalse)为什么不直接改内置 User 模型加字段因为 Django 的 User 模型在首次 migrate 之后结构就基本冻结了强行加字段会带来非常痛苦的迁移兼容问题。用 OneToOne 扩展是官方推荐的做法既保留了内置认证的完整功能又让自定义字段独立管理。3. 核心业务逻辑实现购物车、订单与支付3.1 购物车的 Session 实现方案购物车我选用 Session 存储而不是数据库表核心代码非常简洁# orders/views.py from django.shortcuts import get_object_or_404 from products.models import Product CART_SESSION_KEY cart def get_cart(request): return request.session.get(CART_SESSION_KEY, {}) def add_to_cart(request, product_id): product get_object_or_404(Product, idproduct_id, status1) cart request.session.get(CART_SESSION_KEY, {}) key str(product.id) if key in cart: cart[key][quantity] 1 else: cart[key] { quantity: 1, price: str(product.price), name: product.name, image: product.cover_image.url if product.cover_image else , } request.session[CART_SESSION_KEY] cart request.session.modified True这里有两个坑必须提醒Session 里的价格要把 Decimal 转成字符串。Decimal 对象不是 JSON 可序列化类型直接存会报错。我存完才发现原来 Django 的 Session 默认用 JSON 序列化于是统一用str(product.price)转成字符串入 session取出时再Decimal(value)转回来。如果你在 session 里存 Decimal页面一刷新就会得到Object of type Decimal is not JSON serializable报错。修改 session 后必须手动标记modified True。Django 只有在request.session被直接赋值或键被删除时才会自动感知变化如果修改的是嵌套字典内部的值比如cart[key][quantity] 1框架不会认为 session 变化了导致刷新后购物车数量不变。第一次踩这个坑的时候排查了半小时。3.2 创建订单的事务处理从购物车生成订单是电商系统中最核心的写入操作涉及多张表变更生成订单主表、生成订单明细、扣库存、清空购物车。这些操作任何一个失败都不能留下半截数据所以我用数据库事务包裹整个操作from django.db import transaction transaction.atomic def create_order(request, address_id): cart get_cart(request) if not cart: raise ValueError(购物车为空) address get_object_or_404(Address, idaddress_id, userrequest.user) items [] total Decimal(0.00) for product_id, item in cart.items(): product Product.objects.select_for_update().get(idproduct_id) if product.stock item[quantity]: raise ValueError(f商品「{product.name}」库存不足) product.stock - item[quantity] product.sales_count item[quantity] product.save(update_fields[stock, sales_count]) line_total Decimal(item[price]) * item[quantity] total line_total items.append(OrderItem( productproduct, product_nameproduct.name, product_imageproduct.cover_image.url if product.cover_image else , priceDecimal(item[price]), quantityitem[quantity], )) order Order.objects.create( order_nogenerate_order_no(request.user.id), userrequest.user, total_amounttotal, receiver_nameaddress.receiver_name, receiver_phoneaddress.receiver_phone, receiver_addressf{address.province}{address.city}{address.district}{address.detail}, ) OrderItem.objects.bulk_create(items) request.session[CART_SESSION_KEY] {} request.session.modified True return orderselect_for_update()是并发防超卖的关键。它在数据库层面给选中的商品行加了行级锁事务提交之前其他事务无法修改这一行。这样两个用户同时下单购买同一款只剩 3 件的商品时第二个请求会在锁上等待拿到锁后重新读取库存发现不足抛出异常。没有这行代码的话两个请求同时读到 stock3各自扣成 2最终库存变成 1实际应卖 2 件卖出去 4 件这就是典型的超卖。bulk_create一次性插入明细。如果循环里一条一条OrderItem.objects.create()一个订单 5 件商品就产生 5 次数据库往返。用 bulk_create 一次搞定数据量大时性能差距非常明显。update_fields精准更新。save()默认会更新所有字段在一些场景下可能会覆盖掉别的请求刚提交的修改。明确指定update_fields[stock, sales_count]只更新这两个字段更安全也更快。3.3 订单状态机设计订单状态我用一个整数字段配合常量定义而不是枚举类型或者独立状态表。原因很简单这个项目的状态流转是单向线性的复杂度不高用权标字段配合代码里的状态检查足够了。def transition_order_status(order, target_status): 订单状态流转检查 allowed { Order.STATUS_PENDING_PAYMENT: [Order.STATUS_PAID, Order.STATUS_CANCELED], Order.STATUS_PAID: [Order.STATUS_SHIPPED], Order.STATUS_SHIPPED: [Order.STATUS_COMPLETED], } if target_status not in allowed.get(order.status, []): raise ValueError(f订单状态不能从 {order.status} 变为 {target_status}) order.status target_status if target_status Order.STATUS_PAID: order.paid_at timezone.now() order.save(update_fields[status, paid_at])状态机的核心价值是兜底非法操作。比如用户在待收货状态下点击取消订单正常逻辑里是不允许的如果只在按钮上做判断、后端不做校验用户手动构造请求就能绕过去。有了这个校验函数所有状态变更必须走这个入口非法状态迁移一律拒绝。3.4 支付回调的幂等处理支付接入用的是第三方支付沙箱环境模拟支付网关回调处理的代码结构要特别注意幂等性——支付平台可能因为网络原因重复发送回调通知如果每次回调都执行一次加余额、改状态操作用户会被重复发货。csrf_exempt def payment_callback(request): # 验签逻辑省略 order_no request.POST.get(order_no) trade_status request.POST.get(trade_status) if trade_status TRADE_SUCCESS: order Order.objects.select_for_update().get(order_noorder_no) if order.status Order.STATUS_PENDING_PAYMENT: order.status Order.STATUS_PAID order.paid_at timezone.now() order.save(update_fields[status, paid_at]) # 触发后续发货通知、短信通知等 return HttpResponse(success)关键逻辑就是先检查当前状态只有待付款状态才允许更新为已支付。重复回调进来时订单已经是已支付状态直接跳过更新操作返回 success 给支付平台告诉它我知道了别重试了。这种设计不依赖外部锁数据库行锁本身已经天然保证了并发安全。4. 实操过程开发环境与关键配置4.1 环境准备与项目初始化本地开发环境我推荐用 venv 创建虚拟环境Python 版本建议 3.10 或 3.11Django 使用 4.x 版本。初始化流程如下# 创建虚拟环境 python -m venv venv # 激活Windows/Linux venv\Scripts\activate # Windows source venv/bin/activate # Linux/macOS # 安装依赖 pip install django4.2 pillow # 创建项目 django-admin startproject tesan_site . # 创建 app python manage.py startapp products python manage.py startapp orders python manage.py startapp usersPillow 必须装Django 的 ImageField 字段依赖它处理图片。不装的话迁移命令执行到 ImageField 模型会直接报错。4.2 Settings 配置的几个关键项# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, users, products, orders, stats, ] # 自定义用户认证扩展告诉 Django 使用 profile 关联 LOGIN_URL /users/login/ LOGIN_REDIRECT_URL / # 上传文件配置 MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media # 时区 —— 必须用本地时区 TIME_ZONE Asia/Shanghai USE_TZ True时区是一个很容易忽略但影响巨大的配置。如果TIME_ZONE不设置Django 默认用 UTC 时间存储。用户在下午三点下单订单创建时间显示的是早上七点。更麻烦的是 Linux 服务器上定时任务如果用系统时区而 Django 用 UTC时间对不上排查起来非常头大。项目初期就配好Asia/Shanghai避免后面改时区导致存量数据时间偏差。4.3 Admin 后台的二次开发Django Admin 虽然是自动生成的但稍微配置一下就能变成一个相当好用的运营后台# products/admin.py from django.contrib import admin from .models import Category, Product admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display (name, parent, sort_order) ordering (sort_order,) admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display (name, category, price, stock, sales_count, status, created_at) list_filter (category, status) search_fields (name, subtitle) list_editable (price, stock, status) readonly_fields (sales_count, created_at, updated_at)list_editable这个配置特别实用运营人员在商品列表页就能直接改价格、改库存、上下架不需要点进编辑页面。列表页支持勾选多个商品批量修改效率比一个个点进去高一个数量级。4.4 数据库迁移与初始数据迁移命令是常规操作但有几个实践要点# 生成迁移文件 python manage.py makemigrations users products orders # 执行迁移 python manage.py migrate # 创建管理员 python manage.py createsuperuser # 导入初始分类数据 python manage.py shell -c from products.models import Category; Category.objects.create(name茶叶茗品); Category.objects.create(name山野干货); ...每次调整完模型字段一定要先makemigrations再migrate。很多新手直接migrate发现数据库表没变化原因就是少跑了第一步。makemigrations 负责生成迁移脚本相当于数据库变更的计划书migrate 才真正执行变更。5. 安全加固与性能优化实践5.1 Django 自带安全能力的正确用法Django 默认开启的安全特性不是摆设开发时必须理解它们的作用CSRF 防护。所有 POST 表单必须携带{% csrf_token %}否则请求会被拒绝。这是为了防止跨站请求伪造。我在开发中曾经图省事在 AJAX 请求里忘记带 CSRF Token调试了很长时间才发现 403 的原因。SQL 注入防护。Django ORM 的查询参数化是默认行为只要不写裸 SQL 字符串拼接基本不会给 SQL 注入机会。XSS 防护。模板系统默认转义 HTML 特殊字符用户在商品详情里输入的script标签会被当作纯文本输出而不是执行。权限控制方面后台视图必须加登录校验和权限校验我用的是 Django 自带的装饰器from django.contrib.admin.views.decorators import staff_member_required staff_member_required def dashboard(request): # 只有 staff 用户才能访问 pass5.2 性能优化三板斧实操中遇到的性能瓶颈主要集中在商品列表页和首页。做了三件事数据库查询优化。商品列表页展示商品封面图、分类名、销量等数据时最容易出现 N1 查询问题。用 Django Debug Toolbar 检测后我加了select_related(category)和only(id, name, price, ...)精准控制查询字段列表页的 SQL 从每页 20 条商品 21 次查询降到了 1 次。图片处理。商品详情页的大图要控制体积我在上传时做了压缩处理。Pillow 里直接转成 WebP 格式体积能减少 60% 以上同时不影响观感。这个优化很重要因为特产类目的商品图通常都是高清实物图一张图两三 MB 是很常见的。缓存。首页的商品分类导航基本不怎么变我用 Django 的缓存框架把它缓存一分钟用cache_page装饰器就能实现from django.views.decorators.cache import cache_page cache_page(60 * 1) def home(request): # 首页视图 pass5.3 部署方案的要点本地开发完成后部署到生产环境的配置和本地有几处重要差异。我用的是 Nginx Gunicorn SQLite小流量阶段方案后期量大了再平滑迁移 PostgreSQLserver { listen 80; server_name your-domain.com; location /static/ { alias /var/www/tesan_site/static/; } location /media/ { alias /var/www/tesan_site/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }静态文件 404 是部署阶段最常见的问题。本地开发时 Django 自动处理静态文件生产环境必须通过collectstatic命令把所有静态文件收集到统一目录再由 Nginx 直接服务。不执行python manage.py collectstatic页面 CSS 和 JS 全都会 404。6. 常见问题与排查技巧实录6.1 问题速查表问题现象可能原因解决方案迁移时报ValueError: ... ImageField ... Pillow未安装 Pillowpip install pillow后重新迁移POST 请求报 403 CSRF 验证失败表单缺少{% csrf_token %}表单内添加模板标签上传图片后页面打不开未配置 MEDIA_URL/MEDIA_ROOT检查 settings 配置和 Nginx 的 media location订单创建后库存不变或超卖未使用select_for_update()并发扣库存必须加行锁Session 购物车刷新即清空未调用request.session.modified True修改嵌套字典后手动标记Admin 后台登录后跳转循环LOGIN_URL 配置错误检查 settings 里的登录 URL时间字段显示 UTC 时间TIME_ZONE 未设置设置TIME_ZONE Asia/Shanghai列表页响应慢SQL 查询多N1 查询问题使用select_related/prefetch_related生产环境静态文件 404未执行collectstatic部署后运行 collectstatic 并配置 Nginx alias6.2 一次真实的排查过程库存超卖这个问题的排查过程值得写下来。我在本地测试时用两个浏览器窗口同时下单同一款商品设置库存为 1结果两个订单都创建成功了数据库里商品表 stock 变成了 -1。当时第一反应是下单逻辑有问题仔细检查后发现是create_order里读库存用的是普通get()没有加锁。两个并发事务同时读到 stock1各自认为库存充足各自扣减成 0然后都成功提交。等到都提交完数据库实际值变成了 -1。解决方法就是前文代码里的select_for_update()。加了这行之后两个并发请求的事务在同一行数据上会形成锁等待第二个请求拿到锁后重新读取库存发现已经是 0抛出了库存不足异常。事务回滚订单不会创建。这个案例也说明本地开发时的并发问题不测试是发现不了的上线前一定要做并发模拟测试。6.3 不能忽视的日志与调试技巧排查问题靠的不是猜是日志。settings 里我配了控制台输出和文件日志双通道LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: INFO, class: logging.FileHandler, filename: BASE_DIR / logs / app.log, }, }, loggers: { django: { handlers: [file], level: INFO, }, django.request: { handlers: [file], level: ERROR, }, }, }遇到奇怪问题时先开 Django Debug Toolbar 看 SQL 执行情况再查日志确认错误链路基本能定位九成以上问题。切忌上来就乱改代码改了半天发现方向错了。7. 测试策略与验收清单7.1 核心接口的自动化测试项目里我写了覆盖核心业务逻辑的自动化测试重点测订单流程的状态变更和权限控制。Django 的测试框架基于unittest写起来很直接# orders/tests.py from django.test import TestCase, Client from django.contrib.auth.models import User from products.models import Product, Category class OrderFlowTest(TestCase): def setUp(self): self.client Client() self.user User.objects.create_user(testuser, password123456) self.category Category.objects.create(name茶叶) self.product Product.objects.create( categoryself.category, name高山绿茶, price99.00, stock10, status1, ) self.client.login(usernametestuser, password123456) def test_create_order_deducts_stock(self): # 加入购物车 self.client.post(f/cart/add/{self.product.id}/) # 创建订单 response self.client.post(/orders/create/, {address_id: 1}) self.assertEqual(response.status_code, 200) # 验证库存扣减 self.product.refresh_from_db() self.assertEqual(self.product.stock, 9)自动化测试的价值不在于求个心理安慰而是改代码的时候能快速发现有没有改坏既有功能。我在后面加优惠券功能时跑一遍测试直接发现订单金额计算被影响几分钟就定位了问题。7.2 手工验收的功能清单自动化测试之外上线前一定要按用户视角过一遍完整流程注册新账号 → 邮箱/手机验证 → 完善资料 → 添加收货地址。搜索商品 → 查看详情 → 加入购物车 → 修改数量 → 结算下单。取消订单 → 商家发货 → 确认收货 → 完成订单。后台创建分类 → 添加商品 → 上传图片 → 调整库存 → 下架商品。清空浏览器缓存 → 模拟新用户访问全流程。这套手工验收流程走完后基本可以保证系统主流程没有致命问题。8. 从项目到产品扩展方向与经验总结8.1 下一步可以加什么系统跑通之后按业务优先级排我给自己的扩展路线是优惠券与营销工具。满减券是特产销售非常常见的运营手段尤其是节日礼盒场景。多商户支持。现在的后台是单商家模式如果要做平台化需要引入商户表、商品归属、商户结算逻辑改动量不小建议一开始就留好商户字段的扩展位。评价体系。下单确认收货后允许用户评价商品详情页展示评价列表对信任导向的特产类目极其重要。物流对接。接入第三方物流查询 API用户可以在订单详情页看快递轨迹能显著减少客服咨询量。消息通知。订单状态变更时通过站内信或邮件通知用户降低用户焦虑感。8.2 我在这个项目里体会最深的三件事第一件电商系统的核心不在页面多华丽而在于数据一致性和流程闭环。库存不能负、订单不能乱、金额不能错这三条守住了页面朴素一点都无所谓。反而是很多花里胡哨的页面效果在真实业务里几乎没人看。第二件Django 的约束是保护而不是限制。刚开始写的时候觉得它规定太多用熟了发现这些约定全是在帮你规避生产环境的问题。比如 admin 默认要求强密码、默认开启 CSRF 防护这些都是已经替你踩过坑得出的最佳实践。第三件测试非常值得。我最初觉得先写完再说测试以后补结果后期每次加功能都提心吊胆生怕改崩了某个隐藏逻辑。补齐了自动化测试之后改代码的胆子大了很多因为跑一下就知道有没有破坏既有功能。如果你从这个项目里只带走一个习惯我希望是写测试这个习惯。