资讯动态

Django甜点电商系统开发实战:从需求建模到部署上线

发布时间:2026/9/28 15:56:26 来源:尧图企业网站定制
经常有人问我Django 练手项目到底做什么好。我的答案一直很固定做电商而且最好是垂直品类的小店。理由很简单——电商系统能把用户认证、商品展示、购物车、订单、后台管理这条完整链路串起来逻辑闭环又不会复杂到劝退新手。我前段时间刚完整做完一个基于 Python 的网上甜点店系统从需求梳理到部署上线都跑了一遍今天专门把整个设计思路、核心实现和踩过的坑复盘出来。这个系统表面上是卖甜点实际上它覆盖了一个小型电商项目会遇到的所有典型问题商品怎么分类展示、购物车怎么存、下单时库存怎么扣才不出错、订单状态怎么流转、管理后台怎么让老板快速改价格。适合想系统掌握 Django 开发流程的初学者也适合准备做课设、毕设或作品集项目的同学参考。1. 甜点店系统的需求是怎么一点点长出来的1.1 从卖蛋糕倒推功能清单做项目最忌讳一上来就写代码。我习惯先假装自己是店主把日常经营动作摊开再翻译成系统功能。店主每天要做什么上架新甜点、修改价格和库存、查看订单、处理售后。顾客要做什么逛店、看详情、选规格、加购物车、下单、付款、查订单状态。俩角色一合功能边界就清晰了。我最终拆出的核心功能有这几块前台商品展示首页展示新品和推荐分类页按甜点类型筛选商品详情页展示图片、价格、库存、描述、甜度等级。用户系统注册、登录、个人信息维护、我的订单列表。购物车添加商品、修改数量、删除、合计金额计算。下单流程填写收货信息姓名、电话、地址、选择配送时间段、提交订单、模拟支付更新订单状态。管理后台商品分类管理、商品管理、订单管理、用户管理。这套需求清单持续迭代了两个版本。第一版砍掉了配送时间段选择第二版发现甜点店和普通电商最大的差异在于配送时效——蛋糕不能过夜顾客对送达时间很敏感于是补了回去。1.2 用户端和管理端的分工逻辑这个项目的特殊性在于双端结构。用户端走的是服务端渲染的普通页面用 Django 模板直接输出 HTML管理端则直接基于 Django 自带的 admin 做二次开发。我见过很多新手把管理端单独做一个前端项目然后通过接口对接。那种做法不是不行但对这种规模的项目来说成本太高了。Django admin 提供了现成的增删改查界面我只需要通过 register 注册模型再配置 list_display、search_fields 这些属性就能得到一个勉强能用的后台。后面我会专门讲怎么把 admin 改得更好用。所谓管理端的第二个需求是数据可视化。店主不仅想看订单列表还想知道哪种甜点卖得好。我在 admin 里额外加了几个简单的统计视图后面会展开。1.3 一个容易被新手忽略的原型问题动手写代码之前我建议先把页面流转图画一遍。不是用复杂工具就是纸笔或 PPT 上画页面之间的跳转关系首页怎么进分类页、分类页怎么进详情页、详情页点了加入购物车之后是跳购物车还是留在原地、提交订单之后跳哪里。这一步能帮你避开一个常见的逻辑混乱进入购物车后清空购物车导致我的订单里看不到已购商品。我第一版就差点犯这个错——下单成功直接删除购物车记录后来仔细过原型才意识到购物车记录被清空没问题但订单明细必须单独落库。这就是为什么订单表不能简单存一个商品 ID 列表必须有独立的订单明细表。原型阶段多花两小时改代码阶段少花两天。2. 技术选型与数据库落地Django 给我的三个确定性2.1 为什么是 Django 而不是 Flask这个项目我在选型时认真对比过 Django 和 Flask。Flask 灵活、轻量但很多东西要自己搭ORM 要选 SQLAlchemy、表单要选 WTForms、迁移要选 Alembic、登录要选 Flask-Login。你把它们拼起来确实也能做但每个环节都要做决策对新手来说更容易迷茫。Django 的价值在于确定性。admin 后台是现成的ORM 和迁移是内置的表单处理有 Form 组件用户认证有 auth 应用。你不需要在技术选型上反复纠结可以把注意力全部放在业务逻辑上。说白了Flask 适合你清楚自己要什么的时候Django 适合你还不完全清楚但想把事做成的时候。甜点店系统这种标准业务流项目Django 就是最省力的选择。具体版本我用的 Django 4.2 Python 3.10。不建议用太老的 Django 版本一方面安全补丁跟不上另一方面新版对 Python 新特性支持更好。如果你本地是 Python 3.12 也没问题Django 4.2 以上都兼容。2.2 数据表设计把甜点店映射成模型数据库设计是整个系统里最值得花时间的事情。我最终设计了六张核心表下面是关键字段。用户表直接使用 Django 内置的 auth.User不额外建用户表。需要存手机号时用 OneToOneField 关联一个 Profile 扩展表。这种做法的好处是不会破坏 Django 自带的认证流程admin 里也能直接管理用户。分类表 Category字段只有 name、sort_order 两个核心业务字段sort_order 用来控制前台展示顺序。甜点表 Dessert这是整个系统的商品实体name商品名category外键关联分类price价格用 DecimalFieldstock库存IntegerFieldimage图片ImageFielddescription描述TextFieldsweetness甜度等级Choices微甜/标准/偏甜flavor_tags口味标签如巧克力、抹茶、芒果is_new、is_recommend用于控制首页展示created_at上架时间购物车表 CartItemuser 外键、dessert 外键、quantity 数量再加一个 Meta unique_together 约束保证同一用户同一商品只有一条记录。订单表 Orderuser 外键、total_amount总价、receiver_name、receiver_phone、receiver_address、delivery_time配送时间段、status 状态字段、created_at。订单明细表 OrderItemorder 外键、dessert 外键、price下单时的快照价格、quantity。这里有个非常关键的设计点OrderItem 里必须冗余一份 price。因为商品价格以后会变如果订单明细只关联 Dessert 表而价格实时去读 Dessert那三个月后你查半年前的订单价格可能已经变了。订单是历史数据必须把下单那一刻的价格固定下来。2.3 Django 默认 User 模型够用吗在没做任何迁移之前我先考虑了一个问题用户表要不要自定义。Django 自带的 User 已经有用户名、密码、邮箱、姓名、is_staff 这些字段对甜点店系统的用户端来说基本够用。但如果我预见到以后要加手机号登录、头像、生日这类字段最稳妥的做法是项目一开始就自定义 User 模型指定 AUTH_USER_MODEL。我在这个项目里采用了一个折中方案用户端注册时只填用户名、邮箱、密码扩展字段收货地址、手机号放到下单时再填通过 Order 表的 receiver 字段保存不强制用户维护个人资料。这样做业务上完全说得通很多人买甜点的时候不想注册完整资料下单时填一次收货信息就够了。如果你打算把这个项目作为长期维护的起点我会更建议走自定义 User 模型的路子。一旦上线再改 AUTH_USER_MODEL会涉及数据库迁移的复杂操作那个坑我见过有人踩非常痛苦。3. 购物车和订单系统里最容易翻车的两条链路3.1 购物车session 方案和数据库方案我选了后者购物车的存储方案向来是电商项目的经典争论点。session 方案是把购物车数据存到服务端 session默认存在数据库表 django_session 里优点是匿名用户也能加购物车不强迫登录缺点是 session 有过期时间用户隔几天再来可能丢购物车而且数据持久性差。数据库方案是建一张 CartItem 表用户登录后购物车记录永久保存在数据库里换设备也能同步缺点是需要登录才能加入购物车并且要为未登录用户做处理。甜点店系统最终选了数据库方案核心考虑是甜点单价不算低顾客通常是计划性购买而非冲动消费购物车保存周期长session 过期丢购物车对体验伤害很大。未登录用户点击加入购物车时我会直接引导他先登录登录成功后再跳回商品详情页。虽然多了一步跳转但换来了购物车的确定性。购物车的数量修改我选择了用表单提交后整页刷新而不是写 Ajax 局部刷新。这个选择在后台代码上省事不少。如果你觉得体验不够顺滑以后可以升级成 Django JSON 接口的方式把数量减一和删除做成异步请求。3.2 下单事务与库存扣减下单是整个系统最需要谨慎的逻辑。核心问题只有一个同时有多个人买同一款甜点时库存会不会超卖。我第一版的下单逻辑是先查库存if stock quantity 就减库存再创建订单。这个逻辑在并发低的时候没问题但一旦两个人同时提交对同一款库存只剩一件的甜点时两个请求都可能通过检查然后各扣一次库存库存就变负数了。正确的做法是用 Django 的事务和行级锁。核心代码长这样from django.db import transaction transaction.atomic def create_order(user, cart_items, receiver_info): total_amount 0 order Order.objects.create( useruser, statuspending, total_amount0, **receiver_info ) for item in cart_items: dessert Dessert.objects.select_for_update().get(pkitem.dessert_id) if dessert.stock item.quantity: raise ValueError(f{dessert.name} 库存不足) dessert.stock - item.quantity dessert.save() OrderItem.objects.create( orderorder, dessertdessert, pricedessert.price, quantityitem.quantity, ) total_amount dessert.price * item.quantity order.total_amount total_amount order.save() CartItem.objects.filter(useruser).delete() return orderselect_for_update 是整段代码的灵魂。它会在数据库层面给选中的那行记录加锁直到事务结束。第二个请求进来时会阻塞在 select_for_update 上等第一个事务提交后才能读取到最新的库存值。这样库存就不会被扣负。这段逻辑还做了一件很重要的事把总价计算放在后端完成而不是信任前端传过来的金额。前端传金额就能被伪造这是电商项目必须注意的底线。3.3 订单状态机设计订单状态我用了最简单的常量设计但依然遵循了状态机的约束。状态定义如下pending待支付paid已支付待制作delivering配送中completed已完成cancelled已取消状态流转我限定为pending 可以转 paid 或 cancelledpaid 转 deliveringdelivering 转 completed。不允许跳变也不允许逆向流转除管理员手动取消外。实现时我没有引第三方状态机库只在 Order 模型里加了 change_status 方法内部用字典维护合法流转关系。这样做的意图是让状态变更都走同一个方法后续如果要加日志只需要在这个方法里统一记录。订单列表页用户只能看到自己的订单并且只能对待支付状态执行取消订单操作。这个权限控制必须在视图层校验不能只在前端隐藏按钮——直接构造请求照样能调到后端接口。4. 关键代码复盘列表、详情、后台管理4.1 商品列表的分页与分类筛选甜点列表页的核心功能是分类筛选和分页。分类筛选最笨的做法是用 if 判断手动拼查询条件但 Django 的 ORM 支持链式过滤代码可以写得很干净def dessert_list(request, category_idNone): queryset Dessert.objects.filter(is_activeTrue) if category_id: queryset queryset.filter(category_idcategory_id) keyword request.GET.get(q, ).strip() if keyword: queryset queryset.filter(name__icontainskeyword) paginator Paginator(queryset, 12) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, shop/list.html, {page_obj: page_obj, category_id: category_id})这里的 category_id 判断为什么用 if 而不是直接在 filter 里写 category_idrequest.GET.get(category)因为当 URL 里没有 category 参数时GET.get 返回 NoneORM 的 filtercategory_idNone会把分类为空的数据也过滤掉而不是返回全部。这一点特别容易踩坑。分页我用了 Django 自带的 Paginator并在模板中循环 page_obj.paginator.page_range 生成页码。同时注意在 URL 中保留 category 和 q 参数否则点击第二页时筛选条件就丢了。处理办法是在模板中手动拼查询字符串?category{{ category_id }}page{{ p }}。4.2 商品详情页的图片与多规格处理甜点详情页比普通商品多一个维度规格。蛋糕类商品通常有 6 寸、8 寸、10 寸的区别价格也不同。我第一版设计是把规格做成独立的 Specification 表后来发现对于甜点店场景大部分商品规格差异不大过度建模反而让管理端变复杂。最终方案是在 Dessert 表上直接加两个字段size_optionsJSONField 存储尺寸和加价如 {6寸: 0, 8寸: 50}和 base_price基础价格。加入购物车时通过表单里的 size 字段确定实际价格。这样管理端上架商品非常直观。图片上传是另一个细节。Django 的 ImageField 要求配置 MEDIA_ROOT 和 MEDIA_URL开发环境还需要在 urls.py 里加 static 服务from django.conf import settings from django.conf.urls.static import static urlpatterns [...] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这一步如果不配你在 admin 里上传的图片会一直显示 404。但要注意这段代码只用于开发环境部署后必须由 Nginx 接管图片服务否则媒体文件会拖垮 Django 进程。4.3 admin 后台的二次定制Django admin 默认界面能用但直接给店主用会有点简陋。我做了三处定制都不复杂但效果明显。第一是列表页显示优化。在 ModelAdmin 里配置 list_display、list_filter、search_fields 和 orderingadmin.register(Dessert) class DessertAdmin(admin.ModelAdmin): list_display [name, category, price, stock, is_recommend, created_at] list_filter [category, is_new, is_recommend] search_fields [name, description] list_editable [stock, is_recommend] list_per_page 20list_editable 是最实用的功能。店主可以在列表页直接修改库存和推荐标记不用点进详情页改。第二是订单管理的操作按钮。我给 OrderAdmin 加了 mark_paid、mark_delivering、mark_completed 这几个 action 方法管理员勾选订单后可以批量更新状态比逐一打开订单改状态高效得多。第三是统计视图。我在 admin 里注册了一个自定义视图展示近 7 天销量 Top10 甜点和今日订单数/销售额两个指标。实现方式是重写 admin 的 get_urls 方法新增一个 URL 指向自定义模板查询逻辑直接聚合 OrderItem 表from django.db.models import Sum top_items ( OrderItem.objects.values(dessert__name) .annotate(total_qtySum(quantity)) .order_by(-total_qty)[:10] )这个自定义页面只需要一个小模板就能让管理后台从数据管理工具变成经营看板。5. 部署上线过程中我踩过的坑5.1 本地跑通之后静态文件全丢了这是我在部署阶段遇到的第一个坑。本地开发时 Django 会自动处理静态文件但 DEBUGFalse 之后Django 就不再提供静态文件服务页面瞬间变成一个没有 CSS 的光杆页面。解决办法分两步。第一步是配置 STATIC_ROOT 并运行 collectstatic 命令让 Django 把所有应用和 admin 的静态文件收集到一个统一目录。第二步是在部署环境用 Nginx 直接服务这个目录location /static/ { alias /opt/dessert_shop/staticfiles/; } location /media/ { alias /opt/dessert_shop/media/; }这里有个潜规则admin 后台的静态文件也在 STATIC_ROOT 里如果你只收集了自己应用的静态文件而忘了 admin 的后台页面就会裸奔。collectstatic 默认会收集 django.contrib.admin 的静态文件前提是你没有在 STATICFILES_FINDERS 里做多余的自定义。5.2 SQLite 到 PostgreSQL 的迁移开发阶段我用了 SQLite部署阶段切成 PostgreSQL。这一步如果是在项目初期就设计好切换成本其实不高但我还是踩了坑——迁移时数字字段的类型差异。Django ORM 的 DecimalField 在 SQLite 里实际存储为 NUMERIC 类型在 PostgreSQL 里是 NUMERIC/Decimal。单纯跑 migrate 没问题问题是已有数据迁移。如果直接用 dumpdata 导出 json 再 loaddata 导入价格字段极少数情况下会出现精度变化比如 28.00 变成 28.0。我最终的方案是生产环境直接新建数据库手动执行迁移创建表结构然后写脚本把开发环境的关键数据商品、分类、用户重新录入。订单这类历史数据不迁移因为开发阶段产生的订单本来就是测试数据。这种抛弃旧数据的方式看似粗暴实际是最干净的。你如果一定要完整迁移推荐用 PostgreSQL 提供的 pgloader 工具但 Django 项目里我更建议从一开始就思考清楚开发默认用 PostgreSQL 配套环境测试也用它避免部署时出现意外。5.3 服务进程管理与环境变量配置我最后采用的是 Gunicorn Nginx 的组合。生产环境的启动命令长这样gunicorn dessert_shop.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 3 \ --timeout 60 \ --access-logfile /var/log/dessert_shop/access.log \ --error-logfile /var/log/dessert_shop/error.logworkers 数量有个经验值2 × CPU 核心数 1。我这个项目运行在一台 2 核云服务器上所以用了 3 个 worker。多了反而会增加进程切换开销对 IO 密集型的 Django 应用并不友好。产品配置里settings.py 中的 SECRET_KEY、数据库账号、可选的第三方密钥都不应该直接写死在代码里。我用的是环境变量 .env 文件的方式settings.py 里用 os.environ.get(SECRET_KEY) 读取部署时在服务器上配置 .env 文件并保证它不属于版本管理。这样做最大的好处是代码仓库泄露了也不会暴露关键密钥。很多新手项目把 SECRET_KEY 直接扔到 GitHub 上这个问题一旦被有心人利用后果很严重尤其是会话伪造攻击。另一个部署细节是 CSRF 安全性。生产环境如果设置了 ALLOWED_HOSTS 却漏了真实域名所有 POST 请求都会被拒绝报错信息显示 CSRF verification failed。这个报错我和同事排查过不少次先查 ALLOWED_HOSTS 再查 CSRF_TRUSTED_ORIGINS能少走弯路。6. 做完这个项目的几点实在体会项目跑通之后我又回头做了两轮重构这里挑三个最有代表性的体会分享。第一垂直品类电商项目的功能收敛比我想象中的重要。甜点店系统不需要做秒杀、优惠券、拼团这些复杂玩法把商品展示、购物车、订单、后台管理这四条链路做扎实就已经能支撑一个真实小店上线运营。新手做作品集不需要贪大小而完整远比大而残缺有说服力。第二表单校验和异常处理是代码质量的真正分水岭。我第一版的下单视图几乎没做异常捕获库存不足直接抛 ValueError用户看到的是 500 错误页。后来改造为在视图内捕获异常并通过 Django messages 框架给用户显示一个友好的提示该商品库存不足请调整数量体验完全不同。代码本身却没有变复杂很多只是多了一个 try-except 和一行 messages.error。第三使用 Django 管理后台并不意味着不写代码。相反为了让 admin 真正贴合业务还是需要在 ModelAdmin 里做大量细节配置哪些字段可搜索、哪些可编辑、列表默认排序、批量操作按钮。调试这些配置的时间我花得比写前端页面还要多。最后分享一个实用的小技巧如果你想给这个项目增加一点亮点可以在订单完成页接入一个简单的销量统计接口把这款甜点最近被购买了 N 次展示出来。实现不复杂就是聚合 OrderItem 表按条件计数但对用户决策的引导作用很明显。我在实际测试中看到加了这个小模块之后热门商品的成交率有明显提升。这大概就是项目从能用到好用之间最值得投入的那一段距离。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑