《Python星球日记》第35天:全栈开发(综合项目)

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》
创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊)
专栏:《Python星球日记》,限时特价订阅中ing


👋 专栏介绍Python星球日记专栏介绍(持续更新ing)
上一篇《Python星球日记》第34天:Web 安全基础

欢迎来到Python星球的第35天!🪐

大家好,今天我们将通过一个综合项目来实践全栈开发,将前面学习的所有技术融会贯通。

一、全栈开发概述

全栈开发是指同时负责前端和后端开发的工程师,能够独立完成一个完整的应用系统。在Python生态中,全栈开发已经成为一项极具价值的技能

在这里插入图片描述

1. 全栈开发的优势

   - 开发流程更加流畅,减少沟通成本
   - 技术栈统一,提高开发效率
   - 更好地理解和解决系统整体问题
   - 职业发展更具竞争力

2. 全栈开发技能组合

   - 前端:HTML、CSS、JavaScript以及框架(React、Vue等)
   - 后端:Python框架(Django、Flask)
   - 数据库:关系型数据库(MySQL、SQLite)
   - 部署运维:Git、Docker、云服务等

在这里插入图片描述

二、博客系统项目需求分析

让我们以一个博客系统为例,从零开始构建一个全栈应用。

1. 功能需求

  • 用户模块:注册、登录、个人信息管理
  • 文章模块:发布、编辑、删除、查看文章
  • 评论模块:发表评论、回复评论
  • 分类与标签:文章分类、标签管理
  • 搜索功能:按关键词搜索文章

2. 技术栈选择

  • 前端:HTML/CSS/JavaScript + Bootstrap(简化响应式设计)
  • 后端:Django(适合快速开发完整应用)
  • 数据库:SQLite(开发阶段)/ MySQL(生产环境)

3. 项目结构规划

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

三、数据库设计

数据库设计是全栈应用的基础,良好的数据库结构设计能够为应用提供坚实的后盾。

1. 实体关系分析

我们需要先明确博客系统中的几个主要实体:用户(User)、文章(Post)、评论(Comment)、分类(Category)和标签(Tag)。

在这里插入图片描述

2. Django模型设计

让我们将ER图转换为Django模型代码:

# users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    profile_pic = models.ImageField(upload_to='profile_pics', blank=True)
    bio = models.TextField(max_length=500, blank=True)
    
    def __str__(self):
        return self.username
# blog/models.py
from django.db import models
from django.urls import reverse
from users.models import User

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(blank=True)
    
    def __str__(self):
        return self.name
    
    class Meta:
        verbose_name_plural = "Categories"

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    views = models.IntegerField(default=0)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    tags = models.ManyToManyField(Tag, blank=True)
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return reverse('post-detail', kwargs={'pk': self.pk})
# comments/models.py
from django.db import models
from users.models import User
from blog.models import Post

class Comment(models.Model):
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
    
    def __str__(self):
        return f"Comment by {self.user.username} on {self.post.title}"

四、后端开发

后端是应用的核心,负责处理业务逻辑和数据交互。我们使用Django框架来实现博客系统的后端功能。

1. Django项目创建

# 创建Django项目
django-admin startproject pythonblog

# 创建应用
cd pythonblog
python manage.py startapp blog
python manage.py startapp users
python manage.py startapp comments

2. 视图函数开发

以博客文章的CRUD操作为例:

# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from .models import Post, Category, Tag
from .forms import PostForm

class PostListView(ListView):
    model = Post
    template_name = 'blog/home.html'
    context_object_name = 'posts'
    ordering = ['-created_at']
    paginate_by = 5

class PostDetailView(DetailView):
    model = Post
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # 增加阅读量
        post = self.object
        post.views += 1
        post.save()
        return context

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    form_class = PostForm
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    form_class = PostForm
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
    
    def test_func(self):
        post = self.get_object()
        # 确保只有文章作者才能编辑
        return self.request.user == post.author

class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Post
    success_url = '/'
    
    def test_func(self):
        post = self.get_object()
        # 确保只有文章作者才能删除
        return self.request.user == post.author

3. URL配置

# blog/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.PostListView.as_view(), name='blog-home'),
    path('post/<int:pk>/', views.PostDetailView.as_view(), name='post-detail'),
    path('post/new/', views.PostCreateView.as_view(), name='post-create'),
    path('post/<int:pk>/update/', views.PostUpdateView.as_view(), name='post-update'),
    path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post-delete'),
    path('category/<int:category_id>/', views.category_posts, name='category-posts'),
    path('tag/<int:tag_id>/', views.tag_posts, name='tag-posts'),
]

4. 表单处理

# blog/forms.py
from django import forms
from .models import Post, Comment

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'category', 'tags']
        widgets = {
            'content': forms.Textarea(attrs={'class': 'markdown-editor'}),
            'tags': forms.CheckboxSelectMultiple(),
        }

五、前端开发

前端负责用户交互界面的实现,我们使用Bootstrap框架来快速构建响应式界面。

1. Base模板

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Python星球博客{% endblock %}</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="{% static 'css/main.css' %}">
    {% block extra_css %}{% endblock %}
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{% url 'blog-home' %}">Python星球博客</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="{% url 'blog-home' %}">首页</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">分类</a>
                    </li>
                </ul>
                <div class="navbar-nav">
                    {% if user.is_authenticated %}
                        <a class="nav-link" href="{% url 'post-create' %}">写文章</a>
                        <a class="nav-link" href="{% url 'profile' %}">个人中心</a>
                        <a class="nav-link" href="{% url 'logout' %}">退出</a>
                    {% else %}
                        <a class="nav-link" href="{% url 'login' %}">登录</a>
                        <a class="nav-link" href="{% url 'register' %}">注册</a>
                    {% endif %}
                </div>
            </div>
        </div>
    </nav>

    <!-- 主内容区 -->
    <main class="container mt-4">
        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
        
        {% block content %}{% endblock %}
    </main>

    <!-- 页脚 -->
    <footer class="bg-dark text-white text-center py-3 mt-5">
        <div class="container">
            <p>&copy; 2025 Python星球博客 | Powered by Django</p>
        </div>
    </footer>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

2. 博客首页

<!-- templates/blog/home.html -->
{% extends 'base.html' %}

{% block title %}Python星球博客 - 首页{% endblock %}

{% block content %}
<div class="row">
    <!-- 文章列表 -->
    <div class="col-md-8">
        <h1 class="mb-4">最新文章</h1>
        {% for post in posts %}
            <div class="card mb-4">
                <div class="card-body">
                    <h2 class="card-title">
                        <a href="{% url 'post-detail' post.pk %}">{{ post.title }}</a>
                    </h2>
                    <p class="card-text text-muted">
                        <small>
                            由 {{ post.author.username }} 发布于 {{ post.created_at|date:"Y-m-d H:i" }}
                            | 分类: <a href="{% url 'category-posts' post.category.id %}">{{ post.category.name }}</a>
                            | 阅读量: {{ post.views }}
                        </small>
                    </p>
                    <p class="card-text">{{ post.content|truncatewords:50 }}</p>
                    <a href="{% url 'post-detail' post.pk %}" class="btn btn-primary">阅读全文</a>
                </div>
            </div>
        {% empty %}
            <p>暂无文章</p>
        {% endfor %}
        
        <!-- 分页 -->
        {% if is_paginated %}
            <nav aria-label="Page navigation">
                <ul class="pagination">
                    {% if page_obj.has_previous %}
                        <li class="page-item">
                            <a class="page-link" href="?page=1">首页</a>
                        </li>
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a>
                        </li>
                    {% endif %}
                    
                    {% for num in page_obj.paginator.page_range %}
                        {% if page_obj.number == num %}
                            <li class="page-item active">
                                <span class="page-link">{{ num }}</span>
                            </li>
                        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
                            <li class="page-item">
                                <a class="page-link" href="?page={{ num }}">{{ num }}</a>
                            </li>
                        {% endif %}
                    {% endfor %}
                    
                    {% if page_obj.has_next %}
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a>
                        </li>
                        <li class="page-item">
                            <a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">末页</a>
                        </li>
                    {% endif %}
                </ul>
            </nav>
        {% endif %}
    </div>

    <!-- 侧边栏 -->
    <div class="col-md-4">
        <div class="card mb-4">
            <div class="card-header">搜索</div>
            <div class="card-body">
                <form action="{% url 'search' %}" method="get">
                    <div class="input-group">
                        <input type="text" name="q" class="form-control" placeholder="搜索文章...">
                        <button class="btn btn-outline-secondary" type="submit">搜索</button>
                    </div>
                </form>
            </div>
        </div>
        
        <div class="card mb-4">
            <div class="card-header">分类</div>
            <div class="card-body">
                <ul class="list-group list-group-flush">
                    {% for category in categories %}
                        <li class="list-group-item d-flex justify-content-between align-items-center">
                            <a href="{% url 'category-posts' category.id %}">{{ category.name }}</a>
                            <span class="badge bg-primary rounded-pill">{{ category.post_set.count }}</span>
                        </li>
                    {% empty %}
                        <li class="list-group-item">暂无分类</li>
                    {% endfor %}
                </ul>
            </div>
        </div>
        
        <div class="card">
            <div class="card-header">热门标签</div>
            <div class="card-body">
                <div class="tags">
                    {% for tag in tags %}
                        <a href="{% url 'tag-posts' tag.id %}" class="badge bg-secondary me-1 mb-1">
                            {{ tag.name }} ({{ tag.post_set.count }})
                        </a>
                    {% empty %}
                        <p>暂无标签</p>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

3. 文章详情页

<!-- templates/blog/post_detail.html -->
{% extends 'base.html' %}

{% block title %}{{ post.title }} - Python星球博客{% endblock %}

{% block content %}
<div class="row">
    <div class="col-md-8">
        <!-- 文章内容 -->
        <article class="card mb-4">
            <div class="card-body">
                <h1 class="card-title">{{ post.title }}</h1>
                <p class="text-muted">
                    <small>
                        由 {{ post.author.username }} 发布于 {{ post.created_at|date:"Y-m-d H:i" }}
                        | 分类: <a href="{% url 'category-posts' post.category.id %}">{{ post.category.name }}</a>
                        | 阅读量: {{ post.views }}
                        {% if post.updated_at != post.created_at %}
                            | 最后编辑: {{ post.updated_at|date:"Y-m-d H:i" }}
                        {% endif %}
                    </small>
                </p>
                
                <!-- 文章内容 -->
                <div class="card-text mt-4">
                    {{ post.content|safe|linebreaks }}
                </div>
                
                <!-- 标签 -->
                <div class="mt-4">
                    {% for tag in post.tags.all %}
                        <a href="{% url 'tag-posts' tag.id %}" class="badge bg-secondary me-1">{{ tag.name }}</a>
                    {% endfor %}
                </div>
                
                <!-- 操作按钮 -->
                {% if user == post.author %}
                    <div class="mt-4">
                        <a href="{% url 'post-update' post.pk %}" class="btn btn-outline-primary">编辑</a>
                        <a href="{% url 'post-delete' post.pk %}" class="btn btn-outline-danger">删除</a>
                    </div>
                {% endif %}
            </div>
        </article>
        
        <!-- 评论区 -->
        <div class="card">
            <div class="card-header">评论 ({{ post.comment_set.count }})</div>
            <div class="card-body">
                <!-- 评论表单 -->
                {% if user.is_authenticated %}
                    <form method="post" action="{% url 'add-comment' post.pk %}">
                        {% csrf_token %}
                        <div class="mb-3">
                            <textarea name="content" class="form-control" rows="3" placeholder="写下你的评论..."></textarea>
                        </div>
                        <button type="submit" class="btn btn-primary">提交评论</button>
                    </form>
                {% else %}
                    <p><a href="{% url 'login' %}">登录</a> 后发表评论</p>
                {% endif %}
                
                <!-- 评论列表 -->
                <div class="mt-4">
                    {% for comment in post.comment_set.all %}
                        <div class="mb-3 pb-3 border-bottom">
                            <div class="d-flex">
                                <div class="flex-shrink-0">
                                    <img src="{{ comment.user.profile_pic.url|default:'static/img/default-avatar.jpg' }}" 
                                         class="rounded-circle" width="50" height="50" alt="">
                                </div>
                                <div class="ms-3">
                                    <h5 class="mt-0">{{ comment.user.username }}</h5>
                                    <p class="text-muted">
                                        <small>{{ comment.created_at|date:"Y-m-d H:i" }}</small>
                                    </p>
                                    <p>{{ comment.content }}</p>
                                    
                                    <!-- 回复按钮 -->
                                    {% if user.is_authenticated %}
                                        <button class="btn btn-sm btn-link reply-btn" 
                                                data-comment-id="{{ comment.id }}">回复</button>
                                        
                                        <!-- 回复表单 -->
                                        <div class="reply-form mt-2" id="reply-form-{{ comment.id }}" style="display: none;">
                                            <form method="post" action="{% url 'add-reply' post.pk comment.pk %}">
                                                {% csrf_token %}
                                                <div class="mb-3">
                                                    <textarea name="content" class="form-control" rows="2" 
                                                              placeholder="回复 {{ comment.user.username }}..."></textarea>
                                                </div>
                                                <button type="submit" class="btn btn-sm btn-primary">提交回复</button>
                                            </form>
                                        </div>
                                    {% endif %}
                                    
                                    <!-- 子评论 -->
                                    {% for reply in comment.comment_set.all %}
                                        <div class="ms-4 mt-3">
                                            <div class="d-flex">
                                                <div class="flex-shrink-0">
                                                    <img src="{{ reply.user.profile_pic.url|default:'static/img/default-avatar.jpg' }}" 
                                                         class="rounded-circle" width="40" height="40" alt="">
                                                </div>
                                                <div class="ms-3">
                                                    <h6 class="mt-0">{{ reply.user.username }}</h6>
                                                    <p class="text-muted">
                                                        <small>{{ reply.created_at|date:"Y-m-d H:i" }}</small>
                                                    </p>
                                                    <p>{{ reply.content }}</p>
                                                </div>
                                            </div>
                                        </div>
                                    {% endfor %}
                                </div>
                            </div>
                        </div>
                    {% empty %}
                        <p>暂无评论,发表第一条评论吧!</p>
                    {% endfor %}
                </div>
            </div>
        </div>
    </div>
    
    <!-- 侧边栏 -->
    <div class="col-md-4">
        <!-- 作者信息 -->
        <div class="card mb-4">
            <div class="card-header">作者信息</div>
            <div class="card-body text-center">
                <img src="{{ post.author.profile_pic.url|default:'static/img/default-avatar.jpg' }}" 
                     class="rounded-circle mb-3" width="100" height="100" alt="">
                <h5>{{ post.author.username }}</h5>
                <p>{{ post.author.bio|default:"这个人很懒,什么都没写..." }}</p>
                <p>文章数: {{ post.author.post_set.count }}</p>
            </div>
        </div>
        
        <!-- 相关文章 -->
        <div class="card">
            <div class="card-header">相关文章</div>
            <div class="card-body">
                <ul class="list-group list-group-flush">
                    {% for related_post in related_posts %}
                        <li class="list-group-item">
                            <a href="{% url 'post-detail' related_post.pk %}">{{ related_post.title }}</a>
                            <p class="text-muted mb-0">
                                <small>{{ related_post.created_at|date:"Y-m-d" }}</small>
                            </p>
                        </li>
                    {% empty %}
                        <li class="list-group-item">暂无相关文章</li>
                    {% endfor %}
                </ul>
            </div>
        </div>
    </div>
</div>

{% block extra_js %}
<script>
    // 回复功能的交互逻辑
    document.addEventListener('DOMContentLoaded', function() {
        const replyButtons = document.querySelectorAll('.reply-btn');
        
        replyButtons.forEach(button => {
            button.addEventListener('click', function() {
                const commentId = this.getAttribute('data-comment-id');
                const replyForm = document.getElementById(`reply-form-${commentId}`);
                
                // 切换显示/隐藏回复表单
                if (replyForm.style.display === 'none') {
                    replyForm.style.display = 'block';
                } else {
                    replyForm.style.display = 'none';
                }
            });
        });
    });
</script>
{% endblock %}
{% endblock %}

六、数据库集成

Django的ORM系统使数据库操作变得简单而优雅。

1. 数据库配置

# pythonblog/settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',  # 开发环境使用SQLite
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# 生产环境可以使用MySQL
"""
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'pythonblog',
        'USER': 'bloguser',
        'PASSWORD': 'your_secure_password',
        'HOST': 'localhost',
        'PORT': '3306',
    }
}
"""

2. 模型创建与迁移

# 创建迁移文件
python manage.py makemigrations

# 应用迁移
python manage.py migrate

# 创建超级用户
python manage.py createsuperuser

3. 管理后台配置

# blog/admin.py
from django.contrib import admin
from .models import Post, Category, Tag

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'category', 'created_at', 'updated_at', 'views')
    list_filter = ('category', 'created_at')
    search_fields = ('title', 'content')
    date_hierarchy = 'created_at'
    filter_horizontal = ('tags',)
    readonly_fields = ('views',)

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'description')
    search_fields = ('name',)

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ('name',)
    search_fields = ('name',)

七、集成测试

测试是确保应用质量的重要环节,Django提供了强大的测试框架。

1. 模型测试

# blog/tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from blog.models import Post, Category, Tag

User = get_user_model()

class PostModelTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # 创建测试用户
        test_user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpassword'
        )
        
        # 创建测试分类
        test_category = Category.objects.create(
            name='测试分类',
            description='这是一个测试分类'
        )
        
        # 创建测试标签
        test_tag = Tag.objects.create(name='测试标签')
        
        # 创建测试文章
        test_post = Post.objects.create(
            title='测试文章',
            content='这是一篇测试文章的内容。',
            author=test_user,
            category=test_category
        )
        test_post.tags.add(test_tag)
    
    def test_post_content(self):
        post = Post.objects.get(id=1)
        self.assertEqual(post.title, '测试文章')
        self.assertEqual(post.content, '这是一篇测试文章的内容。')
        self.assertEqual(post.author.username, 'testuser')
        self.assertEqual(post.category.name, '测试分类')
        self.assertEqual(post.tags.first().name, '测试标签')
        self.assertEqual(post.views, 0)
        
    def test_post_str_method(self):
        post = Post.objects.get(id=1)
        self.assertEqual(str(post), '测试文章')
        
    def test_get_absolute_url(self):
        post = Post.objects.get(id=1)
        self.assertEqual(post.get_absolute_url(), '/post/1/')

2. 视图测试

# blog/tests/test_views.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
from blog.models import Post, Category, Tag

User = get_user_model()

class PostViewsTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # 创建测试用户
        cls.test_user = User.objects.create_user(
            username='testuser',
            email='test@example.com',
            password='testpassword'
        )
        
        # 创建测试分类
        cls.test_category = Category.objects.create(
            name='测试分类',
            description='这是一个测试分类'
        )
        
        # 创建测试标签
        cls.test_tag = Tag.objects.create(name='测试标签')
        
        # 创建测试文章
        cls.test_post = Post.objects.create(
            title='测试文章',
            content='这是一篇测试文章的内容。',
            author=cls.test_user,
            category=cls.test_category
        )
        cls.test_post.tags.add(cls.test_tag)
    
    def setUp(self):
        self.client = Client()
    
    def test_post_list_view(self):
        response = self.client.get(reverse('blog-home'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')
        self.assertTemplateUsed(response, 'blog/home.html')
        
    def test_post_detail_view(self):
        response = self.client.get(reverse('post-detail', args=[1]))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, '测试文章')
        self.assertContains(response, '这是一篇测试文章的内容。')
        self.assertTemplateUsed(response, 'blog/post_detail.html')
        
        # 测试阅读量增加
        post = Post.objects.get(id=1)
        self.assertEqual(post.views, 1)
        
    def test_post_create_view(self):
        # 测试未登录用户无法访问
        response = self.client.get(reverse('post-create'))
        self.assertNotEqual(response.status_code, 200)
        
        # 测试登录用户可以访问
        self.client.login(username='testuser', password='testpassword')
        response = self.client.get(reverse('post-create'))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, 'blog/post_form.html')
        
        # 测试创建文章
        post_data = {
            'title': '新测试文章',
            'content': '这是一篇新的测试文章内容。',
            'category': self.test_category.id,
            'tags': [self.test_tag.id]
        }
        response = self.client.post(reverse('post-create'), post_data)
        self.assertEqual(Post.objects.count(), 2)
        new_post = Post.objects.get(title='新测试文章')
        self.assertEqual(new_post.author, self.test_user)

八、部署上线

将应用部署到生产环境是全栈开发的最后一步。

1. 生产环境设置

# pythonblog/settings.py

# 生产环境设置
DEBUG = False
ALLOWED_HOSTS = ['www.pythonplanet.com', 'pythonplanet.com']

# 静态文件设置
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_collected')

# 媒体文件设置
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# 安全设置
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 3600
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

2. Docker部署

创建Dockerfile文件:

FROM python:3.10-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN python manage.py collectstatic --noinput

EXPOSE 8000

CMD ["gunicorn", "pythonblog.wsgi:application", "--bind", "0.0.0.0:8000"]

创建docker-compose.yml文件:

version: '3'

services:
  web:
    build: .
    restart: always
    volumes:
      - static_data:/app/static_collected
      - media_data:/app/media
    depends_on:
      - db
    environment:
      - DB_HOST=db
      - DB_NAME=pythonblog
      - DB_USER=bloguser
      - DB_PASSWORD=your_secure_password
      - SECRET_KEY=your_secret_key
      - DEBUG=False
  
  db:
    image: mysql:8.0
    restart: always
    volumes:
      - db_data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=pythonblog
      - MYSQL_USER=bloguser
      - MYSQL_PASSWORD=your_secure_password
      - MYSQL_ROOT_PASSWORD=mysql_root_password
  
  nginx:
    image: nginx:latest
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
      - static_data:/var/www/static
      - media_data:/var/www/media
      - ./nginx/ssl:/etc/nginx/ssl
    depends_on:
      - web

volumes:
  db_data:
  static_data:
  media_data:

配置Nginx(nginx/nginx.conf):

server {
    listen 80;
    server_name pythonplanet.com www.pythonplanet.com;
    
    # 重定向HTTP到HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name pythonplanet.com www.pythonplanet.com;
    
    ssl_certificate /etc/nginx/ssl/pythonplanet.crt;
    ssl_certificate_key /etc/nginx/ssl/pythonplanet.key;
    
    # SSL配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    # 静态文件
    location /static/ {
        alias /var/www/static/;
        expires 30d;
    }
    
    # 媒体文件
    location /media/ {
        alias /var/www/media/;
        expires 30d;
    }
    
    # 主应用
    location / {
        proxy_pass http://web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

3. 启动服务

# 构建并启动容器
docker-compose up -d

# 执行数据库迁移
docker-compose exec web python manage.py migrate

# 创建超级用户
docker-compose exec web python manage.py createsuperuser

4. Python星球博客系统界面预览

为了让您更好地理解博客系统的外观和操作流程,我将为您展示几个关键页面的界面效果图。这些图示展示了系统运行后的实际效果,帮助您直观地了解用户交互体验。

1️⃣博客系统首页

这是用户访问博客时看到的首页界面。呈现了最新文章列表、分类和标签等核心功能。

在这里插入图片描述

2️⃣文章详情页

这是用户点击某篇文章后看到的详情页面,展示了完整的文章内容、作者信息以及相关文章推荐。

在这里插入图片描述

3️⃣管理员后台界面

Django提供了强大的管理后台功能,管理员可以通过这个界面对博客系统的所有内容进行管理,包括文章、用户、评论等。

在这里插入图片描述

4️⃣文章创建页面

这是用户创建新文章的界面,提供了直观的编辑器和相关选项,方便用户发布内容。

在这里插入图片描述

5️⃣用户个人主页

这是用户的个人中心页面,展示了用户的基本信息和已发布的文章列表,方便用户管理自己的内容。

在这里插入图片描述

通过以上界面效果图,我们可以看到我们的博客系统已经实现了一个功能完善的全栈应用:

  1. 博客首页:展示了文章列表、分类导航和热门标签,提供良好的浏览体验
  2. 文章详情页:清晰展示文章内容、作者信息和相关文章推荐,增强用户粘性
  3. 后台管理界面:提供强大的内容管理功能,方便管理员高效运营网站
  4. 文章创建页面:提供直观的编辑器和操作界面,降低内容创作门槛
  5. 用户个人中心:便于用户管理个人资料和已发布内容

这个系统基于我们在前面课程中学习的Django框架构建后端,使用Bootstrap实现响应式前端设计,SQLite/MySQL作为数据库存储。通过这个项目,我们将前面学习的各种技术点进行了综合运用,实现了从前端到后端的完整开发流程。

九、总结与进阶

我们成功构建了一个全栈博客系统,它具备完整的前后端功能和数据库交互。

1. 项目亮点

  • 完整的用户认证与授权
  • 响应式前端设计
  • RESTful API设计规范
  • 完善的数据库模型
  • 良好的代码组织结构
  • Docker容器化部署

2. 进阶方向

  • 添加用户通知系统
  • 集成Markdown编辑器
  • 实现文章搜索功能(ElasticSearch)
  • 添加文章统计分析
  • 优化网站性能(缓存、CDN等)
  • 实现CI/CD自动化部署

十、今日练习

现在,让我们开始实践吧!按照以下步骤完成今天的全栈项目开发练习:

1. 创建项目框架

   - 使用Django创建项目结构
   - 配置数据库连接
   - 设计数据模型

2. 实现核心功能

   - 用户认证系统
   - 文章CRUD操作
   - 评论系统
   - 前端页面设计

3. 优化和测试

   - 添加单元测试
   - 优化用户体验
   - 确保响应式设计

4. 部署项目

   - 准备部署环境
   - 配置服务器
   - 上线应用

挑战任务

尝试为博客系统添加以下高级功能中的一个或多个:

- 文章点赞系统
- 用户关注功能
- 文章订阅功能
- 图片上传与管理
- 站内搜索优化

参考资源

  1. Django官方文档: https://docs.djangoproject.com/
  2. Bootstrap文档:https://getbootstrap.com/docs/5.3/
  3. MDN Web文档:https://developer.mozilla.org/
  4. SQLite文档:https://www.sqlite.org/docs.html
  5. Git版本控制: https://git-scm.com/doc
  6. Docker容器化: https://docs.docker.com/

结语

通过这个全栈项目,我们已经将前面所学的Python基础Web开发数据库前后端分离等知识进行了综合应用。全栈开发需要不断实践和学习,希望这个项目能够帮助你巩固知识,并为你的Python学习之旅添加一份成就感!

记得将你的项目分享到GitHub,这不仅是对自己学习成果的展示,也是展示你的技能的好方式。

创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊)
如果你对今天的内容有任何问题,或者想分享你的学习心得,欢迎在评论区留言讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code_流苏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值