
简介基于Django实现的电子商城管理系统源码包适合Python Web初中级开发者、计算机专业学生以及需要快速搭建电商原型的技术人员。项目完整覆盖用户登录注册、商品展示与分类、购物车、订单处理等核心业务模块配有HTML页面、CSS样式、JavaScript交互及Bootstrap前端组件页面模板与静态资源齐备可直接运行或二次开发便于理解Django MTV架构与前后端协作方式。压缩包共255个文件包含64个py源码、59个pyc编译文件、95个png图片资源、15个html模板以及sqlite3数据库、配置文件等整体仅5.96MB目录结构清晰便于分模块学习和部署调试。目前已有320人学习下载适合用于课程设计、毕业设计参照或电商项目练手。资源内还附带markdown说明与gitignore等工程辅助文件能帮助读者快速了解项目运行方式与版本管理配置。SQLite数据库文件也已包含在包内能减少额外配置成本便于快速启动项目。1. 一个 Django 商城卡死的夜晚问题不在后端上个月帮朋友排查一个上线两周的 Django 商城症状很典型商品列表页打开要 6 秒后台登录偶尔 504前端工程师一口咬定是接口慢后端把 SQL 翻了个底朝天也没找到瓶颈。最后定位到根因时大家都沉默了——问题出在模板层对静态资源的引用方式上十几个页面各自为政地引 CSS重复加载了将近 400KB 的样式文件再加上没有做模板继承改一个公共头部要动八个文件。这个基于 Django 实现的电子商城管理系统源码包恰好把这类问题的标准解法都放在了一个可运行的工程里bootstrap 样式体系、index/list/login 三套核心页面、header 公共模板以及配套的 css 与 gitignore 配置。适合正在做课程设计的学生、刚接触 Django 商城开发的初级工程师以及想看看别人怎么组织电商前端资源的人。这篇文章会把工程拆开来讲先看数据模型和目录怎么设计再跟一遍路由、视图与模板的完整渲染链路最后说清楚静态资源到底该怎么管理。2. 目录结构与数据模型设计先看清这个商城怎么组织拿到源码包后第一件事不是急着跑起来而是先把目录结构和数据模型理清。这个商城系统的工程组织方式比较有代表性它没有把静态文件散落在各个 app 里而是统一收拢到全局静态目录模板则通过 Django 的模板继承机制复用公共部分。2.1 从目录布局反推工程习惯解压源码包后能看到这样一层结构project_root/ ├── manage.py ├── db.sqlite3 ├── apps/ │ ├── goods/ # 商品模块 │ ├── user/ # 用户模块 │ └── order/ # 订单模块 ├── templates/ │ ├── index.html # 商城首页 │ ├── list.html # 商品列表页 │ ├── login.html # 登录页 │ └── header.html # 公共头部 ├── static/ │ ├── bootstrap.min.css │ ├── css.css │ ├── index.css │ ├── list.css │ └── login.css └── config/ ├── settings.py └── urls.py注意这里的apps/目录商城类项目常见的拆分维度是按业务域划分而不是按页面划分。很多初学者喜欢建一个myapp然后把所有模型放进去这样做到后期订单、商品、用户逻辑交织在一起迁移脚本会非常痛苦。按业务域拆分后每个 app 的 models.py 都不超过 200 行维护成本明显下降。templates/目录放在全局而不是某个 app 内部这个设计值得留意。Django 默认的 app 级模板目录app/templates/在小型项目里够用但商城类项目往往有多个 app 共用同一套页面骨架全局模板目录配合APP_DIRS True的查找顺序能减少很多路径解析的麻烦。源码包里的header.html就是典型的公共模板它被 index 和 list 同时引用。2.2 商品与订单模型字段设计的边界在哪里商城系统的核心模型通常围绕商品、用户、订单三个实体展开。这套源码里虽然没有给出完整的 models.py 内容但从页面功能反推数据结构大致会涉及以下这些字段模型关键字段说明Categoryname, slug, parentslug 用于 SEO 友好的 URLparent 支持多级分类Goodsname, category, price, stock, sales, image, is_on_salestock 和 sales 是一对需要频繁更新的字段Userusername, password, phone, address继承 AbstractUser 扩展Orderorder_no, user, total_amount, status, created_atorder_no 建议用时间戳加随机数生成OrderItemorder, goods, price, count下单时快照商品价格防止商品改价影响历史订单这里有一个容易踩坑的点价格字段的类型。商城项目中价格绝对不能用 floatDjango 侧用DecimalField(max_digits10, decimal_places2)是标准做法。float 在累加计算时会引入二进制浮点误差比如 0.1 加 0.2 得到 0.30000000000000004 这种问题放到订单金额计算里就是生产事故。源码里虽然没有直接展示这个字段但合格的电商项目一定会处理这一点。商品列表页的排序和筛选逻辑通常依赖Goods.objects.filter(is_on_saleTrue).order_by(sales)这样的查询链。要注意的是sales字段在首页推荐场景下需要倒序在列表页可能需要按价格或上架时间排序所以模型里给sales和price加上db_indexTrue是划算的——商城类应用的查询压力大多集中在这两个字段上。2.3 迁移策略先设计还是先迁移拿到源码后如果想改模型我建议按这个顺序操作python manage.py makemigrations goods python manage.py migratemakemigrations会扫描apps/goods/migrations/目录下的迁移文件对比模型定义生成新的迁移脚本。这里有个细节如果你改了模型字段但没有先生成迁移文件就直接跑migrateDjango 会提示No changes detected但数据库表结构其实没改。很多新手在这里卡住改完模型死活不生效以为是代码没保存实际上是漏了makemigrations这一步。另外源码包带着db.sqlite3如果直接跑项目已有的数据和你的模型变更可能冲突。我的做法是删除db.sqlite3和migrations/目录下除了__init__.py之外的所有文件然后重新迁移这样能保证一套干干净净的库。3. 路由、视图与上下文字典从 URL 到 HTML 的渲染链路模型只是数据的地基真正把数据变成页面的是路由、视图和模板三者之间的配合。这一章沿着一次完整的商品列表页请求把整条链路走通。3.1 URL 设计与视图函数的对应关系商城项目的路由设计有一个基本惯例列表页、详情页、登录页分别对应不同的 URL 模式。看源码包的页面文件能反推出路由配置大概长这样# config/urls.py from django.urls import path from apps.goods import views as goods_views from apps.user import views as user_views urlpatterns [ path(, goods_views.index, nameindex), path(list/, goods_views.goods_list, namegoods_list), path(login/, user_views.login_view, namelogin), ]path()的第一参数是 URL 规则第二参数是视图函数name参数是给这个路由起的别名。别小看这个name模板里用{% url goods_list %}反向解析 URL 靠的就是它。如果哪天你把list/改成了goods/只要name不变模板和视图代码一行都不用动。这个特性在商城项目重构 URL 结构时特别有用。源码包里既然有list.html那必然有对应的列表页视图。商品列表页的视图函数一般长这样# apps/goods/views.py from django.shortcuts import render, get_object_or_404 from .models import Goods, Category def goods_list(request): category_id request.GET.get(category, ) sort request.GET.get(sort, default) goods Goods.objects.filter(is_on_saleTrue) # 按分类筛选 if category_id: goods goods.filter(category_idcategory_id) # 排序逻辑 if sort price_asc: goods goods.order_by(price) elif sort price_desc: goods goods.order_by(-price) elif sort sales: goods goods.order_by(-sales) else: goods goods.order_by(-id) categories Category.objects.all() return render(request, list.html, { goods: goods, categories: categories, current_sort: sort, })这段代码的关键在于QuerySet的惰性求值。Goods.objects.filter(...)这一行并没有真正执行 SQL它只是构建了一个查询对象。直到render()里模板对goods进行迭代时Django 才会真正去数据库取数。这意味着你可以在视图里根据用户传入的参数不断追加filter()或order_by()而不会产生多余的数据库查询——前提是你没有在中间步骤里对 QuerySet 做list()或len()操作一旦强制求值后续的过滤就失效了。request.GET.get(category, )里的默认参数是为了避免category参数不存在时报None错误。排序参数sort同理。这种从 URL 查询参数反推用户意图的模式在商城列表页里是标配。3.2 上下文字典视图与模板之间的数据契约视图函数最后一行的render(request, list.html, {...})第三个参数叫上下文也就是传给模板的数据字典。这个字典的设计直接影响模板的可维护性。在模板里变量名必须和字典的 key 完全一致{% for item in goods %} div classgoods-item h3{{ item.name }}/h3 span¥{{ item.price }}/span {% if item.stock 0 %} a href{% url goods_detail item.id %}立即购买/a {% else %} button disabled已售罄/button {% endif %} /div {% endfor %}注意{{ item.price }}输出的是Decimal类型的值如果价格是 99.90模板里会显示 99.90但如果数据库存的是整数形式就只会显示 99.9。为了统一显示格式常见做法是在模型里定义好__str__方法或者用模板过滤器{{ item.price|floatformat:2 }}强制保留两位小数。这里有一个容易被忽略的性能点循环里访问item.category这样的外键字段时Django ORM 会为每一条商品记录额外执行一次查询也就是 N1 问题。商品列表页 20 件商品就会产生 21 条 SQL。正确的做法是在视图里加select_relatedgoods Goods.objects.filter(is_on_saleTrue).select_related(category)加了这一句之后Django 会用一条LEFT JOIN把分类信息一次性查出来商品和分类的数据都在内存里了。这个优化在 Localhost 上感知不到但部署到线上尤其是数据库和 Web 服务分离的架构里差距可能是 50ms 和 500ms 的区别。3.3 登录视图session 与 redirect 的配合login.html对应的登录视图核心逻辑涉及 session 的读写。源码包的登录页大概率是配合 Django 自带的authenticate和login函数实现的# apps/user/views.py from django.contrib.auth import authenticate, login from django.shortcuts import redirect, render def login_view(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) next_url request.GET.get(next, /) return redirect(next_url) else: error_msg 用户名或密码错误 return render(request, login.html, {error: error_msg}) return render(request, login.html)这里有两个细节值得展开。第一request.POST.get(username)用的是get而不是[]这是防御性编程的习惯——如果表单里没有username字段[]会抛MultiValueDictKeyError而get返回None。第二next参数的处理用户未登录时访问需要登录的页面Django 的login_required装饰器会把用户带到login/?next/order/登录成功后再跳回原来的页面。这个体验细节很多商城项目会忘记处理导致用户登录后总是回到首页还要重新找商品。视图层还有一个常见的权限控制写法就是在视图函数上叠加装饰器from django.contrib.auth.decorators import login_required login_required(login_url/login/) def order_confirm(request): # 订单确认逻辑 passlogin_url参数指定了未登录用户的重定向目标不写的话 Django 默认跳到/accounts/login/在自定义商城系统里往往会 404所以显式声明这个参数能避免一个隐藏的坑。4. 模板继承与静态资源装配CSS 引用的正确姿势源码包里放了一堆 css 文件bootstrap.min.css、css.css、index.css、list.css、login.css这是整个项目里最容易出问题也最容易被忽略的部分。前面说过那个商城卡死的案例根因就在这。这一章完整讲一遍模板继承和静态资源引用的标准做法。4.1 用 block 建立模板骨架Django 模板继承的核心是{% block %}和{% extends %}。源码包的header.html应该是被 index 和 list 共用的但更规范的工程结构里还会有一个base.html作为最底层的骨架!-- templates/base.html -- {% load static %} !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}电子商城{% endblock %}/title link relstylesheet href{% static bootstrap.min.css %} link relstylesheet href{% static css.css %} {% block extra_css %}{% endblock %} /head body {% include header.html %} main {% block content %}{% endblock %} /main script src{% static bootstrap.min.js %}/script {% block extra_js %}{% endblock %} /body /html这个base.html做的事是把每个页面共用的 CSS 和 JS 放在基础模板里只给各页面留出title、extra_css、content、extra_js四个缺口。{% load static %}必须写在文件最开头否则{% static %}模板标签无法识别。子模板通过{% extends base.html %}继承骨架然后只填充自己的缺口!-- templates/index.html -- {% extends base.html %} {% load static %} {% block title %}首页 - 电子商城{% endblock %} {% block extra_css %} link relstylesheet href{% static index.css %} {% endblock %} {% block content %} div classbanner h2限时特惠/h2 /div !-- 首页内容 -- {% endblock %}这样设计的收益很直接公共的 bootstrap.min.css 和 css.css 在 base.html 里只加载一次子页面只需要额外加载自己的 index.css。如果某天要升级 Bootstrap 版本只需要改 base.html 一行代码全站生效——不需要去每个 html 文件里手动替换。4.2 static 标签与路径解析机制{% static index.css %}这行代码做的事情是把静态文件路径自动拼接上你在settings.py里配置的STATIC_URL# config/settings.py STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, ] STATIC_ROOT BASE_DIR / collect_staticSTATICFILES_DIRS告诉 Django 开发服务器去项目根目录的static文件夹找静态资源而STATIC_ROOT是执行python manage.py collectstatic时的输出目录。collectstatic会把STATICFILES_DIRS和应用内的静态文件全部收集到STATIC_ROOT这是部署到生产环境前必须执行的一步。提示开发环境下DEBUGTrue时 Django 会自动处理静态文件的 URL 映射但DEBUGFalse之后这个功能就失效了。如果你在服务器上把 DEBUG 关掉后发现所有 CSS 都丢了先想到collectstatic和 Web 服务器的静态文件配置。源码包里既然有.gitignore文件里面通常会有collect_static/这一项因为收集后的静态文件属于构建产物不应该提交到 Git 仓库。同理db.sqlite3也在忽略列表里。4.3 页面级 CSS 的拆分策略源码包里的 index.css、list.css、login.css 分别对应三个页面这种按页面拆 CSS 的方式在中小型项目里完全够用。但要注意拆分的粒度css.css 放全站通用样式index.css 只放首页特有样式不要一个文件里堆几千行。我见过一个真实案例index.css 里存了 3000 多行样式其中一半是列表页和登录页的导致首页加载时浏览器要下载一大堆用不到的规则。正确的做法是每个页面文件只包含自己真正用到的样式公共部分进 css.css框架部分进 bootstrap.min.css。浏览器在渲染某个页面时只下载 base.html 里的公共 CSS 加当前页面的独立 CSS。如果项目后期页面多了还可以用 Django 的{% block extra_css %}机制做按需加载——列表页不从 base.html 继承 list.css而是在自己的extra_css块里引用。这样首页不会带上列表页的样式反之亦然。这个优化对首屏性能的影响在网速较慢的移动端尤其明显。4.4 模板 include 与自定义模板标签header.html这种公共组件用{% include %}引入但 include 有一个隐性成本每次 include 都会解析一次被引入的模板循环里用 include 要格外小心性能。比如{% for item in goods %} {% include goods_card.html %} {% endfor %}这个写法会有性能问题。共产出 20 件商品Django 就要解析 20 次goods_card.html而且每次 include 的上下文都要做一次深拷贝CPU 开销不低。更好的做法是使用自定义 inclusion tag# apps/goods/templatetags/goods_extras.py from django import template register template.Library() register.inclusion_tag(goods_card.html) def render_goods_card(goods): return {goods: goods}在模板里这样调用{% load goods_extras %} {% for item in goods %} {% render_goods_card item %} {% endfor %}inclusion tag 在注册时就把模板路径绑定好了Django 内部会做缓存解析性能比 include 好很多。源码包里没有涉及这个机制但如果你的商城项目里商品卡片被多个页面复用这个优化值得做。5. 部署前必须处理的三类隐患数据库适配、静态文件与并发配置前面四章把项目的代码结构讲透了但源码在本地跑通和在生产环境稳定运行之间还隔着几道坎。最后一章把部署阶段最常见的三类隐患讲到位。5.1 SQLite 到 MySQL 的数据迁移源码包默认带着db.sqlite3本地开发够用但上线之后建议换成 MySQL。SQLite 在并发写入场景下有锁竞争商城的下单操作一多就容易卡。迁移 MySQL 需要修改 settings.py 并安装驱动pip install mysqlclient# config/settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: mall_db, USER: mall_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }这里把charset设为utf8mb4很重要。MySQL 的 utf8 字符集只支持最多 3 字节的字符而 emoji 表情和生僻字需要 4 字节。商品名称里如果带个 emoji写入就会报错utf8mb4能完整支持 4 字节字符。注意迁移后务必检查库存表里的数据是否完整。SQLite 的布尔字段存的是 0/1MySQL 的BooleanField底层是tinyint(1)这两者在 Django ORM 层面能自动转换但如果你在 SQL 层面手动做过查询要对这条差异心里有数。5.2 DEBUG 关闭后的静态文件服务DEBUGFalse是一个分水岭。关掉之后Django 不再处理静态文件和媒体文件这会导致页面裸奔。正确做法是先执行python manage.py collectstatic --noinput然后让 Nginx 直接服务/static/路径下的文件。Nginx 的配置片段location /static/ { alias /opt/mall/collect_static/; expires 7d; }expires 7d给静态文件加了 7 天的浏览器缓存商城页面的 CSS 和图片资源就不需要每次请求都回源了。但要注意如果更新了 CSS 文件文件名没变浏览器会用缓存里的旧文件用户看不到样式变化。解决这个问题的方法是在{% static %}标签后加版本参数link relstylesheet href{% static index.css %}?v20250101版本号一升级浏览器的 URL 就变了强制重新拉取新文件。5.3 Gunicorn 生产并发配置Django 自带的runserver只能用于开发调试生产环境用 Gunicorn 是常见方案gunicorn config.wsgi:application -w 4 -b 0.0.0.0:8000 --timeout 60-w 4指定 4 个 worker 进程--timeout 60给每个请求设置 60 秒超时。worker 数量一般按 CPU 核心数的 2 到 4 倍来配不是越多越好——worker 多了数据库连接池的开销也会线性增长。前端请求到 Django 还要经过一道反向代理Nginx 里需要转发动态请求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-Real-IP这行别省略。没有它request.META[REMOTE_ADDR]拿到的永远是 Nginx 的 127.0.0.1日志里看不到真实用户 IP对排错和风控都有影响。5.4 SQLite 到 MySQL 切换后的查询验证迁移完数据库后不要直接交付。先跑几个核心链路的验证查询对比前后结果是否一致-- 验证商品数量 SELECT COUNT(*) FROM goods; -- 验证库存不为负 SELECT id, name, stock FROM goods WHERE stock 0; -- 验证订单金额与明细总和一致 SELECT o.order_no, o.total_amount, SUM(i.price * i.count) AS calc_total FROM order o JOIN order_item i ON i.order_id o.id GROUP BY o.id HAVING o.total_amount ! calc_total;第三种查询是最容易被忽视的。订单总金额和明细行金额不一致通常是因为下单时的价格快照逻辑有 bug或者 Decimal 字段被错误地转成了 float 导致精度丢失。商城项目里这类数据校验脚本建议直接写进项目的management/commands/里做成可重复执行的 check 命令每次数据库变更后跑一遍。本文还有配套的精品资源点击获取