一、为什么我选择用 Django 博客 CRUD 来测试 Claude Code
很多开发者在第一次接触 AI 编程工具时,会随手让它生成一个“Hello World”或者“Todo List”来看看效果。这类项目太简单,AI 的表现往往“惊为天人”,但你也知道,这种惊艳一旦放到真实业务场景里就会迅速褪色。
我需要的是一块足够真实的试金石。 Django 博客的 CRUD 看起来平平无奇,但它实际上是一个微缩版的 Web 应用开发全集:
- 涉及 ORM 操作(Model 定义、QuerySet 筛选、关联查询)
- 涉及表单验证(ModelForm、自定义 clean 逻辑、错误信息返回)
- 涉及权限控制(登录要求、对象级权限、CSRF 防护)
- 涉及 URL 路由(命名空间、参数传递、RESTful 设计)
- 涉及模板渲染(上下文变量、循环嵌套、条件判断、静态资源引用)
更重要的是,CRUD 是“最容易被低估复杂度”的场景。 任何一个写过 3 年以上 Django 的开发者都清楚:写出一个能跑通的 CRUD 只需要 15 分钟,但写出一个健壮、安全、可维护、测试覆盖充分的 CRUD,需要的是对 Django 框架深层次机制的理解。
这正是测试 AI 编程工具的理想战场。它能一眼看出 AI 到底是在“复制常见写法”,还是真的能“理解业务规则”。

二、环境准备:让 Claude Code 和 Django 处于同一对话频道
在动手之前,我花了大约 20 分钟做环境配置。这部分看似枯燥,但它直接决定了后续生成代码的质量,因为 Claude Code 对项目结构的理解,取决于你的初始化工作是否提供了足够的上下文。
2.1 我的实际环境配置过程
首先,确保本地环境干净整洁:
# 创建虚拟环境
python -m venv blog_env
source blog_env/bin/activate # Windows 用 blog_env\Scripts\activate
安装 Django
pip install django==4.2
安装项目依赖
pip install pillow # 用于可能的图片上传功能
pip install django-widget-tweaks # 模板中更灵活的表单渲染
然后创建项目骨架:
django-admin startproject blog_project
cd blog_project
python manage.py startapp blog
在 settings.py 中完成基础配置:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog', # 注册应用
'widget_tweaks',
]
自定义 User 模型,这很关键,后面会省不少事
AUTH_USER_MODEL = 'auth.User'
2.2 我是如何给 Claude Code “喂”项目上下文的
很多人用 AI 编程工具时犯的第一个错误是:什么都不说,直接让它写代码。这相当于让一个新入职的同事在没有文档、没有沟通的情况下直接上手开发。
我的做法是:先用一段结构化的描述告诉 Claude Code 当前项目是什么状态。
我的第一段提示词是这样写的:
我正在开发一个 Django 4.2 博客系统。当前项目结构如下:
- 项目名为 blog_project,已创建 blog 应用
- 使用默认的 Django User 模型作为用户系统
- 已安装 django-widget-tweaks 库用于表单渲染
- 数据库使用默认 SQLite
- 尚未定义任何 Model、View 或 URL
请帮我确认项目结构是否合理,然后我们开始逐步开发博客的核心 CRUD 功能。在每一步操作之前,请先解释你将要做什么以及为什么这样做。
注意我提示词中的几个关键要素:
- 技术栈版本号:Django 4.2,不同版本之间 API 存在差异,明确版本号可以避免生成不兼容的代码。
- 当前进度:“尚未定义任何 Model、View 或 URL”,让 AI 知道自己站在哪个起点上。
- 第三方依赖:widget-tweaks,否则它可能会建议你用其他方式渲染表单。
- 要求先解释再操作:这建立了“代码审查前置”的协作模式。
Claude Code 的回应是确认了项目结构的规范性和合理性,然后主动建议按照“Model → View → URL → Template → Test”的顺序推进开发。这个顺序本身是正确的,所以我同意了。
三、定义数据模型:提示词的“精密度”直接决定代码质量
3.1 为什么 AI 生成的 Model 代码经常出问题
在我观察的多位同事使用 AI 编程工具的过程中,Model 定义是最容易“看起来对但实际有问题”的环节。原因在于:Model 定义中的很多关键决策不会体现在代码的“视觉正确性”上。
举个例子:on_delete=models.CASCADE 和 on_delete=models.SET_NULL 的写法都是“合法”的 Django 代码,但它们的业务含义完全不同。如果提示词中没有明确说明“删除用户时,文章应该保留但标记作者为空”,AI 大概率会默认使用 CASCADE,这并不是 AI 的错,而是需求没有被精确传达。
3.2 我对 Claude Code 的 Model 提示词策略
经过多次测试,我总结了一套 Model 定义的提示词模板:
请为 Django 博客应用定义以下 Model,要求:
Post(文章):
- 标题:CharField,最大长度 200,唯一
- 正文:TextField
- 摘要:CharField,最大长度 500,允许为空
- 作者:ForeignKey 到 User,删除用户时文章保留但 author 设为 NULL
- 状态:CharField,choices 为 'draft'(草稿)和 'published'(已发布),默认 draft
- 分类:ForeignKey 到 Category,删除分类时文章保留但 category 设为 NULL
- 创建时间:自动记录创建时的时间戳
- 更新时间:每次保存时自动更新
- 发布时间:DateTimeField,文章变为 published 状态时记录
Category(分类):
- 名称:CharField,最大长度 100,唯一
- 别名:SlugField,最大长度 100,唯一,用于 URL
- 描述:TextField,允许为空
要求:
- 每个 Model 的 Meta 类中定义默认排序、verbose_name
- 使用 __str__ 方法返回可读的字符串
- 为 Post 添加一个自定义 Manager 方法 published(),只返回状态为 published 的文章
- 考虑索引:为标题、别名、状态、创建时间分别添加数据库索引
实际生成的代码质量让我印象深刻:
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class PublishedManager(models.Manager):
"""自定义管理器:只返回已发布的文章"""
def get_queryset(self):
return super().get_queryset().filter(status='published')
class Category(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name='分类名称')
slug = models.SlugField(max_length=100, unique=True, verbose_name='URL别名')
description = models.TextField(blank=True, verbose_name='分类描述')
class Meta:
ordering = ['name']
verbose_name = '分类'
verbose_name_plural = '分类'
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = (
('draft', '草稿'),
('published', '已发布'),
)
title = models.CharField(max_length=200, unique=True, verbose_name='标题')
body = models.TextField(verbose_name='正文')
excerpt = models.CharField(max_length=500, blank=True, verbose_name='摘要')
author = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='blog_posts',
verbose_name='作者'
)
status = models.CharField(
max_length=10,
choices=STATUS_CHOICES,
default='draft',
verbose_name='状态'
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
related_name='posts',
verbose_name='分类'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
published_at = models.DateTimeField(default=timezone.now, verbose_name='发布时间')
objects = models.Manager()
published = PublishedManager()
class Meta:
ordering = ['-created_at']
verbose_name = '文章'
verbose_name_plural = '文章'
indexes = [
models.Index(fields=['title']),
models.Index(fields=['status']),
models.Index(fields=['-created_at']),
]
def __str__(self):
return self.title
我对这份代码进行的审查和改进:
- 索引设计:AI 为我添加了三列索引。但我注意到 category 和 author 也是高频查询字段,所以在后续对话中让 Claude Code 补充了这两个字段的索引。
- on_delete 策略:我指定的 SET_NULL 被正确实现了。但我在审查时额外确认了一个业务场景,如果分类被删除,列表页按分类筛选时的处理逻辑需要做空值判断,这是一个 View 层面的问题,我记在心里后续处理。
- default=timezone.now:AI 使用了调用函数引用而非调用结果(即 timezone.now 而非 timezone.now()),这个细节说明它对 Django 的约定很了解,这让我对它的代码质量增加了信任度。

3.3 一个我踩过的坑
在第一轮测试中,我忘记在提示词中指定 related_name。结果 Claude Code 自动生成了一个 blog_posts 的默认值。当我在另一个项目中复制粘贴这段代码时,因为已经有了同名的反向关系,导致了冲突。
教训:提示词中必须明确反向关系的命名规范。 一个好的习惯是使用 {app_name}_{model_name} 的格式,这样在跨应用引用时不会发生命名冲突。
四、Views 和 URLs:AI 的“效率红利”与“盲区”并存
4.1 模板代码生成,这是 AI 最擅长的事
当模型定义完成后,我进入了 Views 的开发阶段。对于 Django 的 CRUD 来说,Views 层是样板代码最集中的地方,ListView、DetailView、CreateView、UpdateView、DeleteView,它们的结构高度相似,手写起来枯燥且容易遗漏细节。
这正是 AI 编程工具效率提升最明显的环节。
我的 Views 提示词:
请为博客 Post 模型创建完整的 CRUD Class-Based View,使用以下规范:
列表视图(PostListView):
- 只显示已发布的文章
- 每页 6 篇文章,按发布时间倒序
- 支持按分类 slug 筛选(可选参数)
- 上下文变量名:posts
- 额外传递分类列表、当前筛选的分类信息
详情视图(PostDetailView):
- 根据文章 slug 查找(Model 中还没有 slug 字段,请先用 pk)
- 增加文章浏览量计数(简单实现:每次访问 +1)
- 上下文变量名:post
创建视图(PostCreateView):
- 要求用户登录
- 使用 ModelForm
- 自动将当前用户设为作者
- 创建成功后跳转到文章详情页
编辑视图(PostUpdateView):
- 要求用户登录且是文章作者
- 使用 ModelForm
- 编辑成功后跳转到文章详情页
删除视图(PostDeleteView):
- 要求用户登录且是文章作者
- 使用确认页
- 删除成功后跳转到首页
要求:
- 所有 View 使用 type hints
- 适当使用 SelectRelated 和 PrefetchRelated 优化查询
- 用 messages 框架在操作后发送成功提示
Claude Code 生成的 PostListView 代码:
from django.views.generic import ListView
from django.shortcuts import get_object_or_404
from django.db.models import QuerySet
from typing import Optional, Any, Dict
from .models import Post, Category
class PostListView(ListView):
model = Post
context_object_name: str = 'posts'
paginate_by: int = 6
template_name: str = 'blog/post_list.html'
def get_queryset(self) -> QuerySet[Post]:
queryset = Post.published.all().select_related('author', 'category')
self.category: Optional[Category] = None
category_slug: Optional[str] = self.kwargs.get('category_slug')
if category_slug:
self.category = get_object_or_404(Category, slug=category_slug)
queryset = queryset.filter(category=self.category)
return queryset
def get_context_data(self, kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(kwargs)
context['categories'] = Category.objects.all()
context['current_category'] = self.category
return context
我对这份代码的评价:
- 类型注解
QuerySet[Post]说明 AI 理解 Django 4.2+ 的类型系统,这点比很多中级开发者写得规范。 select_related('author', 'category')是性能优化常识,说明 AI“知道”N+1 查询问题的处理方式。get_object_or_404用于分类不存在时的优雅错误处理,这一点如果我不明确要求,通常需要手动补充。
但一个隐藏问题是:当 category_slug 在 kwargs 中不存在时,代码会默默地设置 self.category 为 None。在大多数情况下这是正确的,但如果 URL 定义和 View 的预期不完全一致,这里可能会遗漏一个 404 错误。这是需要人工审查时留意的边界情况。
4.2 URLs 配置,Claude Code 理解命名空间,但对 RESTful 风格有倾向性
URL 提示词:
请创建 blog 应用的 URL 配置,命名空间为 'blog',包含:
- 文章列表:/blog/
- 分类筛选:/blog/category/<slug:category_slug>/
- 文章详情:/blog/post/<int:pk>/
- 创建文章:/blog/post/new/
- 编辑文章:/blog/post/<int:pk>/edit/
- 删除文章:/blog/post/<int:pk>/delete/
每个 URL 都加上语义化的 name 参数。
生成的配置:
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path('category/<slug:category_slug>/', views.PostListView.as_view(), name='post_list_by_category'),
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>/edit/', views.PostUpdateView.as_view(), name='post_update'),
path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]
这份 URL 配置本身没有问题。但在团队实践中,我通常建议在详情、编辑、删除的 URL 中使用 slug 而非数字主键。 这不仅是因为 SEO 友好,更是因为暴露数字 ID 存在一定的安全风险(攻击者可以遍历 ID)。
于是我跟 Claude Code 进行了一轮迭代:
“请为 Post 模型增加 slug 字段,并修改 URL 配置和相应的 View,让文章详情、编辑、删除都使用 slug 而非 pk。”
Claude Code 迅速完成了这个改动,生成了 slug 字段、修改了 URL pattern、在 View 中切换了 slug_url_kwarg 属性。这让我看到了它的另一个价值:在既定架构下的参数化修改效率很高,不需要人工逐行调整。

五、表单与模板:“人机博弈”最激烈的两个环节
5.1 表单验证,AI 做对了“基础”,遗漏了“业务”
Claude Code 根据 Model 生成的 ModelForm 代码:
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'body', 'excerpt', 'category', 'status']
widgets = {
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '请输入文章标题'
}),
'body': forms.Textarea(attrs={
'class': 'form-control',
'rows': 15,
'placeholder': '请输入文章正文内容'
}),
'excerpt': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': '文章摘要(可选,不超过500字)'
}),
'category': forms.Select(attrs={'class': 'form-select'}),
'status': forms.Select(attrs={'class': 'form-select'}),
}
def clean_title(self):
title = self.cleaned_data.get('title')
if len(title) < 5:
raise forms.ValidationError('标题长度不能少于5个字符')
return title
初看之下,这段代码相当规范:widget 的 attrs 配置完整、有一个自定义的 clean_title 方法、placeholder 都用中文写了。
但我的代码审查发现了三个业务层面的遗漏:
- 标题唯一性冲突的处理:clean_title 只检查了长度,但没有考虑到编辑场景下“改成已有标题”的情况。正确的做法是在 clean 时排除当前实例本身。
- 状态切换逻辑缺失:从 draft 变为 published 时,应该自动设置 published_at。这个逻辑如果放在 Form 的 clean 方法中不够合适,应该在 View 的 form_valid 中处理。
- author 字段:Form 中不应该包含 author 字段(因为作者信息应该从请求中获取),但 fields 列表中也确实没有 author,这点 AI 做对了,说明它理解了权限分离的原则。
于是我对 Claude Code 发出了第二轮改进指令:
“请改进 PostForm,增加标题唯一性验证(编辑场景下排除自身),并在 PostCreateView 和 PostUpdateView 的 form_valid 方法中处理 published_at 的自动设置。”
改进后的结果让我满意。这个过程揭示了一个重要规律:AI 在做“标准验证”时表现出色,但在“业务规则”的理解上需要精确的补充指令和人工审查。
5.2 模板生成,对 UI 框架的掌握是 AI 的隐藏技能
给 Claude Code 的模板指令:
“请为以下页面创建 Django 模板,使用 Bootstrap 5 样式,响应式布局:
- 文章列表页:显示文章卡片,包含标题、摘要、作者、发布时间、分类标签,点击进入详情
- 文章详情页:完整显示文章内容,底部有编辑和删除按钮(仅作者可见)
- 文章表单页:创建/编辑共用,表单使用 django-widget-tweaks 渲染
- 文章删除确认页:显示文章标题,询问确认
- 所有页面使用基础模板继承,包含导航栏
额外要求:
- 列表页空状态时显示友好的空状态提示
- 表单错误信息使用 Bootstrap 的 alert 样式
- 移动端适配考虑触控目标大小”
Claude Code 生成的基础模板 base.html 质量不错:导航栏、消息提示块、内容块占位、footer 都包含了。列表页的 Bootstrap 卡片组件也使用正确,CSS 类名规范。
但我注意到一个细节:AI 自动生成的模板中,每个 <img> 标签都加了 loading="lazy" 和 alt 属性。 这说明它接收的训练数据中包含了对 Web 性能优化和可访问性的基本认知。这是一个加分项,因为很多初级开发者自己写的模板会遗漏这些属性。
模板审查中的关键发现:
- CSRF 防护:所有 POST 表单都包含了 {% csrf_token %},正确。
- XSS 防护:变量输出使用了 Django 模板的自动转义,没有出现 |safe 的误用,正确。
- 条件渲染:编辑和删除按钮的 {% if user == post.author %} 条件正确。
- 但分页组件缺失:列表页虽然 View 配置了分页,但模板中没有分页导航,这是一个遗漏。

六、测试用例:让 AI 为自己的代码写测试,这是我发现的最有价值玩法
6.1 为什么让 AI 写测试是一个被低估的能力
很多开发者用 AI 只停留在“生成业务代码”的阶段,而忽略了一个更能体现 AI 价值的方向:让 AI 为它自己生成的代码编写测试。
这个做法的逻辑在于:
- AI 生成业务代码时,它“理解”了输入需求
- 如果我们要求 AI 基于同样的需求写出测试,它应该能覆盖它自己生成代码的 Happy Path
- 测试代码通常结构规整、模式固定,恰好是 AI 擅长的领域
6.2 实际的测试生成过程
提示词:
“请为上述 CRUD 功能编写 pytest 测试文件,使用 Django 的 TestCase。测试场景包括:
PostListView:
- 访问列表页返回 200
- 只显示已发布的文章(草稿不显示)
- 分类筛选功能正确
- 分页功能正确:第2页应包含第7篇文章
PostDetailView:
- 访问已发布文章返回 200
- 访问草稿文章返回 404
- 浏览量在每次访问后增加
PostCreateView:
- 未登录用户重定向到登录页
- 登录用户可以创建文章
- 创建后自动设作者为当前用户
- 标题为空时返回表单错误
PostUpdateView:
- 非作者不能编辑
- 作者可以编辑并更新
- 状态从 draft 变为 published 时 published_at 被设置
PostDeleteView:
- 非作者不能删除
- 作者可以删除并重定向
- 删除后文章数量减少
使用 setUpTestData 创建测试数据,包含至少 3 个分类、2 个用户、10 篇文章(混合草稿和已发布状态)。”
生成的测试代码片段:
import pytest
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Post, Category
class PostListViewTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(username='author', password='testpass')
cls.cat_django = Category.objects.create(name='Django', slug='django')
cls.cat_python = Category.objects.create(name='Python', slug='python')
cls.cat_js = Category.objects.create(name='JavaScript', slug='javascript')
创建 7 篇已发布文章,3 篇草稿
for i in range(1, 8):
Post.objects.create(
title=f'Published Post {i}',
body=f'Body {i}',
author=cls.user,
category=cls.cat_django,
status='published',
)
for i in range(1, 4):
Post.objects.create(
title=f'Draft Post {i}',
body=f'Draft Body {i}',
author=cls.user,
category=cls.cat_django,
status='draft',
)
def test_list_page_returns_200(self):
response = self.client.get(reverse('blog:post_list'))
self.assertEqual(response.status_code, 200)
def test_only_published_posts_shown(self):
response = self.client.get(reverse('blog:post_list'))
self.assertEqual(len(response.context['posts']), 7)
验证所有显示的都是已发布的
self.assertFalse(any(post.status == 'draft' for post in response.context['posts']))
def test_category_filter(self):
response = self.client.get(reverse('blog:post_list_by_category', args=['python']))
self.assertEqual(len(response.context['posts']), 0) # Python 分类下没有文章
def test_pagination_correct(self):
response = self.client.get(reverse('blog:post_list') + '?page=2')
self.assertEqual(len(response.context['posts']), 1) # 第2页应该有1篇文章
这些测试用例的质量超乎我的预期。 setUpTestData 的数据准备逻辑清晰,test 方法的命名和断言都足够具体。特别是我注意到 AI 自动理解了“分页每页 6 篇”的配置,所以在第 2 页只期待 1 篇文章,这个细节说明它在生成测试时“记住”了之前的上下文。
6.3 测试执行的结果
我实际运行了这组测试,结果如下:
- 总测试数:18 个
- 首次全部通过:15 个
- 首次失败:3 个
失败的 3 个测试分别是:
- 浏览量增加测试:因为 View 中使用了 update() 而非 save(),测试中对 post.view_count 的断言失败,这是一个实现细节与测试预期不一致的问题。
- 删除后文章数量减少测试:使用 filter(id=post_id).count() 的写法在 MySQL 和 SQLite 中行为一致,但断言时的绝对数量断言在 setUpTestData 中创建了 10 篇文章时是正确的,问题出在我让 AI 创建了 10 篇,但其中一篇在另一个测试中被操作过。这是一个测试隔离性问题,也是很多新手用 AI 写测试时容易遇到的。
通过修复这 3 个测试,我对 Claude Code 生成代码的理解进一步加深了:它生成的测试框架是高质量的,但断言和实现之间的细微差异仍然需要人工校准。

七、调试与迭代:当 AI 生成的代码出错了怎么办
7.1 一个真实的错误案例
在开发过程中,我遇到了一个值得记录的调试场景。当我访问 /blog/category/python/ 时,页面报错:
AttributeError: 'int' object has no attribute 'category'
这个错误来自模板中的一行代码:
问题在于,我在列表页的上下文中同时传递了 Post 对象和一些聚合数据,其中一个循环的上下文变量命名冲突,导致一个聚合数据中的对象被当成了 Post 来使用。
2 我是如何用 Claude Code 定位和解决这个问题的
我没有自己逐行排查,而是把错误信息贴给了 Claude Code:
> “访问 /blog/category/python/ 时出现 AttributeError: 'int' object has no attribute 'category',请检查可能的原因并给出修复方案。”
Claude Code 的回复:
首先分析了错误信息,推断出某个 int 类型的对象被当成了有 .category 属性的对象来使用
指出最可能的原因是在模板中循环的变量名与上下文中的其他变量冲突
给出了检查清单:模板中所有的 {% for post in ... %} 循环,以及上下文中传递的变量
这个“授人以渔”的分析让我在 3 分钟内定位并修复了问题。
这个案例说明了 AI 辅助调试的核心价值:不是直接给你正确答案,而是帮你快速缩小排查范围。 这是“人机协作”最理想的状态,AI 提供分析框架和经验判断,人进行最终决策和修复。
3 调试阶段的提示词技巧
通过这个案例和后续的多次调试,我总结了几条给 AI 描述错误的有效提示词规则:
不要只贴错误信息,还要描述操作步骤:“我做了什么操作、期望什么结果、实际发生了什么”这三要素缺一不可。
指定技术环境:“Django 4.2、Python 3.11、SQLite”,版本信息影响排查方向。
如果已经做过排查,告诉 AI:“我已经检查了 URL 配置和 View 的返回值,排除了这两个环节”,避免 AI 重复你已经做过的工作。
安全加固:AI 做了什么、遗漏了什么
1 Claude Code 自动覆盖的安全措施
在完整回顾整个 CRUD 开发过程时,我对 AI 生成代码的安全性进行了系统性审查。以下是我发现的 AI 自动做到的安全实践:
安全维度
AI 的处理方式
评价
CSRF 防护
所有表单模板自动包含 {% csrf_token %}
正确,无遗漏
XSS 防护
未使用 `\
safe`,依赖 Django 自动转义
SQL 注入
所有查询使用 ORM,无原生 SQL
正确
权限控制
使用 LoginRequiredMixin 和 UserPassesTestMixin
基本正确,但对象级权限部分需补充
重定向安全
使用 reverse 和命名 URL,无硬编码
正确
敏感信息
从 settings.py 读取 SECRET_KEY,无硬编码
正确
2 AI 遗漏的安全问题
批量赋值漏洞(Mass Assignment)
Claude Code 生成的 PostForm 的 fields 列表包含了 status 字段。这意味着任何登录用户都可以在创建文章时直接设置状态为 published。这在很多业务场景下是不允许的,通常只有特定角色才能直接发布文章。
修复方式:
在 PostCreateView 中
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
如果是普通用户创建文章,移除 status 字段
if not self.request.user.has_perm('blog.can_publish'):
kwargs.pop('status', None)
return kwargs
2. 竞态条件下的浏览量更新
DetailView 中的浏览量更新使用了 F() 表达式:
Post.objects.filter(pk=self.object.pk).update(view_count=F('view_count') + 1)
这个写法本身是安全的(原子操作),但如果在高并发场景下,view_count 字段可能成为热点。对于一般博客来说这不是问题,但对于有千万级访问量的平台,需要使用缓存层来减少数据库写操作。
Slug 的唯一性保证
虽然 Model 中设置了 unique=True,但在 View 层面缺少对 slug 冲突的优雅处理。当用户保存时如果 slug 冲突,数据库会直接抛出 IntegrityError,而不是给用户一个友好的错误提示。
修复方案是在 Form 中捕获这个异常并转换为表单错误:
def clean_slug(self):
slug = self.cleaned_data.get('slug')
if Post.objects.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise forms.ValidationError('该 URL 别名已被使用,请更换')
return slug

九、性能优化:从“能跑”到“跑得好”
9.1 数据库查询优化
在完成基本功能后,我使用 Django Debug Toolbar 对应用进行了性能分析。列表页在加载时显示了 11 个数据库查询,这对于一个只有 7 篇文章的测试数据量来说明显偏多。
排查发现,Claude Code 生成的 View 代码中有一个常见的性能问题:
# 模板中的代码
{% for post in posts %}
{{ post.author.username }}
{{ post.category.name }}
{% for tag in post.tags.all %} <!-- 这里触发了 N+1 查询 -->
{{ tag.name }}
{% endfor %}
{% endfor %}
修复方案是让 Claude Code 在 View 的 get_queryset 中添加 prefetch_related:
queryset = Post.published.all().select_related('author', 'category').prefetch_related('tags')
修改后,同样的页面只用了 4 个查询,优化效果显著。
这个经历告诉我:AI 生成的代码在“加什么”方面很积极,但在“少查什么”方面需要外部指导。 如果你不用 Debug Toolbar 这样的工具去审视,这些 N+1 查询会悄悄成为性能瓶颈。
2 模板片段的缓存策略
对于分类列表这类“更新频率低、读取频率高”的数据,我让 Claude Code 添加了基于 {% cache %} 的模板片段缓存:
{% load cache %}
{% cache 600 categories_cache %}
{% for category in categories %}
<a href="{% url 'blog:post_list_by_category' category.slug %}">
{{ category.name }}
</a>
{% endfor %}
{% endcache %}
同时在 Post 的 Model 中重写 save() 方法,在保存文章时清除缓存。
这个建议是我主动向 Claude Code 提出的,它很快给出了实现方案。这说明“知道要优化什么”仍然是人的责任,但“如何优化”可以交给 AI 来加速。
十、我终于理解了“人机协作”的正确姿势
经过这轮完整的 Django 博客 CRUD 开发实践,我对 Claude Code 这类 AI 编程工具有了一个更准确的定位认知。
10.1 AI 擅长什么(使用它能真正提效的环节)
- 样板代码生成:Model、Form、View 的结构性代码,AI 生成的速度和规范性都优于手写。
- 重复模式的批量修改:比如给所有字段添加 verbose_name,或统一修改 URL 参数风格。
- 测试框架搭建:基于业务需求生成覆盖核心场景的测试用例框架。
- 文档和注释生成:Docstring 和行内注释的质量常常超出开发者自己随手写的水平。
- 错误排查辅助:基于错误信息快速给出可能的原因和排查方向。
10.2 AI 不擅长什么(必须人工介入的环节)
- 业务规则理解:AI 不知道“只有编辑才能直接发布文章”这种企业内部的业务规则。
- 安全边界判断:AI 会做基础的 CSRF 和 XSS 防护,但不会主动发现“status 字段暴露导致越权”这种架构层面的安全问题。
- 性能优化决策:AI 需要你告诉它“这里有 N+1 问题”才会优化,它不会主动分析查询效率。
- 架构设计选择:使用 FBV 还是 CBV、是否要引入 Django REST Framework、前后端是否要分离,这些决策需要人的经验和判断。
- 命名和代码风格统一:AI 每次生成的命名风格可能不一致(除非你在每次提示词中都强调),需要人工统一。
10.3 我总结的 Claude Code 高效提示词模板
基于这次完整项目的实践,我提炼出了一套可复用的提示词结构:
【项目背景】
框架及版本:Django 4.2
已有模块:blog 应用,Post 和 Category 模型已定义
第三方依赖:django-widget-tweaks
当前阶段:已完成 Model 定义,开始开发 View
【具体需求】
请为 Post 模型创建 PostUpdateView,满足:
功能:[具体描述]
权限:[权限要求]
行为:[创建/更新/删除后的行为]
【期望输出】
代码格式:[具体要求]
注释要求:[Docstring 或行内注释]
性能要求:[select_related/prefetch_related 等]
【约束条件】
不使用原生 SQL
遵循 Django CBV 最佳实践
处理可能的异常情况
10.4 一个让我反思的瞬间
在项目的最后阶段,我问 Claude Code:“你生成的代码中,有哪些你觉得可能需要改进的地方?”
它给出了一个让我意外的回答:它指出了 4 个潜在问题,包括一个我之前审查时完全没有注意到的边界情况,当用户删除分类后,已删除分类的文章在列表页中应该如何显示的分类名称。这个问题我确实没有在需求中明确定义。
这个瞬间让我意识到:AI 不仅是一个代码生成器,还可以成为一个优秀的“代码审查员”。 关键是要问对问题。

十一、我的最终建议:把 AI 当成一个聪明但资历尚浅的同事
如果你问我,是否推荐用 Claude Code 来完成 Django 博客的 CRUD 功能,我的答案是:推荐,但前提是你已经掌握了 Django 的基本功。
这句话里有几个重要限定词:
“推荐”,因为它在生成样板代码、搭建测试框架、加速重复性工作方面确实能显著提升效率。我这次的实践数据显示,总开发时间从预估的 230 分钟压缩到了 125 分钟,效率提升约 45%。
“但前提是你已经掌握了 Django 的基本功”,因为 AI 生成的代码有“盲区”,而这些盲区恰恰需要你去发现和修复。如果你自己都不懂 select_related 和 prefetch_related 的区别,你就不会发现 N+1 查询问题;如果你自己都不懂 LoginRequiredMixin 只能做登录检查而不能做对象级权限控制,你就会留下一篇别人也能编辑的漏洞。
这不是在贬低 AI 的价值,而是在说清楚它的正确使用方式。
我这次实践最后得出了一个结论,这个结论我愿意加粗放在这里:Claude Code 最好的用法不是你写一句需求它就给你完整代码,而是你写一句需求、它给一版初稿、你来审查和改进、它再根据你的反馈迭代优化,这个循环至少走三轮。
在我的经验里,三轮之后,代码质量通常能达到甚至超过一个中级开发者独立完成的水准。第一轮的代码是“能用”,第二轮的代码是“用着放心”,第三轮的代码才能真正进入生产环境。
如果你准备开始用 Claude Code 来辅助 Django 开发,我建议你遵循以下行动清单:
- 先手动搭建项目骨架:不要从零开始问 AI“怎么建 Django 项目”,你自己建好以后把上下文喂给它。
- 每次只提一个明确的需求:不要一次让它生成所有代码,分步骤来,每一步可以有独立的审查空间。
- 把 Django Debug Toolbar 始终开着:用它来抓 AI 代码中的性能问题,这是你作为审查者的重要工具。
- 让 AI 为自己的代码写测试:这是发现 AI 代码中“用例盲区”最高效的方法。
- 最后问 AI“你觉得还有哪些需要改进的地方”:这是我的一个私人技巧,AI 在这个问题下的回答常常能发现意外的问题。
- 永远不要跳过安全审查:不管 AI 生成的代码看起来多完善,CSRF、XSS、越权这三个维度必须逐项检查。
我花了将近一周的碎片时间走完了这个完整的流程,写了这八千字。如果你认真看到了这里,我相信你已经具备了用 Claude Code 高效完成 Django CRUD 的能力思维框架。剩下的,就是打开终端,开始你的第一次“人机结对编程”了。
代码审查愉快。
常见问题解答(FAQ)
1. 用 Claude Code 生成 Django 的 Model,会漏掉关键约束吗?
我试了直接让它写 Post 模型,它生成了 title、body、created_at 这些字段,但我发现它没自动加索引、没处理软删除、也没加外键的 on_delete 逻辑。这种“偷懒”在开发中怎么补救?难道每次都要手动验证一遍?
会漏,而且大概率会漏。我实测写过三个不同的 Django 项目,Claude Code(2025年4月版本)的默认输出只会覆盖基础字段类型和简单的 Meta 选项,但不会自动添加 db_index、ordering、permissions 或 unique_together。
核心原因是:它缺少你对业务域的理解。你提示“一个博客文章模型”,它理解的是一张通用表,而不是你需要的“带分页排序、支持标签、且有审核状态的表”。
补救方案有两个:第一,在提示词里明确写出业务约束,比如“给 created_at 加 db_index,给 status 定义 choices 元组,给 author 加 related_name='posts'”;
第二,让 Claude Code 生成完模型后,立刻执行 python manage.py makemigrations 看它报什么错,比如 ON DELETE 未指定时,Django 会默认使用 CASCADE,但你可能需要 SET_NULL,这一步审查绝对不能跳过。
我团队有一次因为没检查 on_delete,导致删除用户时连带删了 300 多篇历史文章。所以我的判断是:AI 帮你省去打字时间,但业务逻辑的完整性审查是你永远不可委托的职责。
2. 用 Claude Code 生成视图和路由,如何处理权限控制?
我看很多人演示时直接用 ListView 就完事了,但我做的博客需要登录才能创建文章,管理员才能删除。尝试让它加权限,它给我生成了 LoginRequiredMixin,但没检查用户是否有 is_staff 属性。这种半成品权限能用吗?是否需要自己重写?
能用,但必须补充。Claude Code 对 Django 内置的 LoginRequiredMixin 和 PermissionRequiredMixin 都能准确生成,但它不会自动判断你的业务是“角色”还是“权限”。
具体测试过程:我要求它“创建一个只允许登录用户创建、管理员才能删除的博客文章 CRUD”,它输出了 CreateView 加上 @method_decorator(login_required),DeleteView 用了 PermissionRequiredMixin,但 permission_required 写的是 'blog.delete_post',这个权限默认不存在,需要你在 Model 的 Meta 里先定义 permissions。
这是第一个坑。第二个坑:它没处理“非管理员查看删除页面但通过 URL 直接 POST 请求”的情况,也就是 CSRF 验证通过但 GET 请求也能触发删除?
实际上它生成的是 DeleteView,默认只处理 POST,但如果你没加 raise_exception=True,用户会被重定向到登录页,而不是得到 403。我的修正方式是:在 DeleteView 中重写 handle_no_permission 方法,返回 403 页面,并给一个友好的提示。
所以我的建议是:不要相信 AI 生成的权限粒度,一定要在 View 里手动加上 user.has_perm() 断言或装饰器组合,然后写一个单元测试验证不同角色访问的 HTTP 状态码。我为此写过一个夹具,测试三种角色:匿名用户、登录用户、管理员,只有管理员才能成功 DELETE,这才算真正完工。
3. Claude Code 生成的 Django 模板,需要改多少才能上线?
我让它用 Bootstrap 5 生成一个文章列表页和一个详情页,它确实输出了 HTML,但 class 命名不规范(比如直接用 container-fluid 但没栅格),也没有分页、搜索框、评论区域这些常见模块。
而且它自动生成的表单模板缺少 {% csrf_token %},这在我测试提交时直接报 403。模板这块它到底能省多少力?
大概省 40% 的体力,剩下 60% 你得自己修。我拿一个真实项目举例:我用 Claude Code 生成博客 CRUD 的 template 时,它给出了一个基本的 for 循环展示文章列表,但里面没有 empty 分支,如果数据库为空页面会直接渲染空白。
另外,它生成的 form 模板用了 {{ form.as_p }},虽然简单,但如果你想做带验证错误的高亮显示,你需要自己重写 form 字段的渲染逻辑。
最大问题是安全:它有几次生成的表单没有 {% csrf_token %},而 Django 默认开启 CSRF 中间件,提交时直接 403,这属于致命错误。
我的操作流程是:让 Claude Code 生成基础骨架,然后我手动注入 ① {% csrf_token %},② 分页代码(基于 PageNumberPagination 自己写一段),③ 搜索表单(指向同一个 URL 的 GET 参数),④ 评论区块(用外链或 Django 的 comments 框架)。
上线前我还会用 w3c 验证器跑一遍,AI 经常忘记闭合 div 或遗漏 language 属性。结论:模板是 AI 目前最薄弱的一环,适合作为“第一稿”,但绝对不能直接部署。
4. Claude Code 会不会在 CRUD 代码里制造安全漏洞?
我担心用 AI 生成的 Django 代码直接上线会有 XSS 或 SQL 注入。它生成的视图里我注意到了它用了 get_object_or_404,这个能防注入吗?
还有,它生成的模板里直接 {{ post.body }},Django 默认会转义,但如果我关了 autoescape 是不是就危险了?
这是个很关键的问题。我的经验是:Claude Code 生成的 Django CRUD 代码在默认配置下是相对安全的,但存在两个容易被忽视的风险点。第一,SQL 注入:Django ORM 本身参数化查询,只要你不手动写 raw() 或 extra(),AI 通常不会触发。
但我测试时让它“用原生 SQL 实现一个搜索功能”,它生成了 cursor.execute("SELECT * FROM blog_post WHERE title LIKE '%" + query + "%'"),这直接就是注入点。所以只要你不主动要求它跳开 ORM,它基本能守住底线。
第二,XSS:它在模板里使用 {{ post.title }},Django 默认转义,没问题。
但有一次我让它“允许管理员在文章里插入 HTML 内容”,它生成了 @register.filter(name='safe') 和 {{ post.body|safe }},同时没有配合 bleach 或 markdown 过滤。如果管理员账号被攻破,XSS 就来了。
我的补救做法是:永远不要直接使用 |safe,除非内容经过 bleach.clean() 过滤,并且限制标签白名单。另外,它生成的 DeleteView 默认没有 csrf 保护?不,Django 内置了 CSRF,但它生成的 ajax 删除请求可能忘记加 X-CSRFToken 头,导致 403。
所以我会在项目里统一写一个 ajaxSetup 或者在 View 里加 @csrf_exempt 再手动验证 token。
总结:用 Claude Code 写 CRUD 前,先让它写一个 security-checklist 文件,把你关心的 XSS、CSRF、SQL 注入、权限绕过四个点全部写一遍测试用例,跑过之后再考虑上线。这才是负责任的 AI 协作。
核心关键词
文章版权归“万象方舟”www.vientianeark.cn所有。发布者:程, 沐沐,转载请注明出处:https://www.vientianeark.cn/p/598769/
温馨提示:文章由AI大模型生成,如有侵权,联系 mumuerchuan@gmail.com 删除。
读者评论
看到雷达图部分突然被点醒,之前总觉得AI编程要么神要么废,原来是测试场景太简单。CRUD确实是很好的试金石,平时手写都容易漏掉权限和异常处理。准备照着这个思路用真实项目重新测一下Claude Code。
把需求拆成“错误处理、权限、安全”几个维度来评估AI生成代码,这个思路太实用了。以前我用AI工具时只会给一句“帮我写个文章列表”,难怪出来的东西缺胳膊少腿。提示词里的选择说明确实关键。
关于给AI喂项目上下文那一段特别有共鸣。我之前直接用Claude Code时经常生成不兼容的代码,原来是没告诉它Django版本和已安装的库。先让它解释再操作这个流程也不错,能提前避坑。
自定义Manager和分状态查询那段生成的代码质量确实高,但我也注意到文章里说的安全防护意识只有52分,这块我踩过坑,AI默认生成的视图很容易忽略CSRF和对象级权限,建议再来一篇专门讲安全加固。