简介:这是一份基于Django框架开发的Python全栈实战项目——手机商城,面向具备基础Python和Web开发能力的学习者,适用于课程设计、毕业设计或全栈技能进阶训练。项目采用前后端分离架构:后端使用Django构建RESTful数据管理接口与后台管理系统,前端为微信小程序,专注商品展示、浏览与交互逻辑,完整覆盖用户端核心业务流程。压缩包共102个文件,含16个Python后端逻辑文件、13个JS/WXML/WXSS小程序源码、13个JSON配置与API响应示例、11个PNG/JPG商品图及4个HTML管理页面模板,整体仅497KB,轻量易部署。已有154人学习下载,资源结构清晰,包含可直接运行的Django项目骨架、小程序源码、SQLite本地数据库及基础商品数据,附带README说明与关键页面(如shop_phones.html、phone_save.html)实现参考,便于快速理解模块划分与前后端联调要点。
1. 这不是又一个 Django 教程 Demo:它是一套能跑通「用户注册→商品浏览→下单支付→后台管理」全链路的手机商城实战项目,专为想摆脱“Hello World”却卡在“真实业务怎么组织”的 Python 全栈学习者准备
你肯定见过太多 Django 博客、待办清单、投票系统——它们教路由、模型、模板,但一到真实项目就断层:用户密码怎么安全存?商品图怎么批量上传又不崩服务器?订单状态流转怎么防重复提交?支付回调怎么验签又不被重放?这个手机商城项目不是概念演示,而是一线工程师从某高校电商实训平台剥离出的可运行骨架。它包含完整的前后端分离结构(Django REST Framework + Vue3 前端已预留接口)、JWT 认证体系、基于 Celery 的异步邮件通知、Redis 缓存热门商品、支付宝沙箱支付对接(含验签逻辑),甚至预置了 Nginx + Gunicorn 生产部署配置模板。它不教你“什么是 MTV”,而是直接让你看到models.py里Order模型如何用DecimalField避免浮点数精度坑,views.py中OrderCreateView怎么用select_for_update()锁库存,以及celery_tasks.py如何把发短信这种耗时操作从 HTTP 请求中摘出来。如果你已经写过 3 个以上 Django 小项目,但每次搭新项目都得重新翻文档查信号怎么监听、事务怎么回滚、静态文件怎么分环境打包——这份资源就是你的“工程化后悔药”。
2. 项目结构与核心模块拆解:从 manage.py 到 celery_tasks.py,每一层都在解决真实业务中的耦合痛点
2.1 为什么选 Django REST Framework 而非纯 Django 模板?——前后端分离不是口号,是应对多端迭代的刚需
这个项目没有用 Django 内置模板渲染 HTML,而是将api/目录作为唯一后端入口,所有接口返回 JSON。原因很实际:某高校实训中,前端团队用 Vue3 开发 H5 商城页,同时还要输出小程序版和后台管理页(Ant Design Pro),如果后端混用模板,每个新端都要改视图逻辑、重写 URL 分发、调试 CSRF 问题。而 DRF 提供了标准化的序列化器(serializers.py)、视图集(viewsets.py)和权限控制(permissions.py),让同一套业务逻辑能被不同前端复用。比如商品列表接口/api/products/,Vue3 调用时传?category=smartphone&sort=price_asc,小程序调用时加&platform=miniprogram,后端只需在ProductListView的get_queryset()中解析参数,无需新增视图类。更关键的是,DRF 的APIView类天然支持@action装饰器,像/api/orders/{id}/cancel/这种非 CRUD 操作,不用硬塞进update()方法里做 if-else 分支。
提示:项目
settings/base.py中已禁用django.contrib.sessions,改用 JWT 存储用户登录态,避免 SessionID Cookie 在跨域请求中被浏览器拦截。JWT 密钥通过环境变量SECRET_KEY注入,生产环境必须替换。
2.2 用户模块:密码安全不是加 salt 就完事,它涉及哈希算法选型、字段校验、第三方登录预留
users/models.py中的CustomUser模型继承自AbstractBaseUser,而非AbstractUser。这不是炫技——AbstractUser自带username字段,而手机商城要求手机号注册,强制保留username会导致冗余字段和迁移混乱。关键改动有三处:
第一,password字段使用make_password()时指定hasher='pbkdf2_sha256'(Django 默认),但项目在settings.py中额外启用了argon2支持(需安装django-argon2),并在AUTH_PASSWORD_VALIDATORS中配置了最小长度、数字、特殊字符校验;
第二,phone_number字段用正则^1[3-9]\d{9}$校验中国大陆手机号,且在save()方法中自动去空格、去短横线,避免用户输138-1234-5678导致后续短信发送失败;
第三,UserProfile扩展模型中avatar字段关联ImageField,但上传路径动态生成为avatars/{year}/{month}/{uid}_{timestamp}.jpg,防止海量头像挤爆单目录 inode。
# users/models.py 关键片段 class CustomUser(AbstractBaseUser): phone_number = models.CharField( max_length=11, unique=True, validators=[RegexValidator(r'^1[3-9]\d{9}$', '手机号格式错误')] ) # ...其他字段 def save(self, *args, **kwargs): self.phone_number = re.sub(r'[-\s]', '', self.phone_number) # 清洗输入 super().save(*args, **kwargs)这段代码确保phone_number字段永远是纯数字字符串,后续调用短信 SDK 时无需再做清洗,避免因格式问题导致发送失败却被记为“用户不存在”。
2.3 商品与订单模块:状态机驱动而非 if-else 堆砌,用 Django Signals 解耦核心流程
products/models.py中Product模型包含stock(库存)、is_on_sale(是否上架)、sales_count(销量)三个关键字段。但真正体现工程思维的是orders/models.py中的Order状态流转设计:它没有用status = models.CharField(choices=...)然后在视图里写一堆if status == 'paid': do_xxx,而是采用状态机模式,定义PENDING,PAID,SHIPPED,COMPLETED,CANCELLED五种状态,并在Order模型中内置transition_to_paid(),transition_to_shipped()等方法。这些方法内部调用self.status = new_status并触发post_save信号。
# orders/models.py class Order(models.Model): PENDING = 'pending' PAID = 'paid' SHIPPED = 'shipped' COMPLETED = 'completed' CANCELLED = 'cancelled' STATUS_CHOICES = [ (PENDING, '待支付'), (PAID, '已支付'), (SHIPPED, '已发货'), (COMPLETED, '已完成'), (CANCELLED, '已取消'), ] status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=PENDING) def transition_to_paid(self): if self.status != self.PENDING: raise ValidationError(f"订单状态非法:当前为 {self.get_status_display()},无法转为已支付") with transaction.atomic(): # 锁定商品库存,防止超卖 product = self.items.first().product product = Product.objects.select_for_update().get(id=product.id) if product.stock < self.items.aggregate(total=Sum('quantity'))['total']: raise ValidationError("库存不足") product.stock -= self.items.aggregate(total=Sum('quantity'))['total'] product.sales_count += self.items.aggregate(total=Sum('quantity'))['total'] product.save() self.status = self.PAID self.save() # 发送支付成功信号,触发后续动作 order_paid.send(sender=self.__class__, order=self)这里select_for_update()是数据库行级锁,保证并发下单时库存扣减不超卖;order_paid.send()是 Django Signal,解耦了“支付成功”这一事件与“发邮件通知”“更新销量统计”“推送消息”等下游动作——这些逻辑全部放在orders/signals.py中监听,而不是塞进transition_to_paid()方法里。当某天需要增加“支付成功后同步到 ERP 系统”,只需新增一个 signal receiver,无需修改订单核心逻辑。
2.4 支付模块:沙箱环境不是摆设,验签逻辑必须手写,否则上线即翻车
项目payments/views.py实现了支付宝沙箱支付对接,但重点不在调用alipay.api_alipay_trade_page_pay(),而在回调验签。很多教程直接跳过这一步,结果上线后被恶意伪造回调刷单。本项目AlipayNotifyView中,收到支付宝 POST 回调后,先用verify_and_decode_alipay_notify()函数验证签名:
# payments/utils.py def verify_and_decode_alipay_notify(data: dict, alipay_public_key: str) -> bool: """ 验证支付宝回调签名并解码数据 data: request.POST.dict() 获取的原始字典 alipay_public_key: 从支付宝开放平台下载的公钥(PEM 格式) """ # 1. 过滤掉 sign 和 sign_type 字段 params = {k: v for k, v in data.items() if k not in ['sign', 'sign_type']} # 2. 按 key 字典序排序,拼接成 query string sorted_params = '&'.join([f'{k}={v}' for k, v in sorted(params.items())]) # 3. 用支付宝公钥验签 try: from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 from Crypto.Hash import SHA256 key = RSA.importKey(alipay_public_key) signer = PKCS1_v1_5.new(key) digest = SHA256.new(sorted_params.encode('utf-8')) return signer.verify(digest, base64.b64decode(data['sign'])) except Exception as e: logger.error(f"支付宝验签失败: {e}") return False这段代码严格遵循支付宝官方文档的验签步骤:过滤签名字段 → 字典序拼接 → SHA256 哈希 → RSA 公钥验签。alipay_public_key从settings.py的ALIPAY_PUBLIC_KEY变量读取,该变量指向keys/alipay_public_key.pem文件内容。如果漏掉字典序排序或用错哈希算法,验签必然失败——这是血泪经验:某次部署时因sorted_params拼接少了=符号,导致所有回调都被拒,订单状态卡在“待支付”,客服电话被打爆。
2.5 后台管理模块:不是 admin.site.register,而是用 django-jazzmin 定制化,解决真实运营需求
Django 自带 admin 后台对开发者友好,但对运营人员极不友好:默认列表页每页只显示 100 条,搜索框不能跨字段模糊匹配,没有导出 Excel 功能,更别说按日期范围筛选订单。本项目弃用原生 admin,改用django-jazzmin主题,并在admin.py中深度定制:
# admin.py from jazzmin import options as jazzmin_options jazzmin_options.JAZZMIN_SETTINGS = { "site_title": "手机商城运营后台", "site_header": "手机商城", "welcome_sign": "欢迎使用手机商城运营系统", "show_ui_builder": True, } @admin.register(Order) class OrderAdmin(ExportActionModelAdmin): # 继承导出功能 list_display = ('order_no', 'user_phone', 'total_amount', 'status', 'created_at', 'updated_at') list_filter = ('status', 'created_at', 'updated_at') # 时间范围筛选 search_fields = ('order_no', 'user__phone_number', 'items__product__name') # 跨表搜索 date_hierarchy = 'created_at' # 顶部时间导航条 actions = ['export_as_csv', 'mark_as_shipped'] # 批量操作 def user_phone(self, obj): return obj.user.phone_number if obj.user else '-' user_phone.short_description = '用户手机号'ExportActionModelAdmin来自django-import-export,点击“导出”按钮即可下载 CSV;search_fields中的items__product__name实现了通过订单内商品名称反向搜索订单;date_hierarchy自动生成年/月/日筛选器。这些不是炫技,而是某次运营活动后,市场部要求“导出上周所有支付成功的 iPhone 订单”,如果没有这些定制,只能写 SQL 或临时写脚本,响应时间从 1 分钟拉长到 30 分钟。
3. 环境搭建与本地运行:从 Python 3.9 到 Redis 7,版本踩坑比代码还致命
3.1 Python 与依赖管理:为什么必须用 requirements.in 而非直接 pip freeze?
项目根目录下有两个关键文件:requirements.in和requirements.txt。前者只写顶层依赖(如Django==4.2.11,djangorestframework==3.14.0),后者是pip-compile requirements.in生成的锁定文件,包含所有子依赖及其精确版本(如asgiref==3.7.2,sqlparse==0.4.4)。这样做不是为了装逼,而是解决“在我机器上能跑,到服务器上就报错”的玄学问题。例如celery依赖kombu,而kombu的 5.3.x 版本与redis-py的 4.6.x 存在连接池 bug,导致定时任务偶尔卡死。requirements.txt锁定了kombu==5.2.4和redis==4.5.4,彻底规避此问题。
# 正确的本地环境初始化流程 $ python -m venv venv $ source venv/bin/activate # Linux/Mac $ venv\Scripts\activate # Windows $ pip install pip-tools # 安装 pip-compile $ pip-compile requirements.in # 生成 requirements.txt $ pip install -r requirements.txt注意:
pip-compile会自动处理-e .(本地包开发模式)和--find-links(私有索引源),比pip freeze更可靠。某次升级 Django 到 4.2.12 后,pip-compile报出channels-redis与新版本不兼容,立刻定位到问题,而非等到部署后才发现 WebSocket 断连。
3.2 数据库与缓存服务:PostgreSQL 不是可选项,是高并发下的生存必需
项目settings/base.py中数据库配置默认指向 PostgreSQL:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.getenv('DB_NAME', 'phone_mall'), 'USER': os.getenv('DB_USER', 'mall_user'), 'PASSWORD': os.getenv('DB_PASSWORD', 'mall_pass'), 'HOST': os.getenv('DB_HOST', 'localhost'), 'PORT': os.getenv('DB_PORT', '5432'), } }为什么不用 SQLite?因为 SQLite 是文件锁,当 Celery worker 并发处理 100 个订单回调时,SELECT FOR UPDATE会因锁竞争导致大量超时。PostgreSQL 的行级锁和 MVCC 机制能支撑千级 QPS。同样,CACHES配置强制使用 Redis:
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": f"redis://{os.getenv('REDIS_HOST', 'localhost')}:{os.getenv('REDIS_PORT', '6379')}/1", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } }/1表示 Redis DB 1(DB 0 留给 Celery broker),避免缓存与任务队列混用。django_redis比原生redis-py多了连接池自动管理、序列化适配,cache.set('hot_products', products, timeout=300)时不会因大对象序列化失败。
3.3 Celery 异步任务:Broker 必须用 Redis,Result Backend 必须用 Django DB,这是稳定性的铁三角
celery.py中的配置是经过压测验证的:
# celery.py app.conf.broker_url = f'redis://{os.getenv("REDIS_HOST", "localhost")}:{os.getenv("REDIS_PORT", "6379")}/0' app.conf.result_backend = 'django-db' # 使用 Django ORM 存储任务结果 app.conf.task_serializer = 'json' app.conf.result_serializer = 'json' app.conf.accept_content = ['json'] app.conf.timezone = 'Asia/Shanghai' app.conf.enable_utc = FalseBroker 用 Redis(DB 0)是因为其发布订阅模型低延迟;Result Backend 用django-db而非 Redis,是因为任务结果(如发送邮件的返回值)需要与订单记录强关联,用 Django ORM 可以直接TaskResult.objects.filter(task_name='send_order_email').filter(status='SUCCESS')查询,且避免 Redis 内存溢出。task_serializer = 'json'是硬性要求——若用pickle,Celery worker 和 Django 进程 Python 版本不一致时会反序列化失败,导致任务无限重试。
3.4 前端联调:Vue3 项目如何对接 Django API?CORS 配置是生死线
Vue3 前端位于frontend/目录,启动命令npm run dev后运行在http://localhost:5173,而 Django 后端在http://localhost:8000。跨域问题由django-cors-headers解决,但配置极易出错:
# settings/base.py INSTALLED_APPS += ['corsheaders'] MIDDLEWARE.insert(0, 'corsheaders.middleware.CorsMiddleware') CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ] # 必须显式允许凭证(cookies/jwt) CORS_ALLOW_CREDENTIALS = True # 允许携带 Authorization 头 CORS_ALLOW_HEADERS = [ "accept", "authorization", "content-type", "user-agent", "x-csrftoken", "x-requested-with", ]CORS_ALLOW_CREDENTIALS = True是关键——Vue3 登录后将 JWT 存入localStorage,后续请求在Authorization: Bearer <token>头中携带,若没开启此选项,浏览器会直接拦截请求,Network 面板显示(blocked:credentials-not-supported)。某次前端同学误删了这行,所有 API 返回 403,排查了 2 小时才定位到 CORS 配置。
4. 避坑指南:那些让项目上线前夜崩溃的 5 个真实问题,附现象、原因与一行修复
4.1 现象:Celery worker 启动后立即退出,日志显示ConnectionRefusedError: [Errno 111] Connection refused
原因:Celery Broker 配置指向redis://localhost:6379/0,但本地未启动 Redis 服务,或 Docker Compose 中 Redis 容器名写错(如写成redis-server而非redis),导致 worker 连不上 Broker。
解决:先确认 Redis 是否运行:redis-cli ping返回PONG;若用 Docker,检查docker-compose.yml中 Redis 服务名是否与broker_url中的 host 一致;最后在celery.py中添加连接重试:
# celery.py app.conf.broker_connection_retry_on_startup = True # Django 4.2+ 新增 # 旧版本用: # app.conf.broker_connection_max_retries = 1004.2 现象:支付宝回调验签始终失败,data['sign']解码后为空
原因:支付宝回调是application/x-www-form-urlencoded编码,但 Django 默认将 POST 数据解析为request.POST,而request.POST对于非标准编码可能丢失原始字节。data['sign']实际在request.body中,需手动解析。
解决:在AlipayNotifyView.post()中改用request.body解析:
# payments/views.py def post(self, request, *args, **kwargs): # 不要用 request.POST,改用原始 body body = request.body.decode('utf-8') # 将 body 转为字典(需处理 & 和 =) data = dict(urllib.parse.parse_qsl(body)) if not verify_and_decode_alipay_notify(data, settings.ALIPAY_PUBLIC_KEY): return HttpResponse("验签失败", status=400) # ...后续逻辑4.3 现象:Nginx 反向代理后,Django 管理后台 CSS 全部失效,页面变成纯文字
原因:Nginx 配置中未正确设置STATIC_ROOT和MEDIA_ROOT的 location 规则,或 Djangosettings.py中STATIC_URL未以/static/结尾,导致浏览器请求/static/css/admin.css时 Nginx 返回 404。
解决:Nginx 配置必须包含:
location /static/ { alias /path/to/your/staticfiles/; # 注意末尾斜杠 expires 1y; add_header Cache-Control "public, immutable"; } location /media/ { alias /path/to/your/media/; # 同理 }且 Django 中STATIC_URL = '/static/'(必须有结尾斜杠),然后运行python manage.py collectstatic --noinput将所有静态文件收集到STATIC_ROOT目录。
4.4 现象:用户注册时提示IntegrityError: duplicate key value violates unique constraint "users_customuser_phone_number_key"
原因:phone_number字段在数据库中建了唯一索引,但用户输入13812345678和13812345678(带空格)被 Django 模型视为不同值,入库后触发唯一约束冲突。
解决:在CustomUser模型的clean()方法中统一清洗:
# users/models.py def clean(self): super().clean() if self.phone_number: self.phone_number = re.sub(r'[-\s]', '', self.phone_number) # 再次校验格式 if not re.match(r'^1[3-9]\d{9}$', self.phone_number): raise ValidationError('手机号格式错误')并在save()前调用self.full_clean(),确保清洗和校验在保存前完成。
4.5 现象:Docker 部署后,Celery worker 日志显示ModuleNotFoundError: No module named 'orders'
原因:Dockerfile 中COPY . /app复制了代码,但未执行pip install -e .安装本地包,导致 Celery worker 的 Python path 中找不到orders模块。
解决:Dockerfile 中RUN pip install -e .必须在COPY之后、CMD之前:
COPY requirements.txt . RUN pip install -r requirements.txt COPY . . RUN pip install -e . # 关键!安装本地包,使 orders 成为可导入模块 CMD ["celery", "-A", "config.celery", "worker", "--loglevel=info"]5. 生产部署与性能调优:从 Gunicorn 配置到数据库索引,让 QPS 从 50 稳定到 800+
5.1 Gunicorn 配置:为什么 workers 数量不能简单设为 CPU 核数 × 2?
Gunicorn 的--workers参数不是越多越好。本项目gunicorn.conf.py设为:
import multiprocessing # 基于经验公式:CPU 核数 × 2 + 1,但最大不超过 12 workers = min(multiprocessing.cpu_count() * 2 + 1, 12) worker_class = 'sync' # 同步模式,避免 asyncio 与 Django ORM 兼容问题 worker_connections = 1000 timeout = 30 keepalive = 5 max_requests = 1000 max_requests_jitter = 100 # 进程管理 preload = True # 预加载应用,避免 fork 后重新导入 daemon = False pidfile = '/var/run/gunicorn.pid'preload = True是关键——它让主进程在 fork worker 前先加载 Django 应用,避免每个 worker 单独加载导致内存翻倍。max_requests = 1000防止内存泄漏积累,worker 处理 1000 个请求后自动重启。某次线上发现内存持续增长,启用此参数后,内存曲线变为锯齿状,峰值稳定在 512MB 内。
5.2 PostgreSQL 索引优化:哪些字段必须加索引?用EXPLAIN ANALYZE说话
订单列表页/api/orders/默认按created_at降序排列,但用户常加?status=completed&start_date=2024-01-01筛选。若无索引,SELECT * FROM orders WHERE status='completed' AND created_at > '2024-01-01' ORDER BY created_at DESC会全表扫描。在orders/migrations/0002_add_indexes.py中添加复合索引:
# orders/migrations/0002_add_indexes.py from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('orders', '0001_initial'), ] operations = [ migrations.AddIndex( model_name='order', index=models.Index(fields=['status', 'created_at'], name='order_status_created_idx'), ), migrations.AddIndex( model_name='order', index=models.Index(fields=['user', 'created_at'], name='order_user_created_idx'), ), ]status和created_at的复合索引,让 WHERE 条件和 ORDER BY 都能走索引,查询时间从 1200ms 降至 12ms。用EXPLAIN ANALYZE验证:
EXPLAIN ANALYZE SELECT * FROM orders_order WHERE status = 'completed' AND created_at > '2024-01-01' ORDER BY created_at DESC LIMIT 20; -- 输出应包含 "Index Scan using order_status_created_idx on orders_order"5.3 Redis 缓存策略:不是所有数据都值得缓存,热点商品详情才是黄金场景
products/views.py中ProductDetailView使用缓存:
# products/views.py from django.core.cache import cache class ProductDetailView(RetrieveAPIView): queryset = Product.objects.all() serializer_class = ProductDetailSerializer def retrieve(self, request, *args, **kwargs): product_id = kwargs['pk'] cache_key = f'product_detail_{product_id}' cached_data = cache.get(cache_key) if cached_data is not None: return Response(cached_data) instance = self.get_object() serializer = self.get_serializer(instance) data = serializer.data # 缓存 10 分钟,但注意:商品价格/库存变更时需主动删除 cache.set(cache_key, data, timeout=600) return Response(data)但ProductListView不缓存——因为商品列表页有分页、排序、筛选,缓存键组合爆炸(list_category_smartphone_sort_price_asc_page_1),且用户行为分散,缓存命中率低于 10%。而商品详情页(如/api/products/123/)是高频访问点,单个商品缓存键固定,命中率超 95%。更重要的是,当管理员修改商品价格时,在ProductViewSet.update()中主动删除缓存:
def update(self, request, *args, **kwargs): response = super().update(request, *args, **kwargs) product_id = kwargs['pk'] cache.delete(f'product_detail_{product_id}') # 破坏缓存,保证一致性 return response5.4 Nginx 安全加固:隐藏版本号、限制请求频率、防御慢速攻击
nginx.conf中的关键安全配置:
# 隐藏 Nginx 版本号,减少信息泄露 server_tokens off; # 限制单 IP 每秒最多 10 个请求,防暴力遍历 limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; # 对 /api/ 路径启用限流 location /api/ { limit_req zone=api_limit burst=20 nodelay; proxy_pass http://django_app; # ...其他 proxy 设置 } # 防慢速攻击:客户端 5 秒内必须发完 header,10 秒内发完 body client_header_timeout 5; client_body_timeout 10; send_timeout 10;burst=20允许突发流量(如秒杀),nodelay避免请求排队阻塞。某次压力测试中,模拟 1000 个 IP 每秒发起请求,Nginx 日志显示 98% 的请求被limit_req拒绝,后端 Django 进程 CPU 稳定在 40%,未出现雪崩。
5.5 日志分级与告警:ERROR 日志自动发邮件,SQL 慢查询单独归档
settings/prod.py中日志配置:
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', }, }, 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': '/var/log/django/app.log', 'maxBytes': 1024*1024*5, # 5MB 'backupCount': 5, 'formatter': 'verbose', }, 'error_file': { 'level': 'ERROR', 'class': 'logging.handlers.RotatingFileHandler', 'filename': '/var/log/django/error.log', 'maxBytes': 1024*1024*5, 'backupCount': 5, 'formatter': 'verbose', }, 'mail_admins': { 'level': 'ERROR', 'class': 'django.utils.log.AdminEmailHandler', 'include_html': True, }, 'slow_sql': { 'level': 'WARNING', 'class': 'logging.FileHandler', 'filename': '/var/log/django/slow_sql.log', 'formatter': 'verbose', } }, 'loggers': { 'django': { 'handlers': ['file', 'error_file', 'mail_admins'], 'level': 'INFO', 'propagate': True, }, 'django.db.backends': { 'handlers': ['slow_sql'], 'level': 'WARNING', # WARNING 级别记录慢查询 'propagate': False, }, } }django.db.backends的WARNING日志会记录执行时间超过LOGGING['django.db.backends']['level']阈值的 SQL(Django 默认 0.5 秒)。slow_sql.log单独归档,运维可定时分析,如发现SELECT * FROM orders WHERE status='pending'未走索引,立刻加索引。
6. 从本地调试到灰度发布:我每次上线前必做的 3 个验证动作,少一个都可能引发资损
6.1 验证支付闭环:用沙箱账号走通“下单→支付→回调→状态更新→邮件通知”全链路
支付是商城的生命线,任何环节断裂都可能导致资损。我的标准验证流程是:
- 前置检查:确认
settings.py中ALIPAY_APP_ID,ALIPAY_PRIVATE_KEY,ALIPAY_PUBLIC_KEY已正确配置,且ALIPAY_RETURN_URL指向公网可访问地址(如https://staging.mall.com/api/payments/alipay/return/); - 沙箱下单:用 Vue3 前端在沙箱环境创建订单,获取
order_no; - 沙箱支付:在支付宝沙箱买家账号(如
2088102177841234)中完成支付,观察 Django 日志是否打印AlipayNotifyView: 支付宝回调验签成功; - 状态验证:进入 Django Admin,搜索该
order_no,确认status已变为PAID,paid_at时间戳已更新; - 邮件验证:检查收件箱,确认收到主题为“您的订单已支付成功”的邮件,且邮件中订单号、金额、商品列表与下单一致;
- 异常模拟:手动修改数据库中该订单
status为PENDING,再次触发支付宝回调(用curl模拟 POST),验证是否因状态非法被拒绝(日志应有ValidationError: 订单状态非法)。
这 6 步缺一不可。曾有一次跳过第 6 步,上线后黑客伪造回调将PENDING订单强行改为PAID,导致 37 笔未付款订单被发货,损失 2.3 万元。从那以后,我每次上线前都强制走一遍异常模拟。
6.2 验证缓存一致性:修改商品价格后,API 返回、缓存、数据库三者必须实时同步
缓存不一致是隐形炸弹。我的验证方法是:
| 操作步骤 | 预期结果 | 验证方式 |
|---|---|---|
1. 用 Django Admin 将商品 ID=123 的价格从2999.00改为2599.00 | 数据库price字段更新 |
本文还有配套的精品资源,点击获取