刚把一个花卉商城的Django毕设完整交付出去,包括程序、文档、代码讲解和后续的一条龙调整,整个过程让我想把这套项目的设计和实现好好梳理一遍。如果你正在准备毕业设计,或者想找一个完整的Django实战项目来练手,这篇内容应该能帮你省下大量翻文档、踩坑的时间。
这个花卉商城系统听起来就是一个典型的电商项目,但它在毕设选题里其实非常讨巧:既有功能复杂度,又不会大到没法在几个月内完成,而且展示性强、答辩时好讲。下面我会从需求拆解、数据库设计、核心功能实现、完整实操流程到常见坑位排查,尽量把整套项目的来龙去脉说得透彻一点。很多东西是我实际写代码、调页面、跑部署时一遍遍试出来的,不是教科书上的标准答案,但对做毕设的人参考价值会更高。
1. 项目整体设计与模块拆解
1.1 核心需求画像
做毕设项目最忌讳一上来就写代码,先想清楚这项目到底要解决什么问题。花卉商城系统本质上是线上卖花,但毕设场景下,它需要覆盖完整的电商闭环:用户能浏览花卉商品、查看分类、搜索、加入购物车、生成订单、模拟支付,管理员能维护商品信息、处理订单状态、管理用户。这些模块串起来,才能证明你掌握了从数据库到后端再到前端的一套完整能力。
我在拆解需求时,把用户角色分成三类:
- 游客:可以浏览商品、查看详情,无法下单,通常用来展示系统的引流能力。
- 注册用户:登录后能管理购物车、下单、查看历史订单、维护个人信息。
- 管理员:登录django自带的后台或自建管理界面,进行商品上下架、库存管理、订单发货等操作。
这三类角色覆盖了权限管理的核心逻辑,答辩时老师问起来也能说出个所以然。
1.2 为什么选Django而非Flask或SpringBoot
经常有人问我毕设选什么框架,我的建议是:除非你的选题是前后端分离 + 高并发,否则Django永远是性价比最高的选择。道理很简单:Django自带Admin后台、ORM、认证系统、模板引擎、表单处理、静态文件管理,这些全是电商类项目的基础组件,能省掉大把手写时间。
对比一下Flask,Django就像是自带精装修的房子,Flask则是清水房。Flask灵活但需要自己搭结构,对毕设来说时间成本太高;SpringBoot虽然企业认可度更高,但Java的学习曲线和配置复杂度,对多数非科班或者基础一般的同学并不友好。
还有一个现实因素:Django的ORM和Admin是答辩利器。你不需要写一堆SQL就能展示数据关系,后台界面直接给老师演示商品管理,视觉效果好,代码量还少。
1.3 项目目录结构与App划分
我习惯把项目拆成四个应用(app),分别是用户、商品、购物车、订单。如果你做的是一个更大一点的商城,可以再加一个统计或者促销的app,但毕设不需要过度设计。
flower_shop/ # 项目根目录 ├── manage.py ├── flower_shop/ # 项目配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── users/ # 用户管理:注册、登录、个人信息 │ ├── goods/ # 商品管理:分类、商品、库存 │ ├── cart/ # 购物车:增删改查、数量调整 │ └── orders/ # 订单:创建、查询、状态流转 ├── templates/ # 共用模板 ├── static/ # 静态文件:css、js、images └── media/ # 用户上传图片这种结构的好处是职责分明,调试时定位问题快,论文里写系统设计也清晰。很多同学喜欢把所有逻辑堆在一个app里,最后几千行代码挤在一起,改一处崩三处,毕业答辩现场演示翻车就太难受了。
2. 数据库模型设计:从商品到订单的完整闭环
2.1 用户模型的扩展方案
Django自带的User模型能用,但字段太少,手机号、收货地址、头像全没有。我的做法是建立一个Profile关联表,用OneToOneField指向内置User,把额外信息放子表里。这样不破坏Django的认证机制,又能扩展业务字段。
from django.contrib.auth.models import User from django.db import models class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="用户") phone = models.CharField(max_length=11, blank=True, verbose_name="手机号") avatar = models.ImageField(upload_to="avatar/", blank=True, verbose_name="头像") address = models.CharField(max_length=200, blank=True, verbose_name="收货地址") created_time = models.DateTimeField(auto_now_add=True, verbose_name="注册时间") class Meta: verbose_name = "用户信息" verbose_name_plural = verbose_name def __str__(self): return self.user.username这里有个细节:ImageField需要安装Pillow库,否则迁移时会报错。我先在虚拟环境里pip install pillow,再跑makemigrations,避免迁移中断。
为什么用扩展表而不是直接继承AbstractUser?如果你项目刚起步,用AbstractUser自定义用户表确实更优雅。但毕设通常已经用到Django内置的用户登录、Admin管理,贸然换用户模型需要重写大量引用关系,风险高。Profile扩展是兼容性最稳妥的方案,答辩时也讲得出设计理由。
2.2 商品分类与商品信息的字段选择
花卉商品比普通数码产品多几个特殊字段:是否热门、上架时间、库存数量、销量、原价和现价,以及一个用于首页大图展示的banner标记。
class Category(models.Model): name = models.CharField(max_length=50, verbose_name="分类名称") parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE, verbose_name="父级分类") class Meta: verbose_name = "分类" verbose_name_plural = verbose_name def __str__(self): return self.name class Flower(models.Model): name = models.CharField(max_length=100, verbose_name="商品名称") category = models.ForeignKey(Category, on_delete=models.CASCADE, verbose_name="所属分类") price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="售价") original_price = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name="原价") stock = models.IntegerField(default=0, verbose_name="库存") sales = models.IntegerField(default=0, verbose_name="销量") image = models.ImageField(upload_to="flower/", verbose_name="商品图片") description = models.TextField(blank=True, verbose_name="商品描述") is_hot = models.BooleanField(default=False, verbose_name="是否热门") is_banner = models.BooleanField(default=False, verbose_name="是否轮播展示") created_time = models.DateTimeField(auto_now_add=True, verbose_name="上架时间")价格字段用DecimalField而不用FloatField,这算是电商项目的老规矩。浮点数做金额运算会出现0.1+0.2不等于0.3的问题,真到了订单结算环节会出大乱子。DecimalField在Python里也是Decimal类型,配合decimal模块做金额计算才是安全的。
你还可以在模型里加一个漂亮的@property,直接返回格式化后的价格字符串,模板里调用就会更省事。
@property def format_price(self): return f"¥{self.price}"2.3 购物车与订单的关联设计
购物车我没有建数据库表,而是用了session存储。理由很直接:购物车属于临时数据,用户可能只是逛逛,没必要每次操作都写入数据库。但如果项目要求用户在不同设备间同步购物车,那还是要建表。
订单设计上,我拆成了订单主表和订单明细表。主表存一次购买行为的总金额、收货信息、订单状态,明细表存每一件花卉的单价和数量。这样设计是符合电商标准模型的,论文的数据库ER图也好画。
class Order(models.Model): STATUS_CHOICES = ( ("pending", "待付款"), ("paid", "已付款"), ("shipped", "已发货"), ("completed", "已完成"), ("cancelled", "已取消"), ) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") order_no = models.CharField(max_length=32, unique=True, verbose_name="订单编号") total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总金额") receiver_name = models.CharField(max_length=50, verbose_name="收货人") receiver_phone = models.CharField(max_length=11, verbose_name="联系电话") receiver_address = models.CharField(max_length=200, verbose_name="收货地址") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending", verbose_name="订单状态") remark = models.TextField(blank=True, verbose_name="订单备注") created_time = models.DateTimeField(auto_now_add=True, verbose_name="下单时间") class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items", verbose_name="所属订单") flower = models.ForeignKey(Flower, on_delete=models.CASCADE, verbose_name="商品") price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="成交单价") quantity = models.IntegerField(default=1, verbose_name="购买数量")订单号我直接用uuid生成,再转成字符串,去掉横线,加上时间前缀。这样即使同一秒有多笔订单,也不会出现主键冲突。
2.4 Django Admin后台的定制思路
如果你做的是普通后台展示,Django自带的Admin已经很有排面了。但直接裸用会有两个问题:一是列表页显示的是默认的object标题,看不出商品名;二是分类筛选和搜索没配好,管理员用起来很别扭。
简单的定制就是注册模型时指定list_display、list_filter、search_fields。
from django.contrib import admin from .models import Flower, Category class FlowerAdmin(admin.ModelAdmin): list_display = ("name", "category", "price", "stock", "sales", "is_hot", "created_time") list_filter = ("category", "is_hot", "is_banner") search_fields = ("name", "description") list_editable = ("price", "stock", "is_hot") list_per_page = 20 admin.site.register(Flower, FlowerAdmin)list_editable这个配置挺好用的,它允许管理员在列表页直接改价格和库存,不用点进详情,效率高很多。给老师演示的时候,这个交互效果也比默认后台强不少。
3. 核心功能实现与代码讲解
3.1 登录注册与会话管理
Django自带的认证系统提供了authenticate和login方法,不用自己写加密逻辑。注册时我一般用内置的UserCreationForm作为基础,再自己写一个注册表单集成Profile字段。
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User from django import forms from .models import Profile class RegisterForm(UserCreationForm): phone = forms.CharField(max_length=11, required=True, label="手机号") email = forms.EmailField(required=False, label="邮箱") class Meta: model = User fields = ("username", "phone", "email") def save(self, commit=True): user = super().save(commit=False) if commit: user.save() Profile.objects.create(user=user, phone=self.cleaned_data["phone"], email=self.cleaned_data.get("email")) return user这里有个小小的细节:email字段在User模型里本来就有,直接在RegisterForm里声明就能覆盖。phone字段我单独存在Profile表里,所以save方法里要同时写两个表。这种操作在答辩时也是常见的提问点,最好能讲清楚为什么不能把所有字段都塞进User表。
登录视图使用内置的LoginView就可以,但我一般重写模板为login.html,并且在登录成功后用next参数把用户带回他原来想访问的页面。这个体验比固定跳转到首页好很多。
3.2 商品列表、分类筛选与详情页
商品列表页用ListView,配合django-filter或者手动写queryset过滤。我在项目里是手动过滤,因为逻辑很简单:
class FlowerListView(ListView): model = Flower template_name = "goods/flower_list.html" context_object_name = "flowers" paginate_by = 12 def get_queryset(self): queryset = Flower.objects.filter(stock__gt=0) category_id = self.kwargs.get("category_id") keyword = self.request.GET.get("keyword", "") sort = self.request.GET.get("sort", "default") if category_id: queryset = queryset.filter(category_id=category_id) if keyword: queryset = queryset.filter(name__icontains=keyword) if sort == "price_asc": queryset = queryset.order_by("price") elif sort == "price_desc": queryset = queryset.order_by("-price") elif sort == "sales": queryset = queryset.order_by("-sales") return queryset排序这里我喜欢加一个switch式的映射,这样用户点一次排序,URL上出现sort=price_asc,模板里再根据当前sort高亮对应的按钮。这个交互不难,但能有效提升项目完成度的观感。
详情页则是简单DetailView,但我会额外做两个事情:一个是在模板里展示同类花卉推荐,另一个是点击加入购物车时通过AJAX提交,不刷新页面试验。
function addToCart(flowerId) { fetch(`/cart/add/${flowerId}/`, { method: "POST", headers: { "X-CSRFToken": document.querySelector("[name=csrfmiddlewaretoken]").value } }) .then(response => response.json()) .then(data => { if (data.code === 200) { alert("加入购物车成功"); document.getElementById("cart_count").innerText = data.cart_total; } }); }3.3 购物车逻辑:session操作还是数据库操作
我前面提到毕设项目我用session存购物车,这里把具体写法放出来。Django的session本质上就是字典,在视图里直接操作即可。
def cart_add(request, flower_id): flower = get_object_or_404(Flower, pk=flower_id) cart = request.session.get("cart", {}) if str(flower_id) in cart: if cart[str(flower_id)]["count"] < flower.stock: cart[str(flower_id)]["count"] += 1 else: cart[str(flower_id)] = { "name": flower.name, "price": str(flower.price), "image": flower.image.url if flower.image else "", "count": 1, } request.session["cart"] = cart request.session.modified = True return JsonResponse({"code": 200, "cart_total": sum(item["count"] for item in cart.values())})注意修改session之后一定要设置request.session.modified = True,否则Django可能认为session没有变化,不写入存储。这个问题特别隐蔽,我见过好几个人的购物车点击加购后刷新就消失,就是这个原因。
购物车页面的核心操作是加减数量、删除商品、计算总金额。总金额我选择在模板层算,因为数据量小,但更规范的做法是在视图里先聚合好传给模板。如果你要写论文,建议后者,代码逻辑更清晰,也方便写单元测试。
3.4 订单流程:结算、生成、模拟支付
下单是购物车的终点,也是最容易出bug的地方。基本流程是这样:
- 确认登录状态,未登录的用户跳转到登录页。
- 从购物车会话中读取商品列表,逐件校验库存是否足够。
- 创建订单主表记录,填写收货人信息。
- 为每一件商品创建订单明细。
- 扣减商品库存,清空购物车。
- 跳转到一个模拟支付页面,点击确认支付后把订单状态从pending改成paid。
第2步一定要做,否则会出现用户下了单但库存不够的尴尬情况。我在项目里也把超卖问题处理了一下,具体写法可以这样:
from django.db import transaction @transaction.atomic def create_order(request): cart = request.session.get("cart", {}) if not cart: return JsonResponse({"code": 400, "msg": "购物车为空"}) user = request.user total_amount = 0 order_items = [] for str_id, item in cart.items(): flower = Flower.objects.select_for_update().get(pk=int(str_id)) if flower.stock < int(item["count"]): return JsonResponse({"code": 400, "msg": f"{flower.name}库存不足"}) flower.stock -= int(item["count"]) flower.sales += int(item["count"]) flower.save() total_amount += flower.price * int(item["count"]) order_items.append(OrderItem(flower=flower, price=flower.price, quantity=int(item["count"]))) order = Order.objects.create( user=user, order_no=generate_order_no(), total_amount=total_amount, receiver_name=request.POST.get("receiver_name"), receiver_phone=request.POST.get("receiver_phone"), receiver_address=request.POST.get("receiver_address"), status="pending", ) OrderItem.objects.bulk_create(order_items) request.session["cart"] = {} request.session.modified = True return JsonResponse({"code": 200, "order_id": order.id})select_for_update是行级锁,确保在高并发下不会多个用户同时读到相同库存然后各自扣减,这就是防止超卖的关键一行。虽然毕设未必会遇到并发测试,但答辩时提到事务与锁,老师会认为你有生产意识。
模拟支付就很简单了,直接一个视图把订单状态改成paid,顺便把付款时间记上。如果你愿意加一点展示效果,可以跳到模板页里做一个倒计时和下单成功动画,整体观感会很加分。
3.5 用户中心与订单管理
用户中心负责展示个人信息、我的订单、收藏之类的功能。我的做法是写一个AccountView,集中处理三个tab页的信息,减少视图数量。
订单列表页要区分状态,我用Django的if标签在模板里做筛选。订单详情里把订单主表信息和明细表一起展示,注意在查询时用select_related把user和flower表都join进来,避免循环查数据库。
def order_detail(request, order_id): order = get_object_or_404(Order.objects.select_related("user").prefetch_related("items__flower"), pk=order_id, user=request.user) return render(request, "orders/order_detail.html", {"order": order})prefetch_related这个细节很多人会忽略,但面试或答辩时拿出来讲,就是加分项。它能一次把订单所有明细连同花卉商品一起查出来,而不是每循环一个明细就发一次SQL。
4. 从零实操:一条龙搭建流程
4.1 环境准备与虚拟环境
做这个项目之前,先确认你的Python版本在3.8以上,Django版本我建议4.2 LTS或者3.2 LTS。Django 4.2是目前比较稳的长期支持版,第三方库兼容性好,网上教程也多。
我习惯用venv建虚拟环境,简单不折腾。
python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install django pillow如果你的项目还要做验证码、图表展示之类,可以加装captcha和matplotlib,但这些都不是必需品,按需装就行。
4.2 创建项目和App
django-admin startproject flower_shop cd flower_shop python manage.py startapp users python manage.py startapp goods python manage.py startapp cart python manage.py startapp orders创建App后,记得在settings.py的INSTALLED_APPS里注册。同时把语言改成中文、时区改成上海。
LANGUAGE_CODE = "zh-hans" TIME_ZONE = "Asia/Shanghai" USE_TZ = TrueUSE_TZ建议保持True,但要注意数据库里的DateTimeField在写入时会转成UTC,展示时模板里会转回本地时间,如果你发现自己存的时间比实际慢了8小时,不用慌,这是时区转换机制,不是bug。
4.3 配置数据库:SQLite还是MySQL
毕设项目直接用SQLite就够了,零配置、文件即数据库、答辩现场不怕连不上。但如果你的项目要演示给老师看,并且数据量比较大,MySQL会让论文更有分量。
Django连接MySQL需要在settings里改配置:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "flower_shop", "USER": "root", "PASSWORD": "yourpassword", "HOST": "127.0.0.1", "PORT": "3306", } }同时需要安装pymysql,并在项目__init__.py里设置:
import pymysql pymysql.install_as_MySQLdb()这个操作说多了都是泪。以前MySQLdb库在Python3下兼容性不好,pymysql就是替代品,装好后记得unicode编码要统一,否则会出现中文乱码。
4.4 数据迁移和创建超级用户
模型写好后,执行迁移:
python manage.py makemigrations python manage.py migrate如果你新增了app但没写任何模型,makemigrations时会提示没有变化,这是正常的,不用慌。
创建管理员:
python manage.py createsuperuser用户名和密码自定义,邮箱可以留空。这个账号用来登录Django Admin后台。
4.5 模板与静态文件配置
模板路径我在settings里统一指到根目录的templates文件夹,静态文件指到static文件夹。
TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR / "static"] MEDIA_URL = "/media/" MEDIA_ROOT = BASE_DIR / "media"这里有个常见的坑:DEBUG=True时,Django能直接跑静态文件,但部署到服务器时经常出现样式全丢的情况。所以我在项目里加了静态文件收集配置,后续部署时跑collectstatic就能解决。
4.6 运行开发服务器与调试
python manage.py runserver浏览器打开http://127.0.0.1:8000,就能看到系统首页。如果你在管理后台上传商品图片后,详情页图片加载不出来,多半是没在urls.py里加media的路由。
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)这个路由只会在开发和DEBUG环境下生效,生产环境要用Nginx托管media文件,这块我在常见问题章节再细说。
5. 实战中常见的坑与排查技巧实录
5.1 找不到模板或静态文件404
症状:点击页面后报TemplateDoesNotExist,或者浏览器里css文件出现404。
排查思路分三步:
- 第一步看settings里TEMPLATES的DIRS是否包含templates目录路径。
- 第二步看app目录下是否创建了templates/应用名/模板名的三层结构,比如goods/templates/goods/flower_list.html。
- 第三步看静态文件在浏览器里直接访问URL是否可加载,如果静态文件404,先去settings确认STATICFILES_DIRS配置项。
实际排查过程中,我发现九成的问题都是路径写错。Django模板加载规则是先查DIRS,再查每个app的templates目录,如果你在两个地方都有同名模板,会优先加载DIRS里的,很容易出现改了A文件但页面显示B文件的情况。
5.2 外键查询导致的N+1性能问题
在商品列表页关联分类名称时,很多人会写成:
for flower in flowers: category_name = flower.category.name如果flowers有100条,这条查询会执行100次,非常要命。正确的做法是使用select_related。
flowers = Flower.objects.select_related("category").all()这样Django会用join把category一次取出来,查询次数从101降到1。虽然毕设数据量小看不到速度差,但答辩时这一条问答就能体现出你对性能优化的理解。
5.3 中文乱码和数据编码问题
Django 4.x默认全中文没问题,但有些老项目或从别处拷贝的数据库会出现"??"或者UnicodeDecodeError。
最常见的原因是MySQL数据库或表的编码不是utf8mb4。在建库时就指定:
CREATE DATABASE flower_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;写连接时也可以在OPTIONS里加charset:
"OPTIONS": {"charset": "utf8mb4"}开发中还要注意Python文件头部的编码注释,虽然Python3默认utf-8,但Windows下用记事本保存文件容易产生BOM头,导致首行报错。建议直接用VS Code或PyCharm写代码,别用记事本。
5.4 页面POST表单报403 CSRF验证失败
这个坑每个Django新手都会遇到。解决方案有几种:
- 在模板的表单里加
{% csrf_token %}。 - 如果是AJAX提交,在headers里带上X-CSRFToken。
- 暂时在View上加@csrf_exempt,但这种方法不推荐,毕设答辩被问到安全性容易吃亏。
我实际写项目时,前端模板里统一加了模板标签,AJAX部分统一通过一个JS函数读取cookie中的csrftoken。这样只要后端逻辑不变,前端所有POST请求都是安全的,也不会报403。
5.5 订单状态异常与库存不一致
假设用户下了一单但没支付,数据库里库存先扣了,如果用户不支付,库存一直占着。更合理的做法是:下单时不扣库存,只是锁定库存;支付成功后再真实扣减。我在毕设里简化成了“下单即扣库存”,因为缺少超时取消订单的异步任务。
如果你想让项目在答辩时更有亮点,可以加一个订单超时自动取消的定时任务,存一个过期时间字段,每次用户访问订单列表时扫描一次,将过期的pending订单改为cancelled并恢复库存。这个功能代码量不大,但讲“系统健壮性”时非常加分。
我实际写的时候,用了Django-Crontab或者Celery都可以,但Celery对毕设来说太重了。最轻量的是在自定义中间件里做一个简单的过期检查,或者干脆在每次下单/查看购物车时触发一次检查函数。
5.6 部署上线的内存和路径坑
如果你要把项目放到云服务器上展示,我强烈建议先跑一遍collectstatic:
python manage.py collectstatic把静态文件统一收集到STATIC_ROOT指定的目录里,再由Nginx转发。直接用runserver撑生产环境不仅性能差,而且静态文件管理混乱。
再有就是路径分隔符的问题,Windows下开发用的是反斜杠,Linux服务器上是正斜杠。所有自定义路径都尽量用PurePath拼接,不要写死“/home/user”这种地址。
5.7 答辩现场演示的稳定性建议
最后说一个比较务实的点:答辩前把用到的数据准备好,比如花卉商品图片、分类、热门标记、几笔模拟订单,提前都录入到数据库里,不要把现场时间浪费在添加数据上。
我当时还把下单流程里的支付环节改成了一键模拟,避免演示时因为网络问题卡在支付页面。这种细节考虑越多,答辩过程就越顺。有时候老师没耐心看完整流程,你只要把最关键的注册、商品浏览、下单、后台管理这几个页面切换得干净利落,就已经足够证明你的工作量了。
写在最后
我实际带过的毕设项目里,花卉商城算是综合体验很好的类型。它不像社交或内容平台那样需要复杂推荐算法,也不像管理系统那样缺乏商业闭环感,电商的完整链路做下来,从需求到设计再到编码,你都走了一遍,毕业设计无非就是想让你经历这个过程。
如果你打算从零开始做这套系统,我的建议是先把数据库模型图手绘出来,再去动代码。模型定好了,后面的视图和模板就像填格子一样顺畅。代码遇到问题也别太焦虑,Django的错误提示已经很友善了,按着报错堆栈逐行找,基本上都能解决。真解决不了的,把问题拆成最小复现,缩小到某个函数或某个模板标签,再搜索时会精准得多。