
简介本资源是面向Python后端开发者与Django初学者的电商项目实战学习包聚焦「天天生鲜」线上商城系统开发全流程覆盖Web框架应用、异步任务调度、分布式文件存储及高性能缓存等核心后端能力。压缩包共376个文件含47个核心Python源码含models、views、celery配置及redis集成逻辑、70个HTML模板页、104张PNG与38张JPG界面截图、16个JS交互脚本及12个CSS样式文件辅以docx教程文档与sql数据库脚本整体26.32MB结构清晰便于按模块研读源码与对照笔记。已有805人学习下载资源提供完整可运行的Django项目骨架包含用户认证、商品管理、购物车、订单流程、FastDFS图片上传集成、Celery异步邮件通知及Redis缓存优化等真实场景实现配套笔记详述各模块设计思路与关键配置是掌握电商系统架构与Django工程化实践的优质入门范例。1. 为什么一个“天天生鲜”Django商城项目比官方教程更能教会你电商后端的真实逻辑你写过python manage.py runserver也照着文档配过settings.py但真要从零搭一个能上线的生鲜商城——用户要能注册、商品要能分类展示、购物车得实时更新、订单生成后库存得扣减、图片得上传到独立存储、下单成功还得发邮件通知……这时候你会发现Django 的auth模块不是开箱即用Model关系设计稍有偏差就会卡在支付回调校验Redis缓存键命名不带业务前缀会导致后台管理页和前台商品页互相污染而Celery任务失败后连日志都找不到在哪查。这个“天天生鲜”项目不是玩具 Demo它是一套完整跑通的生产级骨架用户中心用AbstractUser扩展了手机号登录商品 SKU 和 SPU 分离建模支撑多规格如苹果按“500g/箱”“1kg/箱”购物车数据既存session又落Redis实现登录态同步订单号用datetime random拼接避免碰撞FastDFS客户端封装成FdfsStorage类直接挂载到DEFAULT_FILE_STORAGE。它不教你怎么“学会 Django”而是让你看清——当流量进来时请求怎么穿过中间件、视图怎么调用服务层、事务怎么在select_for_update和atomic之间取舍、缓存失效策略怎么配合库存扣减。适合刚写完博客系统的 Python 后端也适合想补全电商链路认知的 5 年开发者。2. MTV 架构落地从模型设计到视图分层拆解天天生鲜的 5 大核心模块2.1 商品与库存模型SPU/SKU 分离设计与事务安全扣减天天生鲜没有把“苹果”直接建一张表而是拆成GoodsType水果、GoodsSPU红富士苹果、GoodsSKU红富士苹果-500g装-12.8、GoodsImage多张详情图。这种设计让同一款商品支持不同规格、价格、库存且图片可独立管理。关键在于GoodsSKU模型中stock字段的更新逻辑# apps/goods/models.py class GoodsSKU(models.Model): stock models.IntegerField(default0) sales models.IntegerField(default0) def reduce_stock(self, num): 原子性扣减库存防止超卖 from django.db import transaction with transaction.atomic(): # 加锁确保并发安全 sku GoodsSKU.objects.select_for_update().get(idself.id) if sku.stock num: raise ValueError(库存不足) sku.stock - num sku.sales num sku.save()提示select_for_update()必须在atomic块内使用否则锁会立即释放若用F表达式如stockF(stock)-num虽快但无法判断扣减后是否为负需额外filter(stock__gtenum)验证而验证与更新非原子操作高并发下仍有超卖风险。GoodsType表还关联了goods_type_id到GoodsSPU实现品类聚合GoodsImage通过sku外键绑定支持同一 SKU 多图轮播。迁移命令执行后数据库生成 4 张表其中goods_sku是实际交易主体goods_spu仅用于前端分类页聚合展示。2.2 用户认证扩展手机号登录短信验证码的完整链路Django 默认User模型只支持用户名密码天天生鲜继承AbstractUser新建User模型并添加mobile字段和is_delete软删除标记# apps/user/models.py class User(AbstractUser): mobile models.CharField(max_length11, uniqueTrue, nullTrue, blankTrue) is_delete models.BooleanField(defaultFalse) class Meta: db_table df_user verbose_name 用户 verbose_name_plural verbose_name登录视图不再调用authenticate(username..., password...)而是先查User.objects.filter(mobilemobile)再校验密码# apps/user/views.py def login(request): if request.method POST: mobile request.POST.get(mobile) pwd request.POST.get(pwd) user User.objects.filter(mobilemobile).first() if user and user.check_password(pwd): login(request, user) # Django 内置 login 函数 return redirect(reverse(goods:index))注意mobile字段设为uniqueTrue但允许nullTrue兼容老用户无手机号场景is_delete替代物理删除所有查询需加.filter(is_deleteFalse)Admin 后台重写get_queryset()方法自动过滤。短信验证码未集成第三方 SDK而是用django.core.mail模拟发送开发环境生产环境替换为阿里云 SMS 接口参数结构严格匹配send_sms(mobile, template_code, data)data字典传入{code: 123456}渲染模板。2.3 购物车双存储机制Session 与 Redis 的协同策略购物车数据不只存在request.session更同步写入Redis解决用户登录前后购物车合并问题# utils/cart.py def merge_cart_cookie_to_redis(request, user): 登录时合并 cookie 购物车到 redis cart_str request.COOKIES.get(cart) if not cart_str: return cart_dict json.loads(cart_str) # cart_dict 格式{sku_id: count} conn get_redis_connection(default) cart_key fcart_{user.id} pl conn.pipeline() for sku_id, count in cart_dict.items(): pl.hset(cart_key, sku_id, count) pl.execute()Redis中购物车以hash结构存储key为cart_{user_id}field为sku_idvalue为数量。未登录用户购物车存于cookiemax_age3600登录后触发merge_cart_cookie_to_redis并清空cookie。前端 JS 通过/cart/add/接口操作购物车接口内根据request.user.is_authenticated自动路由到Redis或session存储逻辑。2.4 订单创建与状态机从草稿到支付成功的全流程控制订单模型OrderInfo包含order_id唯一字符串、pay_method1 支付宝/2 微信、status1 待支付/2 待发货/3 已发货/4 完成/5 已关闭、total_count、total_price等字段。创建订单核心逻辑在OrderPlaceView.post()# apps/order/views.py def post(self, request): # 1. 获取用户选择的地址和商品 addr_id request.POST.get(addr_id) pay_method request.POST.get(pay_method) sku_ids request.POST.getlist(sku_ids) # 2. 校验库存逐个 SKU 扣减 for sku_id in sku_ids: try: sku GoodsSKU.objects.get(idsku_id) sku.reduce_stock(1) # 调用前面定义的原子扣减方法 except ValueError as e: return JsonResponse({res: 2, errmsg: str(e)}) # 3. 创建订单主记录 order_id datetime.now().strftime(%Y%m%d%H%M%S) %06d % user.id order OrderInfo.objects.create( order_idorder_id, useruser, addr_idaddr_id, pay_methodpay_method, total_counttotal_count, total_pricetotal_price ) # 4. 创建订单商品明细 for sku_id in sku_ids: sku GoodsSKU.objects.get(idsku_id) OrderGoods.objects.create( orderorder, skusku, count1, pricesku.price )提示订单创建必须在单次事务中完成否则出现“订单生成但库存未扣减”或“库存扣减但订单未生成”的数据不一致。此处虽未显式atomic但OrderInfo.objects.create()和后续OrderGoods.objects.create()在同一 HTTP 请求内需确保整个post()方法被transaction.atomic装饰否则高并发下仍可能出错。3. 第三方组件集成Celery 异步任务、FastDFS 文件上传与 Redis 缓存实战3.1 Celery 配置与邮件异步发送避免阻塞主流程项目使用Celery Redis作为消息中间件celery.py配置文件位于项目根目录# celery.py from __future__ import absolute_import, unicode_literals import os from celery import Celery os.environ.setdefault(DJANGO_SETTINGS_MODULE, dailyfresh.settings) app Celery(dailyfresh) app.config_from_object(django.conf:settings, namespaceCELERY) app.autodiscover_tasks()邮件发送任务定义在apps/user/tasks.py# apps/user/tasks.py from celery import shared_task from django.core.mail import send_mail from django.conf import settings shared_task def send_register_active_email(to_email, username, token): 发送激活邮件 subject 天天生鲜欢迎信息 message sender settings.EMAIL_FROM receiver [to_email] html_message h1%s, 欢迎注册天天生鲜/h1请点击以下链接激活账户br/a hrefhttp://127.0.0.1:8000/user/active/%shttp://127.0.0.1:8000/user/active/%s/a % (username, token, token) send_mail(subject, message, sender, receiver, html_messagehtml_message)视图中调用方式为send_register_active_email.delay(to_email, username, token).delay()触发异步执行。启动 Celery worker 命令为celery -A dailyfresh worker -l info注意-A参数指定应用模块名即celery.py所在包-l info输出日志级别若报错Connection refused检查Redis是否运行redis-server及settings.py中CELERY_BROKER_URL redis://127.0.0.1:6379/1地址是否正确。3.2 FastDFS 文件上传自定义 Storage 类对接 Django 文件系统项目未使用 Django 默认FileSystemStorage而是封装FdfsStorage类使ImageField自动上传至 FastDFS# utils/fastdfs/fdfs_storage.py from django.core.files.storage import Storage from fdfs_client.client import Fdfs_client class FdfsStorage(Storage): def __init__(self, client_confNone, base_urlNone): if client_conf is None: client_conf settings.FDFS_CLIENT_CONF if base_url is None: base_url settings.FDFS_URL self.client_conf client_conf self.base_url base_url def _open(self, name, moderb): pass def _save(self, name, content, max_lengthNone): client Fdfs_client(self.client_conf) res client.upload_by_buffer(content.read()) if res.get(Status) ! Upload successed.: raise Exception(上传失败) filename res.get(Remote file_id) return filename def url(self, name): return self.base_url name在settings.py中配置# settings.py FDFS_CLIENT_CONF os.path.join(BASE_DIR, utils/fastdfs/client.conf) FDFS_URL http://192.168.200.133/ DEFAULT_FILE_STORAGE utils.fastdfs.fdfs_storage.FdfsStorageclient.conf文件需指定tracker_server地址FDFS_URL为 Nginx 访问地址。商品图片字段定义为# apps/goods/models.py class GoodsImage(models.Model): sku models.ForeignKey(GoodsSKU, on_deletemodels.CASCADE) image models.ImageField(upload_togoods, verbose_name图片路径)upload_togoods仅作路径前缀实际存储由FdfsStorage._save()决定返回值为 FastDFS 返回的group1/M00/00/00/wKgZhVnZQz-AJkEFAABaXqJvGcI032.jpg这类 IDurl()方法拼接FDFS_URL即可生成可访问 URL。3.3 Redis 缓存策略首页商品分类与热门商品的分级缓存首页index视图对商品分类和热门商品做两级缓存一级缓存RedisTTL3600二级缓存Django的LocMemCache仅开发环境# apps/goods/views.py def index(request): # 尝试从 Redis 获取缓存 conn get_redis_connection(default) category_list conn.get(index_category_list) if category_list: category_list json.loads(category_list) else: # 查询数据库 category_list GoodsType.objects.all() # 序列化并存入 Redis conn.setex(index_category_list, 3600, json.dumps([{ id: c.id, name: c.name, logo: c.logo.url if c.logo else } for c in category_list]))热门商品缓存键为index_goods_list_{type_id}每个品类单独缓存避免全量刷新。缓存更新时机在GoodsType模型保存后触发信号# apps/goods/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from .models import GoodsType receiver(post_save, senderGoodsType) def clear_index_cache(sender, instance, **kwargs): conn get_redis_connection(default) conn.delete(index_category_list) conn.delete(findex_goods_list_{instance.id})提示post_save信号在save()方法提交后触发确保数据库已写入delete()操作是主动失效而非更新缓存符合“缓存一致性”中“先删缓存再更新 DB”的推荐模式本例 DB 更新即save()故删缓存放最后。4. 部署与调试Nginx Gunicorn Supervisor 上线配置与常见错误排查4.1 Gunicorn 启动配置进程数、超时与日志分离项目使用Gunicorn替代runservergunicorn.conf.py配置如下# gunicorn.conf.py import multiprocessing bind 127.0.0.1:8000 bind_network 127.0.0.1:8000 workers multiprocessing.cpu_count() * 2 1 worker_class sync worker_connections 1000 timeout 30 keepalive 2 max_requests 1000 max_requests_jitter 100 # 日志 accesslog /var/log/dailyfresh/access.log errorlog /var/log/dailyfresh/error.log loglevel info启动命令为gunicorn -c gunicorn.conf.py dailyfresh.wsgi:applicationworkers设为CPU 核数×21是经验公式避免 I/O 阻塞timeout30防止长连接耗尽资源accesslog和errorlog必须提前创建目录并赋权chown www-data:www-data /var/log/dailyfresh。4.2 Nginx 反向代理与静态文件处理分离动态与静态请求Nginx配置将/static/和/media/直接映射到文件系统其余请求转发给 Gunicorn# /etc/nginx/sites-available/dailyfresh upstream dailyfresh { server 127.0.0.1:8000; } server { listen 80; server_name www.tiantianshengxian.com; location /static/ { alias /home/www/dailyfresh/static/; expires 1y; add_header Cache-Control public, immutable; } location /media/ { alias /home/www/dailyfresh/media/; expires 1y; } location / { proxy_pass http://dailyfresh; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意alias末尾必须带/否则路径拼接错误expires 1y对静态资源启用强缓存proxy_set_header三行确保 Django 能正确获取客户端 IP 和域名。4.3 Supervisor 进程守护自动重启与日志轮转supervisord配置文件/etc/supervisor/conf.d/dailyfresh.conf[program:dailyfresh] command/home/www/dailyfresh/venv/bin/gunicorn -c /home/www/dailyfresh/gunicorn.conf.py dailyfresh.wsgi:application directory/home/www/dailyfresh userwww-data autostarttrue autorestarttrue redirect_stderrtrue stdout_logfile/var/log/dailyfresh/gunicorn.log stdout_logfile_maxbytes10MB stdout_logfile_backups10生效命令sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start dailyfresh若supervisorctl status显示FATAL检查command中虚拟环境路径是否正确directory是否为项目根目录user是否有权限读取gunicorn.conf.py。4.4 常见错误定位从 502 Bad Gateway 到 Redis 连接拒绝502 Bad GatewayNginx 无法连接 Gunicorn。先ps aux | grep gunicorn看进程是否存在再netstat -tuln | grep :8000确认端口监听最后检查gunicorn.conf.py中bind地址是否与 Nginxproxy_pass一致。Redis Connection ErrorDjango 报ConnectionRefusedError。执行redis-cli ping若返回PONG则服务正常否则sudo systemctl start redis-server检查settings.py中CACHES和CELERY_BROKER_URL的host和port是否与redis.conf一致。Static files not foundNginx 日志报open() /home/www/dailyfresh/static/css/base.css failed (2: No such file or directory)。确认python manage.py collectstatic --noinput已执行且STATIC_ROOT指向/home/www/dailyfresh/static/检查 Nginxalias路径末尾/是否遗漏。Celery task not executedWorker 日志无输出。执行celery -A dailyfresh inspect active_queues查看队列是否注册确认tasks.py中shared_task装饰器存在且模块路径在INSTALLED_APPS中已注册。5. 性能优化技巧利用 Redis Pipeline 批量查询商品库存与价格5.1 购物车页面加载优化单次 Pipeline 替代多次 Redis 查询购物车页面需显示每个 SKU 的名称、价格、库存若对每个 SKU 调用hget()会引发 N1 查询。天天生鲜改用Pipeline批量获取# apps/cart/views.py def cart_show(request): user request.user conn get_redis_connection(default) cart_key fcart_{user.id} # 获取所有 sku_id 和 count cart_dict conn.hgetall(cart_key) # {b1: b2, b3: b1} skus [] total_price 0 for sku_id_bytes, count_bytes in cart_dict.items(): sku_id int(sku_id_bytes) count int(count_bytes) # Pipeline 批量查询 pl conn.pipeline() pl.hget(fsku_info_{sku_id}, name) pl.hget(fsku_info_{sku_id}, price) pl.hget(fsku_info_{sku_id}, image) result pl.execute() # 一次网络往返获取全部字段 sku { id: sku_id, name: result[0].decode() if result[0] else , price: Decimal(result[1].decode()) if result[1] else 0, image: result[2].decode() if result[2] else , count: count } skus.append(sku) total_price sku[price] * count context {skus: skus, total_price: total_price} return render(request, cart.html, context)pl.execute()将三条hget命令打包发送网络开销从 3 次降为 1 次。sku_info_{id}是预热缓存的 hash 结构由后台定时任务每日凌晨更新# apps/goods/management/commands/preload_sku_cache.py from django.core.management.base import BaseCommand from apps.goods.models import GoodsSKU from django_redis import get_redis_connection class Command(BaseCommand): def handle(self, *args, **options): conn get_redis_connection(default) for sku in GoodsSKU.objects.all(): conn.hmset(fsku_info_{sku.id}, { name: sku.name, price: str(sku.price), image: sku.image.url if sku.image else }) conn.expire(fsku_info_{sku.id}, 86400) # 24小时提示hmset已废弃新版本用hset但hset(name, mapping)语法兼容expire必须在hmset后立即调用否则缓存永不过期预热脚本通过python manage.py preload_sku_cache手动触发或用crontab每日 2:00 执行。5.2 数据库查询优化select_related 与 prefetch_related 的精准使用商品详情页需展示GoodsSKU、其所属GoodsSPU、GoodsType及多张GoodsImage。若用默认查询# 错误N1 查询 sku GoodsSKU.objects.get(idsku_id) # 1次 spu sku.spu # 第2次 goods_type spu.type # 第3次 images sku.goodsimage_set.all() # 第4次且每张图再查1次正确写法为# 正确1次查询 sku GoodsSKU.objects.select_related(spu__type).prefetch_related(goodsimage_set).get(idsku_id) # select_related 用于 ForeignKeyspu 和 spu.type 是外键链 # prefetch_related 用于 ManyToMany 和 reverse ForeignKeygoodsimage_set 是反向外键生成 SQL 仅 2 条一条JOIN查询sku、spu、goods_type另一条IN查询所有关联图片。select_related最多支持两级外键spu__type三级需拆分prefetch_related不限层级但会多一次查询。5.3 缓存穿透防护空值缓存与布隆过滤器的轻量实现用户恶意请求不存在的sku_id9999999每次都会穿透到数据库。天天生鲜采用空值缓存# utils/cache.py def get_sku_or_none(sku_id): conn get_redis_connection(default) cache_key fsku_{sku_id} sku_data conn.get(cache_key) if sku_data is not None: return json.loads(sku_data) if sku_data ! b else None try: sku GoodsSKU.objects.get(idsku_id) conn.setex(cache_key, 3600, json.dumps({ id: sku.id, name: sku.name, price: str(sku.price), stock: sku.stock })) return sku except GoodsSKU.DoesNotExist: # 缓存空值防止穿透 conn.setex(cache_key, 300, ) # 5分钟空缓存 return Nonecache_key值为空字符串表示该 SKU 不存在setex设置 5 分钟过期既防穿透又避免永久缓存错误。布隆过滤器未引入第三方库因项目规模小空值缓存已足够应对常规攻击。本文还有配套的精品资源点击获取