基于Django与Vue的全栈项目开发实践
项目架构设计与环境配置
本项目采用前后端分离架构,后端基于 Django 框架构建 RESTful API,前端使用 Vue.js 实现动态交互。整体技术栈为:Python 3.8 + Django 3.2 + DRF(Django REST Framework)+ MySQL + Redis + Vue 2.x。
虚拟环境管理
为保证依赖隔离,推荐使用 virtualenvwrapper-win 管理多个 Python 虚拟环境。
# 安装虚拟环境工具
pip install virtualenv virtualenvwrapper-win
# 设置工作目录(添加至系统环境变量)
WORKON_HOME = D:\Envs
# 创建指定解释器的虚拟环境
mkvirtualenv -p python luffy_project
# 常用命令
workon # 查看所有环境
workon luffy_project # 进入环境
deactivate # 退出当前环境
rmvirtualenv env_name # 删除环境
Django 项目结构优化
标准项目目录如下:
luffy_backend/
├── manage.py
├── scripts/ # 可执行脚本(不提交 Git)
├── logs/ # 日志存储路径
├── luffy_backend/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── dev.py # 开发配置
│ │ └── prod.py # 生产配置
│ ├── urls.py
│ ├── wsgi.py
│ └── apps/ # 所有业务模块集中存放
│ ├── home/
│ ├── user/
│ └── __init__.py
└── utils/ # 公共工具包
├── exceptions.py
├── response.py
└── models.py
开发配置文件(dev.py)
通过修改 sys.path 实现模块路径简化导入。
import os
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# 添加 apps 和根目录到 Python 路径
sys.path.insert(0, str(BASE_DIR / 'apps'))
sys.path.insert(0, str(BASE_DIR))
SECRET_KEY = 'your-secret-key'
DEBUG = True
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'home', # 注册子应用
'user'
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'luffy_backend.urls'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'luffy',
'USER': os.getenv('DB_USER', 'luffy'),
'PASSWORD': os.getenv('DB_PASS', 'Luffy123?'),
'HOST': '127.0.0.1',
'PORT': 3306,
'OPTIONS': {'charset': 'utf8mb4'}
}
}
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
媒体资源访问路由配置
在主路由中开放 media 文件访问权限。
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path, include
from django.views.static import serve
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('home.urls')),
path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
日志系统集成
利用 Python 内置 logging 模块实现多处理器日志输出。
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'detailed': {
'format': '{levelname} {asctime} {module}:{lineno} {message}',
'style': '{'
},
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'detailed'
},
'file': {
'level': 'WARNING',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(BASE_DIR.parent, 'logs', 'app.log'),
'maxBytes': 100 * 1024 * 1024,
'backupCount': 5,
'formatter': 'detailed',
'encoding': 'utf-8'
}
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': True
}
}
}
封装通用日志对象:
# utils/logger.py
import logging
app_logger = logging.getLogger('django')
统一响应格式封装
继承 DRF Response 类定制返回结构。
# utils/response.py
from rest_framework.response import Response
class SuccessResponse(Response):
def __init__(self, data=None, message='操作成功', code=200, status=None, **kwargs):
result = {
'code': code,
'message': message,
'data': data or {}
}
result.update(kwargs)
super().__init__(data=result, status=status)
全局异常处理
重写 DRF 异常处理器以支持自定义错误码和日志记录。
# utils/exceptions.py
from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response
from .response import SuccessResponse
from .logger import app_logger
def exception_handler(exc, context):
request = context['request']
view = context['view']
# 使用原生处理器先尝试处理
response = drf_exception_handler(exc, context)
if response is None:
# 非 DRF 认知的异常(如数据库错误、视图错误)
app_logger.error(
f'未捕获异常 | 用户:{getattr(request.user,"username","匿名")}|'
f'IP:{request.META.get("REMOTE_ADDR")}|'
f'路径:{request.path}|错误:{str(exc)}'
)
return SuccessResponse(code=999, message='服务器内部错误', status=500)
# 已知异常也记录日志
app_logger.warning(f'客户端异常 | {request.path} | {exc}')
return response
注册到 settings:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler'
}
数据库初始化
创建专用数据库用户并授权:
CREATE DATABASE luffy CHARACTER SET utf8mb4;
CREATE USER 'luffy_user'@'%' IDENTIFIED BY 'SecurePass123!';
GRANT ALL PRIVILEGES ON luffy.* TO 'luffy_user'@'%';
FLUSH PRIVILEGES;
安装数据库驱动:
pip install mysqlclient
扩展用户模型
继承 AbstractUser 添加手机号与头像字段。
# apps/user/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
avatar = models.ImageField(upload_to='avatars/', default='default.png', verbose_name='头像')
class Meta:
db_table = 'luffy_user'
verbose_name = '用户信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
前端基础搭建(Vue)
全局样式与配置
清除默认样式:
/* assets/css/reset.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
ul, ol { list-style: none; }
a { text-decoration: none; color: inherit; }
img { vertical-align: middle; }
table { border-collapse: collapse; }
配置基地址:
// assets/js/config.js
export default {
API_BASE: process.env.NODE_ENV === 'development'
? 'http://127.0.0.1:8001/api/v1'
: 'https://api.luffy.com/v1'
}
挂载为 Vue 原型属性:
// main.js
import Vue from 'vue'
import config from '@/assets/js/config'
Vue.prototype.$config = config
常用插件集成
npm install axios vue-cookies element-ui --save
import axios from 'axios'
import cookies from 'vue-cookies'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Vue.prototype.$http = axios
Vue.prototype.$cookies = cookies
核心功能实现
轮播图接口开发
抽象公共模型:
# utils/models.py
from django.db import models
class BaseModel(models.Model):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
is_active = models.BooleanField(default=True, verbose_name='是否激活')
order = models.PositiveSmallIntegerField(default=1, verbose_name='排序')
class Meta:
abstract = True
轮播图表定义:
# apps/home/models.py
from django.db import models
from utils.models import BaseModel
class Banner(BaseModel):
title = models.CharField(max_length=32, verbose_name='标题')
description = models.TextField(blank=True, verbose_name='描述')
link = models.URLField(verbose_name='跳转链接')
image = models.ImageField(upload_to='banners/', verbose_name='图片')
class Meta:
db_table = 'home_banner'
ordering = ['-order']
def __str__(self):
return self.title
序列化与视图:
# serializers.py
from rest_framework import serializers
from .models import Banner
class BannerSerializer(serializers.ModelSerializer):
class Meta:
model = Banner
fields = ['id', 'title', 'description', 'link', 'image', 'order']
# views.py
from rest_framework.generics import ListAPIView
from .models import Banner
from .serializers import BannerSerializer
from utils.response import SuccessResponse
class BannerListView(ListAPIView):
queryset = Banner.objects.filter(is_active=True)
serializer_class = BannerSerializer
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
return SuccessResponse(data=response.data, message='获取轮播图成功')
登录注册体系
支持多方式登录与短信验证注册。
# serializers.py
from rest_framework_simplejwt.tokens import RefreshToken
from django.core.cache import cache
import re
class BaseLoginSerializer(serializers.Serializer):
def validate(self, attrs):
self._authenticate(attrs)
self._generate_token()
return attrs
def _authenticate(self, attrs):
raise NotImplementedError
def _generate_token(self):
user = getattr(self, 'user', None)
refresh = RefreshToken.for_user(user)
self.context['token'] = str(refresh.access_token)
self.context['refresh'] = str(refresh)
class MultiFieldLoginSerializer(BaseLoginSerializer):
account = serializers.CharField()
password = serializers.CharField()
def _authenticate(self, attrs):
account = attrs['account']
password = attrs['password']
user = User.objects.filter(
models.Q(username=account) |
models.Q(mobile=account) |
models.Q(email=account)
).first()
if user and user.check_password(password):
self.user = user
else:
raise serializers.ValidationError('账号或密码错误')
class SMSCodeLoginSerializer(BaseLoginSerializer):
mobile = serializers.CharField()
code = serializers.CharField()
def _authenticate(self, attrs):
mobile = attrs['mobile']
code = attrs['code']
cached_code = cache.get(f"sms_code_{mobile}")
if code != cached_code:
raise serializers.ValidationError('验证码无效')
self.user = User.objects.get(mobile=mobile)
发送短信验证码:
# utils/sms.py
import random
from tencentcloud.sms.v20210111 import sms_client, models
from django.core.cache import cache
class TencentSMS:
def __init__(self, mobile, template_id='1871234'):
self.mobile = mobile
self.template_id = template_id
def send(self):
code = ''.join([str(random.randint(0, 9)) for _ in range(4)])
cache.set(f"sms_code_{self.mobile}", code, timeout=60 * 5) # 5分钟有效
# 实际调用腾讯云 SDK 发送
# client.SendSms(...)
return {'status': 'success', 'code': code}
注册逻辑:
class RegisterSerializer(serializers.ModelSerializer):
code = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['mobile', 'password', 'code']
extra_kwargs = {'password': {'write_only': True}}
def validate_mobile(self, value):
if not re.match(r'^1[3-9]\d{9}$', value):
raise serializers.ValidationError('手机号格式不正确')
if User.objects.filter(mobile=value).exists():
raise serializers.ValidationError('该手机号已注册')
return value
def validate(self, attrs):
code = attrs.pop('code')
cached = cache.get(f"sms_code_{attrs['mobile']}")
if code != cached:
raise serializers.ValidationError('验证码错误')
attrs['username'] = f"user_{attrs['mobile'][-4:]}"
return attrs
def create(self, validated_data):
return User.objects.create_user(**validated_data)
