
去年带团队做了一个面向广告策划师的项目协作平台技术栈就是标题里这两个关键词Python和Django。简单说这是一个把广告策划需求从“客户拍脑袋”到“设计师交方案”全流程线上化的网站设计师可以在上面认领策划任务、提交创意草案、跟客户来回对稿最终沉淀出可复用的方案库。这篇博文就把整个项目的设计思路、核心模块的实现细节和踩过的坑完整复盘一遍既有代码也有思路适合正在用Django做企业级业务系统的朋友参考尤其是刚接触Django项目实战的新手可以少走很多弯路。1. 项目定位与核心需求拆解1.1 这是一个什么性质的网站先把这个项目说透。它不是一个展示作品的“作品集网站”也不是一个卖模板的商城而是一个内部业务协作平台。使用角色分为三类客户、设计师、平台管理员。客户在网站上发布广告策划需求比如“某饮料品牌夏季新品推广全案”设计师浏览需求池认领适合自己的单子方案做完提交上去客户在线查看并给出修改意见管理员负责审核需求、指派订单、统计平台数据。这样的定位决定了技术选型和功能设计的方向必须有完善的用户体系和权限控制因为三类角色的操作边界完全不同必须有清晰的状态流转因为一个订单从发布、认领、制作、提交、修改到验收涉及的节点非常多必须有可靠的文件管理因为方案通常是一堆PPT、PDF、图片素材。标题里“的设计师”三个字我理解成两层含义第一层是这个网站为设计师服务解决他们找活、交活、改稿的痛点第二层是这个网站本身的设计师——也就是我作为开发者在设计整个系统时考虑最多的就是“设计师”这个核心用户的操作体验。下文所有的模块拆解都是围绕这两个含义展开的。1.2 用户角色与权限边界权限设计是这个项目最基础也最容易出错的地方。Django自带的User模型只满足登录、登出、管理员后台这类基础功能但区分“客户”和“设计师”不能靠改一个字段而是要用用户Profile扩展 分组权限的组合方案。我采用的是继承AbstractUser的方式在用户模型上增加user_type字段用IntegerField配合choices实现三类身份标识再用Django的PermissionMixin做基本授权。这样做的优势很明显以后想给用户加头像、手机号、公司名称等资料字段直接往Profile模型里加就行不会动到原生User表结构迁移也非常干净。权限校验我尽可能放在视图层的装饰器里而不是散落在模板里判断。比如只有user_typeDESIGNER的用户才能访问“设计师工作台”客户只能看到“需求列表”和“我的订单”。模板端只用{% if %}判断要不要显示某个入口按钮真正的数据保护和越权拦截全部走视图装饰器。这个原则一定要坚持否则一旦模板判断出错数据就裸奔了。1.3 核心业务流程图景推演拿一个完整的订单来推演业务流会帮助你理解这个项目为什么需要那么多功能模块而不是照抄教程里的博客demo。客户注册登录后在工作台填写广告需求表单内容包括品牌名称、产品类型、预算区间、期望周期、参考案例链接等。提交后订单状态为“待审核”。管理员在后台审核需求是否合规如果品牌词违规或者预算明显低于市场价可以直接驳回订单状态变为“已驳回”审核通过后变为“待认领”。设计师在需求池里看到并认领订单变为“制作中”。设计师上传初版方案订单变为“待验收”。客户在线查看方案并填写修改意见订单退回变为“修改中”。设计师改完重新提交再次进入“待验收”。直到客户点“验收通过”订单变为“已完成”。这条流转线看起来简单实际编码时牵扯到状态枚举、操作日志、通知提醒、文件版本管理四个子问题。状态枚举我用了IntegerField配合choices状态变化在模型层统一封装方法处理避免到处写死数字。操作日志用一张单独的表记录“谁在什么时间对哪个订单做了什么”客户和设计师扯皮时可以快速定位。通知提醒先用简单的站内信模型后续想上邮件或短信直接加后端逻辑就行。文件版本管理是我一开始忽略后来不得不补的因为客户一句“还是第三版更好”如果你没有保存历史上传记录就完全被动——这个问题在第4部分会详细展开。2. 技术选型解析Django为什么适合这类业务系统2.1 对比其他方案后的选择理由写这类业务系统其实绕不开一个比较用不用Django。我个人的选择逻辑很简单——优先看团队的交付效率和后续维护成本。Node.js的Express或Koa做这类系统也可以但要自己组装ORM、鉴权库、校验库、文件上传中间件项目还没写功能光搭架子就要一两天。Flask更轻自由度也更大但自由意味着每个开发者搭出来的工程结构千奇百怪后面的人接手就要花大量时间理解“这个项目为何这样组织”。对于广告策划网站这种业务逻辑相对固定的项目Django的“全家桶”模式反而是优势ORM管数据库Admin管后台内置Auth管用户forms管表单校验模板语言管页面渲染全部自带且方案成熟你只需要聚焦“广告业务规则”本身。Django的ORM也是我特别看重的。广告项目里查询需求池、按状态统计订单数这类操作非常多ORM提供的filter、annotate、select_related能大幅减少手动写SQL的精力。配合迁移机制模型改一改运行makemigrations就能自动生成SQL变更这在快速迭代开发期能节省大量时间。2.2 Python版本与环境准备实操Python版本选择上我推荐3.10或3.11Django用4.2 LTS长期支持版本。到我说的时间点Django 5.x也发布了但4.2毕竟是LTS安全性更新周期长对于公司业务系统来说更稳妥。Python的安装不细讲了但务必记住两点第一安装时勾选“Add Python to PATH”这是新手最容易漏掉的漏了会导致命令行里敲python提示找不到命令第二装完之后在命令行运行python --version确认版本能正常输出版本号再继续。环境隔离一定用venv。我不想用Anaconda或Pipenv理由非常简单venv是Python内置的模块不需要额外安装工具团队成员之间项目的依赖不会互相污染。创建命令python -m venv venv激活后pip install django pillow。这里额外提一下Linux环境。项目最后部署到云服务器时服务器上只有Python 3.6甚至更老的版本我当时的处理方法是先sudo apt update然后通过源码编译或者添加第三方源装新版本Python。这种环境问题在生产环境非常普遍建议在开发阶段就统一Python版本部署前先确认服务器系统版本和目标Python版本是否兼容不要等到上线前两天才踩这个坑。2.3 Django项目的工程初始化实操工程初始化现在看起来很简单但每一步都有讲究。我用项目名ads_platform来演示。# 创建虚拟环境并激活Windows/Linux通用 python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux/Mac # 安装Django和图像处理库Pillow pip install django pillow # 创建项目 django-admin startproject ads_platform cd ads_platform # 创建两个appaccounts负责用户orders负责订单业务 python manage.py startapp accounts python manage.py startapp orders创建完app后第一件事不是写代码而是把app注册进settings.py的INSTALLED_APPS。很多新手忘了这步写了模型、配了URL结果python manage.py migrate提示“No migrations to apply”半天找不到原因。随后把LANGUAGE_CODE改成zh-hansTIME_ZONE改成Asia/Shanghai否则后台时间显示的是UTC时间和北京时间差8小时客户反馈“提交时间不对”的时候你就傻眼了。项目根目录创建一个media/文件夹同时去settings.py里配置MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media并在根路由urls.py里加上开发环境下的媒体文件访问配置否则上传的设计稿图片在本地环境显示不出来from django.conf import settings from django.conf.urls.static import static urlpatterns [...] urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这个动作很多人会忘记在检查清单里。3. 数据库模型设计与核心建模思路3.1 用户扩展模型的正确姿势Django的django.contrib.auth.models.User是自带的用户表永远不要直接改它的结构正确做法是创建UserProfile一对一来扩展。我在accounts/models.py里的设计如下from django.db import models from django.contrib.auth.models import AbstractUser class UserType(models.IntegerChoices): CUSTOMER 1, 客户 DESIGNER 2, 设计师 ADMIN 3, 管理员 class User(AbstractUser): user_type models.IntegerField(choicesUserType.choices, defaultUserType.CUSTOMER) class Meta: db_table auth_user_custom class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) company_name models.CharField(max_length100, blankTrue, verbose_name公司/品牌名称) phone models.CharField(max_length20, blankTrue, verbose_name手机号) wechat models.CharField(max_length50, blankTrue, verbose_name微信号) avatar models.ImageField(upload_toavatars/, blankTrue, nullTrue, verbose_name头像) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.user.username} 的资料用AbstractUser代替默认User因为user_type是登录后必须判断的字段每次查询都去Profile表拿会多一次IO。直接放在User表上一个查询就能拿到身份和基础信息。这里有个细节related_nameprofile给反向查询一个清晰的名字后面在视图里用request.user.profile就能直接取到扩展资料。3.2 广告需求订单模型订单表是核心它承载了业务流转中绝大多数状态变化和操作记录。我按需求设计了AdProject模型class AdProject(models.Model): class Status(models.IntegerChoices): PENDING 1, 待审核 APPROVED 2, 待认领 CLAIMED 3, 制作中 SUBMITTED 4, 待验收 REVISION 5, 修改中 COMPLETED 6, 已完成 REJECTED 7, 已驳回 title models.CharField(max_length200, verbose_name项目名称) brand models.CharField(max_length100, verbose_name品牌名称) description models.TextField(verbose_name需求描述) budget_range models.CharField(max_length50, verbose_name预算区间) expected_deadline models.DateField(verbose_name期望交付日期) status models.IntegerField(choicesStatus.choices, defaultStatus.PENDING, db_indexTrue) customer models.ForeignKey(accounts.User, on_deletemodels.CASCADE, related_nameprojects, verbose_name客户) designer models.ForeignKey(accounts.User, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_nameclaimed_projects, verbose_name设计师) assigned_at models.DateTimeField(nullTrue, blankTrue, verbose_name认领时间) submitted_at models.DateTimeField(nullTrue, blankTrue, verbose_name提交时间) feedback models.TextField(blankTrue, verbose_name客户反馈) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) updated_at models.DateTimeField(auto_nowTrue, verbose_name更新时间) class Meta: ordering [-created_at] indexes [models.Index(fields[status, created_at])]状态枚举用整型而不是字符串原因是数据库存储空间更小、查询速度更快代码里通过Status.choices也能拿到可读标签Template里用get_status_display直接显示中文状态名。db_indexTrue加在status字段上因为“按状态筛选需求池”是最频繁的查询操作没有索引列表页会随着数据量增长越来越慢。designer字段用SET_NULL而不是CASCADE因为“设计师被删除后历史订单不能跟着消失”这是业务数据的底线。类似的customer字段用CASCADE是因为客户账号注销时他的需求也没意义了。这两个外键策略要结合业务来理解不能一刀切。3.3 方案版本与沟通记录模型前面提过方案版本管理的坑这里具体说模型怎么设计。设计师可能多次上传方案文件我不能只存“最新一个”而要用一对多保存每次上传记录class DesignWork(models.Model): project models.ForeignKey(AdProject, on_deletemodels.CASCADE, related_nameworks) designer models.ForeignKey(accounts.User, on_deletemodels.CASCADE, related_namedesign_works) file models.FileField(upload_toworks/%Y/%m/, verbose_name方案文件) cover models.ImageField(upload_toworks_covers/%Y/%m/, blankTrue, nullTrue, verbose_name方案封面) revision_note models.CharField(max_length255, blankTrue, verbose_name版本说明) version models.PositiveIntegerField(default1, verbose_name版本号) uploaded_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-version]版本号不是简单自增而是在同一个项目下计算“已有版本数1”。这个逻辑放在表单处理的视图函数里做用Count聚合查询from django.db.models import Count version project.works.aggregate(totalCount(id))[total] 1沟通记录模型则类似一个迷你论坛/留言板客户和设计师围绕订单互相发消息用于沉淀对稿信息。实际项目中这个模型还可以扩展支持“只读”的附件评论但核心就是user、content、project、created_at四个字段。把这两个模型放在一起看你会发现Django建模的核心思想就是把业务名词直接翻译成模型把业务关系翻译成外键别过度设计就好。4. 核心功能实现与实战代码精讲4.1 按照Django标准流程创建业务模块从零到一的实操过程是按“创建app - 注册app - 写模型 - 做迁移 - 写视图 - 配URL - 写模板 - 跑起来”的流程推进的。很多人一上来就写视图模型都没建后面数据库结构一变视图全部跟着重构这是最消耗时间的错误路径。我在这个项目里创建了两个appaccounts和orders。accounts负责注册、登录、用户资料orders负责需求单和方案的业务逻辑。两个app分工清晰后每个app的models.py都不会太臃肿而且各自的内聚性也更强。曾经有一个版本我把用户模型放在orders里结果orders的模型里有一堆和用户无关的字段迁移文件也互相纠缠后来花了一个下午拆开才理顺。教训就是app的划分要第一优先级想清楚。创建app之后立刻在settings.py里注册。这一步做完才开始写模型。模型写好后依次执行python manage.py makemigrations accounts orders python manage.py migrate我看到很多新手在改了模型之后忘了makemigrations或者迁了一半报错不知道怎么处理。遇到迁移冲突我的建议是不要乱删迁移文件先把db.sqlite3备份一份然后python manage.py makemigrations --merge试一下再不行再看具体错误信息多数情况是外键引用的问题找到冲突的模型改一下字段名就行。只要数据库里还没有生产数据最粗暴的方案就是删掉db.sqlite3和所有app下的migrations目录重新迁移但如果线上已经部署请老老实实做增量迁移。4.2 视图函数与表单校验的实战写法功能实现上视图我倾向于用“函数视图装饰器”而非ViewSet。虽然DRFDjango REST Framework的ViewSet很强大但这个项目没有前后端分离需求模板直接渲染就够了。函数视图的好处是逻辑直接一行一行看得很清楚调试也简单。以“设计师认领订单”的视图为例from django.shortcuts import get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST login_required require_POST def claim_project(request, project_id): if request.user.user_type ! User.UserType.DESIGNER: return redirect(dashboard) project get_object_or_404(AdProject, idproject_id) if project.status ! AdProject.Status.APPROVED: return redirect(project_list) project.designer request.user project.status AdProject.Status.CLAIMED project.assigned_at timezone.now() project.save() return redirect(designer_workspace)三个防线层层递进login_required保证已登录身份判断保证只有设计师状态判断保证订单处于“待认领”才能被认领。这个“认领”动作本质是一次条件更新任何一步不满足就直接拒绝。表单侧Django的ModelForm在CURD场景非常好用。但要注意广告需求表单包含expected_deadline日期字段用户可能会提交任意格式的日期一定要在Form里指定widgetforms.DateInput(attrs{type: date})让浏览器弹日期选择器后端再解析能挡掉一大半格式错误。关于表单校验我还踩过一个经验DateField默认要求“YYYY-MM-DD”格式但中文用户习惯输入“2024年5月1日”这会在is_valid()阶段直接报错。我当时的解决方法是写一个自定义validators先正则匹配中文日期格式转成iso格式再交给字段解析这个细节很实用。4.3 文件上传与版本管理的完整实现设计稿文件上传是整个项目里最容易“出幺蛾子”的环节。我用的方案是Django原生FileField配合Pillow处理上传的图片。Pillow一定要装因为ImageField底层依赖它不装的话上传图片时会报“无法识别图像文件”的错误。处理“设计师上传新版本”的视图代码如下login_required def upload_work(request, project_id): project get_object_or_404(AdProject, idproject_id) if project.designer ! request.user or project.status not in [AdProject.Status.CLAIMED, AdProject.Status.REVISION]: return redirect(designer_workspace) if request.method POST: form DesignWorkForm(request.POST, request.FILES) if form.is_valid(): work form.save(commitFalse) work.project project work.designer request.user work.version project.works.aggregate(totalCount(id))[total] 1 work.save() project.status AdProject.Status.SUBMITTED project.submitted_at timezone.now() project.save() return redirect(project_detail, project_idproject.id) else: form DesignWorkForm() context {form: form, project: project} return render(request, orders/upload_work.html, context)这段代码里有几个细节值得你注意。form.save(commitFalse)先不写库拿到模型实例再补充外键字段最后再save()这是Django表单处理外键关联的标准姿势。版本号用聚合1计算能保证同一个项目内版本号不重复。文件上传成功后立即更新项目状态和提交时间这一步很容易漏——很多新手上传了文件但订单状态没变客户还以为没交付整个流程就断了。文件存储路径方面我设置了upload_toworks/%Y/%m/文件会按月归档后续清理旧文件或者做备份都很方便。这里提醒一下生产环境一定要把MEDIA_ROOT配置到非系统盘目录并且要定期备份。我把这个目录配置在Nginx的alias里配合Django的security中间件设置X-Content-Type-Options: nosniff防止上传文件被当成HTML脚本执行。4.4 基于Cookie的Token保持登录方案项目最初的MVP阶段没有做JWT因为服务端渲染页面加上Session已经完全够用。但设计师这边提出一个需求希望公司内部网络环境里设计师登录后长时间不用重新登录同时管理员可以对会话进行统一控制。于是我设计了“Cookie存Token”的方案。具体做法是登录成功后生成一个随机Token字符串存到HttpResponse.set_cookie里同时把Token关联到用户Session。Token的有效期设为7天Cookie设置为HttpOnly防止脚本读取。核心逻辑如下import secrets def login_view(request): if request.method POST: form LoginForm(request.POST) if form.is_valid(): username form.cleaned_data[username] password form.cleaned_data[password] user authenticate(request, usernameusername, passwordpassword) if user: login(request, user) token secrets.token_urlsafe(32) request.session[auth_token] token response redirect(dashboard) response.set_cookie( auth_token, token, max_age7 * 24 * 3600, httponlyTrue, samesiteLax, secureFalse, # 开发环境 ) return response else: form.add_error(None, 用户名或密码错误) else: form LoginForm() return render(request, accounts/login.html, {form: form})登出时除了调用logout(request)还要记得删除Cookielogin_required def logout_view(request): logout(request) response redirect(login) response.delete_cookie(auth_token) return response这套方案的取舍我解释一下不用Django内置Session Cookie因为默认Session是保存在数据库中的每次请求都要查表如果用Cache-Session则只有服务器能解码。我加上自研Token是为了在Redis里维护“在线设计师名单”管理员踢人时直接删Redis中的Token效果是立即让该设计师的会话失效。这个可踢人的能力默认方案做不到。你的项目如果不需要管理员干预会话直接用Django原生Session就足够了。4.5 数据查询优化与DeleteObject的细节项目上线后随着订单量增长我发现列表页越来越慢。排查之后确认是典型的N1问题遍历订单列表时每一条订单的customer和designer外键都要触发一次数据库查询。解决办法很简单在查询时用select_relatedproject_list AdProject.objects.select_related(customer, designer).filter(statusAdProject.Status.APPROVED)select_related会把外键通过JOIN一次性取出来N1变11列表页速度肉眼可见地提升。这个优化技巧在Django项目里非常高频凡是列表页展示外键关联字段都应该考虑它。另外Django执行查询-删除对象也是很多人口中的坑点。比如批量删除订单时如果订单关联了DesignWork直接delete()可能会因为外键默认PROTECT而报错。我的习惯是删除前先查关联数据再决定是连坐删除还是转移归属。下面是一段订单归档流程里的删除逻辑示例# 删除一个订单前检查是否有方案文件 def delete_project_safely(project_id): project get_object_or_404(AdProject, idproject_id) work_count project.works.count() if work_count 0: project.works.all().delete() project.delete()这里我选择先删子表再删主表避免外键约束报错。遇到ProtectedError时先select_related或者prefetch_related去找哪些外键引用了它再定向处理不要直接强删否则会留下孤儿数据。5. 前端集成、调试与部署上线实战5.1 页面渲染与Bootstrap整合Django模板系统做后端渲染非常直接流畅。我在基础模板base.html里引入了Bootstrap 5的CDN链接然后所有页面{% extends base.html %}复用导航栏和页脚。广告策划网站对视觉要求比一般后台系统高但我不建议在初期就自定义一套复杂CSS一是拖时间二是容易把业务逻辑淹没在样式调整里。先用Bootstrap把信息架构搭清楚后期再请专门的前端同学来优化视觉。模板里最常用的操作就是循环订单列表加条件判断{% for project in project_list %} div classcard mb-3 div classcard-body h5 classcard-title{{ project.title }}/h5 p classcard-text{{ project.brand }} · 预算:{{ project.budget_range }} · 截止:{{ project.expected_deadline|date:Y-m-d }}/p span classbadge bg-info{{ project.get_status_display }}/span a href{% url project_detail project.id %} classbtn btn-sm btn-primary查看详情/a /div /div {% empty %} p暂无符合条件的项目/p {% endfor %}Django模板的empty标签非常好用列表为空的时候可以优雅地显示提示信息不用在视图里单独做判断。5.2 调试技巧与日志定位开发阶段我重度依赖print()和Django调试工具栏。django-debug-toolbar是神器在settings.py里注册后在右侧边栏显示SQL执行次数、耗时时长、模板渲染上下文。定位慢查询时直接看“SQL Queries”栏。但是上线后print()就看不见了必须配置LOGGING。我按下面的配置加了文件日志卡在哪个视图、哪个SQL一查就出来LOGGING { version: 1, disable_existing_loggers: False, handlers: { file: { level: DEBUG, class: logging.FileHandler, filename: BASE_DIR / logs / debug.log, }, }, loggers: { django: { handlers: [file], level: INFO, }, orders: { handlers: [file], level: DEBUG, propagate: True, }, }, }另一个调试经验密码错误、表单校验不通过这类问题不要直接看数据库先在前端页面用{{ form.errors }}打印表单错误八成是格式问题。如果某个POST请求返回500去日志里看Traceback的具体行号比盲猜字段名高效得多。5.3 生产部署与静态文件管理部署上线我用的组合是“Gunicorn Nginx SQLite迁移至PostgreSQL”。开发时用SQLite方便但生产环境千万别用并发高一点就会出现“database is locked”。迁移数据过程我写了一遍脚本先把开发环境的SQLite数据导出为JSON然后在PostgreSQL上执行python manage.py migrate再用loaddata导入。注意dumpdata导出的JSON是带app标签的导入前确认数据库已建好表否则会报“无法加载内容”。配置Nginx转发时静态文件是最容易翻车的环节。开发环境Django会自动通过runserver处理静态文件生产环境不会。我的处理是开发时执行python manage.py collectstatic把所有app的静态文件收集到项目根目录的staticfiles/文件夹里然后在Nginx配置中加入location /static/ { alias /path/to/ads_platform/staticfiles/; } location /media/ { alias /path/to/ads_platform/media/; }这个写错最常见的症状就是网页样式丢失、上传图片不显示。调试时先确认MEDIA_URL对应的路径在服务器上真实存在、权限可读。我一次线上事故就是因为media目录权限是700Nginx进程无法读取最后chmod 755 media解决。6. 常见问题与排查技巧实录做一个速查表给读者这里面的条目都是我实际踩过或者帮别人排查过的症状常见原因解决方案迁移时报“No migrations to apply”app未注册到INSTALLED_APPS检查settings.py的app列表运行makemigrations时指定app名称上传图片后无法显示未配置MEDIA_URL和MEDIA_ROOT或Nginx未映射media开发环境在urls.py加static()生产环境检查Nginx的alias路径和目录权限登录后访问某些页面仍重定向到登录页装饰器用错或session失效检查视图函数装饰器顺序确认login_required大写正确Django 4.x时间保存差8小时TIME_ZONE没有设置settings.py设置Asia/Shanghai并USE_TZ True或False视具体方案定数据库字段修改后apply失败旧数据违反新约束先备份再写数据迁移脚本清洗数据列表页加载缓慢未使用select_related导致N1检查循环内是否频繁查询外键字段Cookie Token登录偶尔失效SameSite和Secure属性配置冲突开发环境secureFalse生产环境secureTrue且确保HTTPS删除用户时报ProtectedError有外键引用且on_delete不是CASCADE分析外键关系改用on_deleteSET_NULL或先删除关联子记录这些问题的共同核心是先确认状态再查配置最后看代码。状态写在数据库里不会撒谎配置决定能不能跑通代码才决定跑得快不快。排查时按这个顺序来基本能过滤掉八成问题。另一个独家的避坑技巧是不要把业务判断塞进模板。我见过一个项目里模板中写{% if request.user.user_type 1 %}数字1是什么意思只有写代码的人知道。我在这个项目里强制要求模板只用{% if user.user_type user.UserType.ADMIN %}这种可读写法或者在视图里提前把布尔值塞进context。这样即使过了半年再回来改代码面对模板也不会一脸茫然。7. 上线后的迭代思考与扩展方向项目交付以后我又遇到两个反馈比较集中的优化点。第一个是客户想看到方案的实时预览而不是下载文件。这对存储和网络带宽要求高但体验提升明显。第二个是设计师希望有自动化的“需求匹配提醒”即平台新增了跟设计师擅长领域匹配的订单时主动通知到人。这两点都指向同一个方向功能上线只是开始后续围绕角色的真实效率提升才是重点。技术层面新功能顺利的话我会引入消息队列比如Redis Django Channels做站内通知和实时状态刷新前端逐步引入一个轻量的Vue框架做交互组件后端继续用Django提供接口。这个演进路径在Django社区很成熟不会推倒重来只是在现有项目上做增量。从我的角度看这个广告策划网站项目最有价值的不是某个炫酷的技术点而是“如何把一个业务从想法到流程再到系统落地”的完整方法论。Django是承重墙Python是水泥和钢筋真正决定房子好不好住的是蓝图也就是你在画模型之前对业务理解的深浅。建议你把大量时间花在业务梳理和流程确认上模型和代码反而是水到渠成的事情。最后分享一个过来人的体会做业务系统永远给自己留一条“手工干预”的后路。系统再完善客户都会提出超出流程的意外情况这时候管理员在后台能修改状态、能指定设计师、能补录附件比任何技术方案都管用。这个看起来不“高级”的功能恰恰是平台能顺利运营的兜底。踩过这个坑之后我强烈建议你在做权限设计时给管理员预留一个“维特”的入口不要把所有逻辑都锁死在自动流程里。