Django入门教程:搭建你的第一个Python网站(别怕,真不难)

清醒开发者
2025-12-15 17:04
阅读 239

上周五晚上十点半,我坐在工位上盯着屏幕上疯狂滚动的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,原因很现实:

  1. 自带Admin后台:运营同学最需要的就是这个!不用我们额外开发管理界面
  2. ORM成熟稳定:对接MySQL数据库,写查询逻辑比手写SQL安全多了
  3. 生态完善:各种第三方包应有尽有,比如django-cors-headersdrf这些
  4. 学习成本低:组里新人也能快速上手维护

说实话,在游戏公司待久了,对“稳定”两个字特别敏感。新技术固然酷炫,但线上事故的代价太高了。去年双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_relatedprefetch_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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝