Django入门教程:搭建你的第一个Python网站(别怕,真不难)
上周五晚上十点半,我坐在工位上盯着屏幕上疯狂滚动的ERROR: django.db.utils.OperationalError: (2003, "Can't connect to MySQL server on 'xxx.xxx.xxx.xxx'")日志,一边啃着冷掉的外卖,一边在心里默默问候产品经理祖宗十八代。
这事儿得从三天前说起。我们组正在做一款新游戏的运营后台,需求是给运营同学提供一个简单的数据看板,让他们能查玩家留存、活动参与率这些基础指标。本来这种活儿应该是运维或者专门的数据平台团队干的,但架不住人家说“你们服务端最懂业务逻辑”,硬是塞给了我们。
我第一反应就是:“这么简单的需求,用Django搭个Web页面不就完了?几个Model,几个View,再套个Bootstrap模板,两天搞定!” 结果现实狠狠打了我的脸——你以为的简单,往往藏着无数个坑在等你跳。
为什么选Django?被逼的,但真香
在网易这三年多,我主要用的是C++和Go写游戏服务端,Python更多是用来写脚本、处理数据。但这次的需求明显不适合用重型框架,毕竟只是个内部工具,又不是要扛百万QPS的核心服务。
考虑过Flask,轻量灵活;也想过FastAPI,异步性能好。但最后还是选了Django,原因很现实:
- 自带Admin后台:运营同学最需要的就是这个!不用我们额外开发管理界面
- ORM成熟稳定:对接MySQL数据库,写查询逻辑比手写SQL安全多了
- 生态完善:各种第三方包应有尽有,比如
django-cors-headers、drf这些 - 学习成本低:组里新人也能快速上手维护
说实话,在游戏公司待久了,对“稳定”两个字特别敏感。新技术固然酷炫,但线上事故的代价太高了。去年双11期间,因为某个微服务用了不成熟的异步库,导致整个充值系统卡了两个小时,那场面……至今想起还心有余悸。
所以这次果断选择了“老而弥坚”的Django。稳定压倒一切,尤其是在面对运营同学的时候——他们可不管你技术多牛逼,只关心能不能按时看到数据。
环境搭建:别在第一步就劝退自己
很多新手一上来就被环境配置搞懵了。什么virtualenv、pip、requirements.txt,听着就头大。其实没那么复杂,记住一个原则:隔离环境,避免污染系统Python。
# 创建项目目录
mkdir ops-dashboard && cd ops-dashboard
# 创建虚拟环境(Python 3.8+)
python3 -m venv venv
# 激活虚拟环境
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# 安装Django
pip install django==4.2.7
# 验证安装
python -m django --version
这里有个小技巧:永远不要用系统全局的pip安装包!我见过太多同事因为这个导致不同项目依赖冲突,最后只能重装系统(夸张了,但真的很烦)。
创建项目也很简单:
django-admin startproject ops_dashboard .
cd ops_dashboard
python manage.py startapp players # 创建应用
注意那个点!很多人漏掉它,结果项目结构乱七八糟。Django的项目结构虽然有点“约定大于配置”的味道,但习惯了其实挺清爽的。
数据库设计:别让运营同学哭着来找你
这次的核心是玩家数据展示,所以数据库设计要兼顾查询效率和业务理解。我们的MySQL表结构大概是这样的:
# players/models.py
from django.db import models
class Player(models.Model):
player_id = models.BigIntegerField(unique=True, help_text="玩家ID")
nickname = models.CharField(max_length=64, help_text="昵称")
create_time = models.DateTimeField(auto_now_add=True, help_text="注册时间")
last_login = models.DateTimeField(null=True, blank=True, help_text="最后登录时间")
class Meta:
db_table = 'player_info'
indexes = [
models.Index(fields=['create_time']),
models.Index(fields=['last_login']),
]
class ActivityParticipation(models.Model):
player = models.ForeignKey(Player, on_delete=models.CASCADE)
activity_id = models.CharField(max_length=32)
participate_time = models.DateTimeField()
reward_claimed = models.BooleanField(default=False)
class Meta:
db_table = 'activity_participation'
indexes = [
models.Index(fields=['activity_id', 'participate_time']),
models.Index(fields=['player_id']), # 注意这里会自动创建
]
关键点来了:别忘了加索引!运营同学最喜欢问“昨天有多少人参加了XX活动”,如果没有合适的复合索引,分分钟给你跑出个全表扫描。
我们之前就吃过亏——某次活动上线后,运营想查参与人数,结果SQL跑了5分钟还没结束,DBA直接打电话过来骂人。从此以后,建表必加索引成了我们组的铁律。
另外,help_text这个字段看似无用,其实在Django Admin后台会显示为字段说明,对非技术人员特别友好。运营同学再也不用问“这个字段到底是啥意思”了。
性能优化:别让简单的查询变成线上事故
Django ORM用起来很爽,但一不小心就会写出N+1查询的灾难。比如这个看似 innocuous 的代码:
# BAD EXAMPLE - 千万别这么写!
def get_players_with_activities():
players = Player.objects.all()
for player in players:
print(f"{player.nickname}: {player.activityparticipation_set.count()}") # N+1!
表面上看没问题,但实际上每循环一次就会发一条SQL去查活动记录。如果有1000个玩家,就是1001条SQL!在线上环境这就是定时炸弹。
正确的做法是用select_related或prefetch_related:
# GOOD EXAMPLE
def get_players_with_activities():
players = Player.objects.prefetch_related('activityparticipation_set')
for player in players:
print(f"{player.nickname}: {player.activityparticipation_set.count()}")
这样只需要2条SQL就搞定了。记住:在Django里,能用QuerySet方法解决的,就别用Python循环。
我们还在生产环境中加了一些监控:
# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'level': 'DEBUG', # 开发环境开启,生产环境关闭
},
},
}
开发阶段开着这个,能清楚看到每条SQL的执行时间和参数,方便及时发现性能问题。
接口设计:运营同学不是程序员
这次的需求里,运营同学需要通过Web界面查看数据,而不是调用API。但为了灵活性,我还是把核心逻辑封装成了API,前端用简单的HTML + AJAX调用。
# players/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.core.paginator import Paginator
import json
@require_http_methods(["GET"])
def player_list(request):
"""玩家列表接口"""
page = int(request.GET.get('page', 1))
page_size = int(request.GET.get('page_size', 20))
# 安全校验
if page_size > 100:
page_size = 100
players = Player.objects.all().order_by('-create_time')
paginator = Paginator(players, page_size)
page_obj = paginator.get_page(page)
data = {
'total': paginator.count,
'page': page,
'page_size': page_size,
'results': [
{
'player_id': p.player_id,
'nickname': p.nickname,
'create_time': p.create_time.strftime('%Y-%m-%d %H:%M:%S'),
'last_login': p.last_login.strftime('%Y-%m-%d %H:%M:%S') if p.last_login else None,
}
for p in page_obj
]
}
return JsonResponse(data)
安全校验很重要!别以为内部系统就不用防XSS、SQL注入。我们之前有个实习生,直接把request.GET.get('page_size')传给Paginator,结果有人传了个page_size=999999,直接把内存打爆了。
另外,分页是必须的。运营同学可能不知道10万条数据一次性返回是什么概念,但你的服务器知道。
生产部署:别在最后一步翻车
本地跑得好好的,部署到生产环境就挂,这是每个程序员的噩梦。Django部署有几个关键点:
1. 静态文件处理
开发时Django会自动serve静态文件,但生产环境必须用Nginx:
# nginx.conf
server {
listen 80;
server_name ops.yourcompany.com;
location /static/ {
alias /path/to/your/project/staticfiles/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
记得运行python manage.py collectstatic收集静态文件!
2. 数据库连接池
默认的Django数据库连接没有池化,高并发下会频繁创建销毁连接。我们用的是django-db-geventpool:
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django_db_geventpool.backends.mysql',
'HOST': 'your-db-host',
'NAME': 'ops_dashboard',
'USER': 'ops_user',
'PASSWORD': 'secure_password',
'OPTIONS': {
'MAX_CONNS': 20,
'MIN_CONNS': 5,
}
}
}
3. 进程管理
别再用nohup python manage.py runserver &了!用Gunicorn:
pip install gunicorn
gunicorn --workers 4 --bind 0.0.0.0:8000 ops_dashboard.wsgi:application
Worker数量一般是CPU核心数*2+1,具体要根据负载测试调整。
效果如何?运营同学终于笑了
折腾了一周,这个小系统终于上线了。最让我欣慰的是,运营同学第一次自己就能查到想要的数据,不用再半夜打电话问我“能不能帮忙导个表”。
性能方面,在我们内部的压力测试中:
- 单机4核8G,Gunicorn 4 workers
- 简单查询接口平均响应时间 < 50ms
- 复杂聚合查询(带GROUP BY)< 300ms
- 支持50+并发用户同时使用
下面是具体的性能对比数据:
| 查询类型 | 优化前响应时间 | 优化后响应时间 | 提升倍数 |
|---|---|---|---|
| 玩家列表(分页) | 1200ms | 45ms | 26.7x |
| 活动参与统计 | 800ms | 120ms | 6.7x |
| 单玩家详情 | 200ms | 30ms | 6.7x |
最大的收益其实是可维护性。现在组里的新人接手这个项目,看一眼代码就知道怎么加新功能。不像某些祖传PHP代码,改一行就要祈祷三天。
写在最后:为什么我要写这篇教程
在网易待了三年多,最近开始考虑换个环境。一方面是想接触更多不同类型的技术栈,另一方面也是觉得不能一直在舒适区待着。游戏服务端虽然有趣,但Web开发的基础能力也不能丢。
写这篇教程,既是对自己这次实践的总结,也是希望帮到那些和我一样被临时抓壮丁去做Web开发的游戏程序员。Django真的没那么可怕,关键是要理解它的设计理念,而不是死记硬背API。
顺便吐槽一下:产品经理们,请你们下次提需求的时候,能不能先想清楚到底要什么?别再说“就做个简单的页面”了,simple is relative啊!
最后,如果你也在用Django做内部工具,或者遇到了什么奇怪的问题,欢迎在评论区交流。毕竟程序员的世界里,没有解决不了的Bug,只有不够努力的Google(和Stack Overflow)。
P.S. 刚刚收到消息,运营同学又提了新需求:要支持Excel导出。看来我的加班生活还要继续……不过这次我学聪明了,直接用django-import-export,5分钟搞定,嘿嘿。

评论 0