
简介这是一套基于Python开发的完整在线商城网站实战项目源码面向Python初学者与Web开发入门者提供从前端展示到后台管理、从用户注册登录到购物支付的全流程实现参考。资源包含网站前台与后台两大模块支持本地快速部署运行涵盖商品浏览、分类检索、购物车管理、物流填写及支付宝扫码支付等核心电商功能适合作为课程设计、毕业项目或自学练手范例。压缩包共427个文件总计9.04MB其中HTML/CSS/JS构成前端页面与交互逻辑共127个Python脚本10个实现Flask后端路由与业务处理PNG/JPG/GIF等图像资源303个支撑界面展示另有SQL数据库文件、配置文件INI及文档说明结构清晰、即开即用。目前已有301人学习下载配套说明书详述各页面访问路径与操作流程内容预览显示多套CSS皮肤样式与模块化样式文件便于二次定制与UI优化。1. 为什么一个“Python在线商城网站”项目比你想象中更值得从头搭一遍不是所有商城 Demo 都叫「在线商城网站」——它得能真实走通用户注册 → 浏览商品 → 加入购物车 → 提交订单 → 后台管理库存的全链路且全程不依赖现成 SaaS 平台或低代码工具。这个标题里的“Python实现”明确指向用 Django 或 Flask 自主构建后端服务前端可轻量Jinja2 模板直出也可解耦Vue/React REST API但核心逻辑、数据模型、状态流转必须由你亲手定义和调试。它解决的不是“怎么放几张商品图”而是“如何让库存扣减在并发下单时不超卖”“如何让用户登录态在页面跳转中不丢失”“如何让管理员改个价格立刻生效而不是等缓存过期”。适合刚学完 Web 框架基础、正卡在“写完 Hello World 就不会往下走”的开发者也适合想补全工程闭环意识的转岗者。这不是玩具项目是能放进简历里、经得起面试官追问“你处理过支付回调幂等吗”的实战载体。2. 用 Django 搭建最小可行商城从django-admin startproject到首页可访问2.1 为什么选 Django 而非 Flask三个硬性理由ORM 内置强一致性商城最怕库存错乱Django 的select_for_update()可直接在数据库行级加锁配合事务比 FlaskSQLAlchemy 手动写with db.session.begin_nested():更少出错Admin 后台开箱即用商品上下架、用户封禁、订单状态批量修改不用另起一套 Vue 管理系统5 分钟配好admin.py就能交付给测试同学中间件链清晰可控比如要加“未登录用户禁止访问购物车页”Django 的LoginRequiredMixin一行继承就搞定而 Flask 需自己写装饰器session 判断重定向逻辑新手极易漏掉login_required和return redirect的组合。提示本方案默认使用 Django 4.2 LTS长期支持版Python 3.10。不推荐用 5.x因部分第三方包如 django-crispy-forms尚未完全适配会触发RemovedInDjango50Warning类警告影响调试专注度。2.2 四步初始化创建项目、配置数据库、生成核心 App、跑通首页# 1. 创建虚拟环境并激活强制避免包冲突 python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 2. 安装 Django 并启动项目 pip install Django4.2,4.3 django-admin startproject mall . python manage.py startapp product python manage.py startapp order python manage.py startapp user此时目录结构为mall/ ├── manage.py ├── mall/ # 项目配置目录 │ ├── __init__.py │ ├── settings.py # 关键后续要改 DATABASES 和 INSTALLED_APPS │ ├── urls.py │ └── asgi.py ├── product/ # 商品模块 ├── order/ # 订单模块 └── user/ # 用户模块# 3. 修改 mall/settings.py —— 重点改三处 # a) 注册新 App顺序重要user 必须在 auth 之后 INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, user, # ← 新增 product, # ← 新增 order, # ← 新增 ] # b) 配置 SQLite开发阶段够用无需额外安装数据库服务 DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } # c) 静态文件路径让 CSS/JS 能被正确加载 STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]# 4. 迁移数据库并启动服务 python manage.py makemigrations python manage.py migrate python manage.py createsuperuser # 创建管理员账号用于登录 admin python manage.py runserver访问http://127.0.0.1:8000/admin/用刚创建的账号登录即可看到空的后台界面访问http://127.0.0.1:8000/应显示 Django 默认欢迎页。这表示基础骨架已通下一步才开始写业务逻辑。2.3 首页路由与模板用最简方式验证前后端连通# product/views.py from django.shortcuts import render def home(request): return render(request, home.html)# product/urls.py from django.urls import path from . import views urlpatterns [ path(, views.home, namehome), ]# mall/urls.py —— 在 urlpatterns 中插入 include from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(product.urls)), # ← 新增这一行 ]!-- templates/home.html -- !DOCTYPE html html headtitle在线商城首页/title/head body h1欢迎来到 Python 在线商城/h1 p当前时间{{ now|date:Y-m-d H:i:s }}/p /body /html# mall/settings.py —— 补充模板路径配置 TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], # ← 新增 APP_DIRS: True, OPTIONS: { context_processors: [ django.template.context_processors.debug, django.template.context_processors.request, django.contrib.auth.context_processors.auth, django.contrib.messages.context_processors.messages, ], }, }, ]重启服务后访问根路径应显示带时间的欢迎页。注意{{ now }}能渲染说明模板引擎工作正常没有报TemplateDoesNotExist说明DIRS配置正确。这是后续所有页面渲染的基石。3. 商品与用户模块落地定义模型、填充测试数据、暴露 API3.1 商品模型设计为什么Product要拆成Category和Product两张表商城不是静态展示需支持“手机分类下有 iPhone、华为、小米”“电脑分类下有 MacBook、ThinkPad”。若把分类名直接写进Product.category字段CharField会导致修改“手机”为“智能手机”时要批量更新所有商品记录无法统计“每个分类下多少商品”需GROUP BYCOUNT但字段无索引前端下拉菜单需手动维护字符串列表易遗漏。所以必须建独立Category模型# product/models.py from django.db import models class Category(models.Model): name models.CharField(max_length100, uniqueTrue) slug models.SlugField(max_length100, uniqueTrue) # 用于 URL如 /category/smartphone/ def __str__(self): return self.name class Product(models.Model): name models.CharField(max_length200) description models.TextField(blankTrue) price models.DecimalField(max_digits10, decimal_places2) # 精确到分 stock models.PositiveIntegerField(default0) # 库存不能为负 category models.ForeignKey(Category, on_deletemodels.CASCADE, related_nameproducts) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name# 生成迁移并应用 python manage.py makemigrations product python manage.py migrate注意on_deletemodels.CASCADE表示删除分类时其下所有商品一并删除。生产环境慎用此处为简化逻辑。若需保留商品改用models.PROTECT删除前校验或models.SET_NULL需nullTrue。3.2 用户扩展为什么不用User.objects.create_user()直接存手机号Django 自带User模型含username,email,password但商城需要手机号登录、收货地址、昵称。若强行往User表加字段违反单一职责User应只管认证业务属性归业务模型升级风险Django 升级可能调整User表结构自定义字段易冲突。标准做法是一对一扩展# user/models.py from django.db import models from django.contrib.auth.models import User class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) phone models.CharField(max_length11, blankTrue) avatar models.ImageField(upload_toavatars/, blankTrue) address models.TextField(blankTrue) def __str__(self): return f{self.user.username}s profile# user/signals.py —— 自动为新用户创建 Profile from django.db.models.signals import post_save from django.dispatch import receiver from django.contrib.auth.models import User from .models import UserProfile receiver(post_save, senderUser) def create_user_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(userinstance) receiver(post_save, senderUser) def save_user_profile(sender, instance, **kwargs): instance.profile.save()# user/apps.py —— 确保信号注册 from django.apps import AppConfig class UserConfig(AppConfig): default_auto_field django.db.models.BigAutoField name user def ready(self): import user.signals # ← 关键导入 signals 模块# user/admin.py —— 后台合并显示 from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User from .models import UserProfile class UserProfileInline(admin.StackedInline): model UserProfile can_delete False verbose_name_plural Profile class CustomUserAdmin(UserAdmin): inlines (UserProfileInline,) admin.site.unregister(User) admin.site.register(User, CustomUserAdmin)此时在 Admin 后台新建用户会自动出现 Profile 区域填手机号、头像即可。这是后续实现“手机号登录”的数据基础。3.3 用 Django Shell 快速填充测试商品数据别用手点 Admin 录 100 条商品。用shell批量造数且保证数据真实可用有图片、有库存、有分类python manage.py shell# 在 shell 中执行 from product.models import Category, Product from django.core.files.base import ContentFile import os # 创建分类 cat_phone Category.objects.create(name智能手机, slugsmartphone) cat_laptop Category.objects.create(name笔记本电脑, sluglaptop) # 为每类添加 3 个商品模拟真实 SKU for i in range(1, 4): Product.objects.create( namefiPhone {15i} Pro, descriptionf苹果最新旗舰{i}TB 存储, price7999.00 i * 1000, stock50 i * 10, categorycat_phone ) Product.objects.create( namefMacBook Pro M3, descriptionf搭载 M3 芯片{16i}GB 内存, price12999.00 i * 500, stock30 i * 5, categorycat_laptop ) print(✅ 商品数据填充完成共, Product.objects.count(), 条)血泪经验ContentFile用于模拟上传图片如product.image.save(iphone.jpg, ContentFile(bfake_image_data), saveTrue)但开发阶段可先留空image models.ImageField(upload_toproducts/, blankTrue)等前端联调时再补。过早处理文件存储会陷入MEDIA_ROOT权限、Nginx 静态服务等无关问题。4. 购物车与订单核心逻辑用 Session 实现无登录购物用事务保障库存安全4.1 购物车为什么不用数据库表Session 的三个不可替代优势新手常问“购物车数据不该存数据库吗”答案是登录前必须用 Session登录后可同步到数据库。原因无登录态也能用用户未注册就加购体验不能断天然隔离每个浏览器 Session ID 不同A 用户加的购物车B 用户绝对看不到自动过期SESSION_COOKIE_AGE 12096002 周用户两周不访问购物车自动清空省去定时清理脚本。Django Session 默认用数据库存储无需额外配置# cart/utils.py —— 购物车操作封装 from django.conf import settings from decimal import Decimal class Cart: def __init__(self, request): self.session request.session cart self.session.get(settings.CART_SESSION_ID) if not cart: cart self.session[settings.CART_SESSION_ID] {} self.cart cart def add(self, product, quantity1, override_quantityFalse): product_id str(product.id) if product_id not in self.cart: self.cart[product_id] {quantity: 0, price: str(product.price)} if override_quantity: self.cart[product_id][quantity] quantity else: self.cart[product_id][quantity] quantity self.save() def save(self): self.session.modified True def __iter__(self): product_ids self.cart.keys() products Product.objects.filter(id__inproduct_ids) for product in products: self.cart[str(product.id)][product] product for item in self.cart.values(): item[price] Decimal(item[price]) item[total_price] item[price] * item[quantity] yield item def get_total_price(self): return sum(Decimal(item[price]) * item[quantity] for item in self.cart.values())# mall/settings.py —— 定义购物车 Session Key CART_SESSION_ID cart# cart/views.py from django.shortcuts import render, redirect, get_object_or_404 from django.views.decorators.http import require_POST from product.models import Product from .utils import Cart require_POST def cart_add(request, product_id): cart Cart(request) product get_object_or_404(Product, idproduct_id) cart.add(productproduct, quantity1, override_quantityFalse) return redirect(cart_detail) def cart_detail(request): cart Cart(request) return render(request, cart/detail.html, {cart: cart})此时/cart/add/1/即可将商品 ID1 加入购物车/cart/显示列表。关键点require_POST防止 GET 请求误触发加购get_object_or_404保证商品存在否则返回 404 而非 500。4.2 下单接口用select_for_update()锁住库存避免超卖这是商城最核心、最容易翻车的环节。常见错误写法# ❌ 错误先查库存再扣减中间有时间差 stock product.stock if stock quantity: product.stock - quantity product.save() # 此时另一请求可能已读到旧 stock 值正确做法在数据库层面加行锁确保“读-改-写”原子性# order/views.py from django.db import transaction from django.http import JsonResponse from django.contrib.auth.decorators import login_required from product.models import Product from .models import Order, OrderItem login_required transaction.atomic def create_order(request): cart Cart(request) if not cart.cart: return JsonResponse({error: 购物车为空}, status400) # 创建订单主表 order Order.objects.create( userrequest.user, total_amountcart.get_total_price(), statuspending ) # 遍历购物车为每个商品创建子订单项并扣减库存 for item in cart: product item[product] quantity item[quantity] # 关键用 select_for_update() 锁住该商品行 try: locked_product Product.objects.select_for_update().get(idproduct.id) except Product.DoesNotExist: raise Exception(f商品 {product.id} 不存在) if locked_product.stock quantity: raise Exception(f商品 {product.name} 库存不足仅剩 {locked_product.stock}) # 扣减库存在锁住的行上操作 locked_product.stock - quantity locked_product.save() # 创建订单项 OrderItem.objects.create( orderorder, productproduct, quantityquantity, priceitem[price] ) # 清空购物车 cart.clear() return JsonResponse({ success: True, order_id: order.id, redirect_url: /order/success/ })提示transaction.atomic保证整个函数内所有 DB 操作在同一个事务中select_for_update()在 PostgreSQL/MySQL 上生效SQLite 不支持开发时务必切到 MySQL 或 PostgreSQLpip install mysqlclient否则锁无效。4.3 订单状态机为什么status字段必须用 ChoiceField 而非字符串订单状态不是随意写的字符串而是有严格流转规则pending→paid→shipped→delivered禁止paid→pending或shipped→paid。用choices强制约束# order/models.py class Order(models.Model): STATUS_CHOICES [ (pending, 待支付), (paid, 已支付), (shipped, 已发货), (delivered, 已签收), (cancelled, 已取消), ] user models.ForeignKey(User, on_deletemodels.CASCADE) total_amount models.DecimalField(max_digits10, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(auto_now_addTrue) def can_cancel(self): return self.status in [pending, paid] def cancel(self): if not self.can_cancel(): raise ValueError(订单不可取消) # 退款逻辑、库存回滚等在此处实现 self.status cancelled self.save()Admin 后台编辑时status会自动变成下拉框且只能选预设值。这是防止脏数据的第一道防线。5. 避坑指南上线前必须排查的 5 个高频翻车点5.1 现象首页打开空白浏览器控制台报GET http://127.0.0.1:8000/static/css/main.css net::ERR_ABORTED 404原因Django 开发模式下静态文件CSS/JS需通过django.contrib.staticfiles收集但DEBUGTrue时runserver会自动提供/static/路由一旦DEBUGFalse就必须用collectstatic并配置 Web 服务器Nginx来服务静态文件。新手常忽略DEBUG切换带来的行为差异。解决开发阶段确保DEBUGTrue部署前运行python manage.py collectstatic --noinput并将STATIC_ROOT BASE_DIR / staticfiles再让 Nginx 指向该目录。5.2 现象用户注册后无法登录Admin 后台显示密码为明文如pbkdf2_sha256$...未加密原因未使用UserCreationForm或create_user()方法而是直接User.objects.create(username..., password...)导致密码未哈希。Django 的authenticate()函数只会比对哈希值明文密码永远失败。解决注册视图中必须用form.save()UserCreationForm已重写save方法或手动调用User.objects.create_user(username, email, password)。5.3 现象并发下单时日志显示DatabaseError: could not serialize access due to read/write dependencies among transactions原因PostgreSQL 的可序列化隔离级别下select_for_update()在高并发时可能触发序列化失败。这不是代码 bug而是数据库主动拒绝不安全的并发。解决捕获异常并重试最多 3 次而非抛出 500。在create_order视图中加from django.db import transaction, DatabaseError import time for attempt in range(3): try: with transaction.atomic(): # 原下单逻辑 break except DatabaseError as e: if could not serialize access in str(e) and attempt 2: time.sleep(0.1 * (2 ** attempt)) # 指数退避 continue else: raise5.4 现象商品详情页 URL 是/product/1/但 SEO 要求为/product/iphone-15-pro/原因Django URL 路由默认按 ID 匹配但slug字段更友好。若只在urls.py写path(product/int:pk/, ...)无法匹配字符串。解决在Product模型中加get_absolute_url()方法并在urls.py改用slug# product/models.py def get_absolute_url(self): return reverse(product_detail, kwargs{slug: self.slug}) # product/urls.py path(product/slug:slug/, views.product_detail, nameproduct_detail)同时确保slug字段在保存时自动生成用django.utils.text.slugify。5.5 现象管理员在 Admin 后台修改商品价格前端商品列表仍显示旧价格刷新也不变原因浏览器缓存了 HTML或 Django 模板未启用cache模板标签但更可能是QuerySet被意外缓存。Django 的QuerySet有惰性求值特性若在视图中写products Product.objects.all()然后多次遍历products第二次遍历会复用第一次结果即使数据库已改。解决强制重新查询或用list()转为列表。更彻底的是在settings.py中关闭模板缓存开发阶段TEMPLATES [{ BACKEND: django.template.backends.django.DjangoTemplates, OPTIONS: { debug: True, # ← 开启 debug 模式禁用模板缓存 } }]6. 进阶技巧用 Django Debug Toolbar 实时观测 SQL、缓存、信号把黑匣子变成透明流水线6.1 为什么 Debug Toolbar 比print()和日志更高效当你怀疑“为什么这个页面慢”print(time.time())只能告诉你总耗时日志里INFO级别又太泛。Debug Toolbar 直接在浏览器底部弹出面板分 Tab 显示SQL列出本次请求执行的所有 SQL标红慢查询 100ms点击可看执行计划EXPLAINCache显示命中/未命中次数以及cache.set()/cache.get()的 keySignals列出触发的信号如post_save确认UserProfile是否真的被创建Templates显示渲染了哪些模板、上下文变量值、继承关系。这才是真正“所见即所得”的调试。6.2 三步接入 Debug Toolbar兼容 Django 4.2pip install django-debug-toolbar# mall/settings.py —— 仅开发环境启用 if DEBUG: INSTALLED_APPS [debug_toolbar] MIDDLEWARE [debug_toolbar.middleware.DebugToolbarMiddleware] INTERNAL_IPS [127.0.0.1]# mall/urls.py —— 仅开发环境挂载 if settings.DEBUG: import debug_toolbar urlpatterns [ path(__debug__/, include(debug_toolbar.urls)), ] urlpatterns重启服务打开任意页面右下角会出现 图标。点击展开就能看到实时数据。6.3 用 Toolbar 定位“库存扣减失效”的真实原因真实案例复盘某次压测发现100 个并发请求下单最终库存只扣了 92丢失 8 次。用 Toolbar 的 SQL Tab 发现92 个请求执行了UPDATE product_product SET stock %s WHERE product_product.id %s8 个请求执行了SELECT ... FOR UPDATE但没后续UPDATE。进一步看ViewsTab发现这 8 个请求的视图函数抛出了Product.DoesNotExist异常原因是select_for_update()后另一请求已删除该商品极小概率而代码没做try/except捕获。于是补上try: locked_product Product.objects.select_for_update().get(idproduct.id) except Product.DoesNotExist: # 商品已被其他请求删除跳过此商品 continue没有 Toolbar你得在日志里 grep 8 行Product.DoesNotExist再关联到具体请求 ID耗时 2 小时有了 Toolbar3 分钟定位。我的习惯是每次写完一个新功能如购物车结算必开 Toolbar 跑一遍盯着 SQL 数量是否 N1、Cache 命中率是否该缓存没缓存、Signals是否该触发没触发。它不帮你写代码但让你一眼看清代码在数据库、缓存、信号层的真实行为。这种确定性是靠猜和日志永远换不来的。希望帮到你。本文还有配套的精品资源点击获取