这话题我太有发言权了。这几年我在网上约稿、也帮朋友做过画师作品集网站,最大的感受就是:约稿需求明明很旺盛,交易方式却还停留在"群里吼一声、私聊发图、转账打款"的原始阶段。买家找不到可靠的画师,画师也疲于应付各种白嫖咨询。所以我自己动手写了一个基于 Python + Vue 的动漫画家约稿平台,后端用 Django,前端用 Vue,整个开发调试都在 PyCharm 里完成。这篇文章不是堆概念的教程,而是完整记录我从需求拆解、技术选型、数据库设计、接口开发、前端联调、踩坑修复到上线部署的全过程,适合有 Python/Django 基础、想上手 Vue 全栈项目,或者正在准备毕设和个人作品的同学参考。
我先把结论放在前面:这个平台的核心不是"展示画作",而是把"约稿、报价、改稿、交付、验收"这条多轮次交易链路做成线上闭环。你要是只做一个画作画廊,那根本不需要订单状态机;但一旦涉及钱、涉及交稿时间、涉及多轮修改,你就得认真设计业务模型。下面我把每个环节的关键思考都展开说。
1. 约稿平台到底在解决什么问题:先画清角色和流程
1.1 约稿的真实流程和电商订单有什么区别
很多人第一次做这类平台,会下意识地往"电商"上靠:画师是店铺,作品是商品,买家下单付款,完事。但真正的约稿不是这样的。一次完整约稿大概是这个路径:买家提出需求(比如"我要一张 1920x1080 的横版插画,日系厚涂风格,商业用途,三周内交稿,预算 1500 左右")——这不是一个已经存在的商品,而是一个待匹配的订单。画师看到需求后报价,买家在多个报价里挑人;选中后画师开始创作,中间可能要反复沟通草稿、线稿、配色;最后交付源文件,买家验收,才确认完成。
这就带来了两个电商没有的特殊点:第一,订单在成交前就有"双向选择"过程,不能简单设个购物车;第二,交付物不是标准品,可能存在多轮版本迭代,平台必须把每次交付、每次反馈都记录下来,不然出了问题谁都说清。我在设计时把这两点当作核心约束:需求大厅是起点,状态机是主线,交付记录是证据链。
1.2 角色分工与权限边界
这个系统有三类角色,我分别给他们划了明确边界:
- 普通用户(约稿人):可以浏览需求大厅、发起约稿、查看订单详情、选择报价、验收作品、写评价。
- 画师:想要接单,不能只注册用户,还必须提交入驻申请,平台审核通过后成为签约画师,才能报价、接单、上传交付物。
- 平台管理员:负责画师入驻审核、处理争议仲裁、封禁违规账号。
有人会问,为什么画师入驻要单独审核,不开放自由注册?我做这个决定是因为在真实约稿场景里,"作品质量参差"是最大的信任障碍。如果任何用户都能接单报价,买家根本不敢下单。审核机制可以极大降低初期刷单、白嫖体验的风险。哪怕是 MVP 版本,我也建议保留这个人工审核环节,后台直接复用 Django Admin,不额外写管理页面。
1.3 一期功能清单:先做透主线再考虑扩展
我当时给自己列了一个功能优先级,避免贪多:
- P0 必做:登录注册、画师入驻审核、需求发布与管理、需求大厅筛选、画师报价、买家选单、订单状态流转、作品交付上传、订单验收、站内留言。
- P1 二期:在线实时聊天、支付托管、争议举证、画师作品集展示、标签系统。
- P2 远期:智能推荐、热门画师榜单、移动端适配。
实时聊天我最终放弃在首期实现,站内留言板已经够用。支付托管也没有在一期接入,因为涉及第三方支付接口资质和回调处理,会让项目复杂度翻倍。我建议你做个人项目或毕设时也这样划分:先把"需求发布-报价-创作-交付-验收"这条主链路走通,再谈支付和IM。
2. Django与Flask的取舍,以及我最终定下的技术组合
2.1 为什么这个项目选Django而不是Flask
标题里同时出现 Django 和 Flask,我猜很多人也在纠结。我的答案是:如果项目里有"管理后台"和"复杂状态流转",Django 赢在起跑线;如果只是写一个几百行就能跑通的简单 API,Flask 更轻快。约稿平台属于典型的前一种。
我对比过两条路线:
| 对比维度 | Django + DRF | Flask + Flask-SQLAlchemy |
|---|---|---|
| 管理后台 | 自带 Django Admin,画师审核、订单仲裁几乎零成本 | 需要自己写页面或用第三方库拼 |
| 用户认证 | Auth 系统 + SimpleJWT,开箱即用 | Flask-JWT-Extended 得手动配 |
| ORM 与迁移 | Django ORM + migrations 自动化程度高 | SQLAlchemy 灵活但迁移配置繁琐 |
| 技术约束 | 钻戒很强,写久了容易"被框架教育" | 自由度高,但组件选型都要自己拍板 |
| 适合场景 | 中后台功能多、权限复杂、模块化项目 | 快速原型、轻量 API、微服务 |
我最终选了 Django 还有一个务实理由:这个项目自带大量审核、筛选、后台管理需求,Django 的 Model、Form、Admin 三层配合能省下大量重复代码。实际开发下来,画师入驻审核页面我几乎没有额外写,直接在 Admin 里把状态一改就好。
2.2 前后端技术栈全景
我用的具体版本和工具是这样:
- 后端:Python 3.10,Django 4.2,Django REST Framework,djangorestframework-simplejwt,django-cors-headers,Pillow 处理图片。
- 数据库:MySQL 8.0。如果你本地图省事也可以用 SQLite 起步,但上线前我建议换成 MySQL,因为约稿平台的并发读写和文件元数据管理比想象中多。
- 前端:Vue 3 + Vite + Pinia + Vue Router + Axios + Element Plus。Element Plus 让我省了至少三天的样式工作量。
- 开发 IDE:PyCharm Professional。它自带数据库工具和前端支持,我在一个窗口里就能改后端模型、看数据库表结构、调前端断点。
有人会用"PyCharm 太重"来质疑,但我对这个项目是重度使用,图它的代码跳转、重构能力和内置 HTTP Client。建议你别用社区版硬扛,专业版对 Django 和 JavaScript 的支持完全是两个体验。
2.3 PyCharm里的初始化步骤与工程结构
我在 PyCharm 里初始化项目时按下面顺序操作,避免环境混乱:
- 用 PyCharm 新建项目,选择虚拟环境 venv,Python 版本选 3.10 以上。
- 在 Terminal 安装后端依赖:
pip install django djangorestframework djangorestframework-simplejwt django-cors-headers pillow mysqlclient。 - 执行
django-admin startproject config .,注意后面的点号表示在当前目录创建配置包。 - 创建模块应用:
python manage.py startapp users、startapp orders、startapp works。 - 在 PyCharm 的 Run/Dashboard 里新建 Django Server 配置,让日志和断点都在 IDE 里显示。
- 前端我单独在项目根目录下建
frontend子目录,用npm create vite@latest初始化 Vue 项目,然后用 PyCharm 打开该子目录的开发环境。
工程目录大致是:
commission_platform/ ├── config/ # Django 配置 ├── users/ # 用户与画师档案 ├── works/ # 作品集与交付物 ├── orders/ # 订单、报价、状态流转 ├── common/ # 通用工具、权限类 ├── frontend/ # Vue3 前端这样的好处是前后端代码在同一个仓库里,部署时可以分别构建,但版本管理不分裂。
3. 约稿业务的数据建模:四张核心表怎么从需求翻译出来
3.1 用户体系与画师档案
Django 自带的 User 表直接用作登录账号,但画师信息不能硬塞进去。我用 OneToOne 扩展出一个画师档案表。关键字段如下:
class PainterProfile(models.Model): class Status(models.TextChoices): PENDING = 'pending', '待审核' APPROVED = 'approved', '已入驻' BANNED = 'banned', '已封禁' user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='painter_profile') display_name = models.CharField(max_length=50, blank=True) avatar = models.ImageField(upload_to='avatars/', blank=True) bio = models.TextField(blank=True) style_tags = models.CharField(max_length=200, blank=True, help_text='如:厚涂、日系、Q版,逗号分隔') status = models.CharField(max_length=10, choices=Status.choices, default=Status.PENDING) created_at = models.DateTimeField(auto_now_add=True)把 status 放在画师档案里,而不是在 User 上加 is_painter 布尔字段,是为了可以表达"申请中""已封禁"这类中间态。我在做权限判断时,会要求 status == APPROVED 才允许接单,这个细节在初期就容易踩坑。
3.2 订单主表与状态机设计
订单表是整个系统的灵魂。我给它设计了一个明确的状态机,用 TextChoices 常量来定义:
class CommissionOrder(models.Model): class Status(models.TextChoices): PENDING = 'pending', '待接单' IN_PROGRESS = 'in_progress', '创作中' SUBMITTED = 'submitted', '已交付' CONFIRMED = 'confirmed', '已验收' CANCELED = 'canceled', '已取消' title = models.CharField(max_length=100) description = models.TextField() style_tag = models.CharField(max_length=50) budget_min = models.DecimalField(max_digits=10, decimal_places=2) budget_max = models.DecimalField(max_digits=10, decimal_places=2) deadline = models.DateField() buyer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posted_orders') painter = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='accepted_orders') status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) finished_at = models.DateTimeField(null=True, blank=True)这里有个设计重点:painter 字段一开始为空,订单是"悬赏式需求",谁被选中后才会被赋值。状态流转我严格控制为一条单向链路:待接单 -> 创作中 -> 已交付 -> 已验收,期间只允许取消。不允许"已验收"之后跳回"创作中",也不允许跳过交付直接验收。这样虽然在极端情况不够灵活,但对首期平台来说,规则简单不会扯皮。
3.3 沟通、报价与交付记录的细节
订单不能只有一张表就完事,报价和交付记录必须拆出来。
报价表(Quote)记录每个画师对某个订单的竞争:
class Quote(models.Model): order = models.ForeignKey(CommissionOrder, on_delete=models.CASCADE, related_name='quotes') painter = models.ForeignKey(User, on_delete=models.CASCADE) price = models.DecimalField(max_digits=10, decimal_places=2) description = models.TextField(blank=True) is_selected = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True)交付记录表(DeliveryRecord)单独存在的理由,是约稿经常要交多轮改稿。如果我把交付文件直接挂在订单表一个字段上,那草稿、修改稿、终稿就没法区分了。我设计成:
class DeliveryRecord(models.Model): order = models.ForeignKey(CommissionOrder, on_delete=models.CASCADE, related_name='deliveries') version_no = models.IntegerField(default=1) comment = models.CharField(max_length=200, blank=True) file = models.FileField(upload_to='deliveries/%Y/%m/') preview = models.ImageField(upload_to='previews/', blank=True) created_at = models.DateTimeField(auto_now_add=True)站点内留言我用了一张极简的 Message 表:外键指向订单、发送者、内容、时间。首期没有做已读未读和实时推送,只按时间正序展示,够用。
3.4 核心表关系一览
我把核心关系总结成一张表,方便你对照建表:
| 表名 | 关键外键 | 说明 |
|---|---|---|
| auth_user | 无 | Django 自带用户表 |
| users_painterprofile | user -> auth_user | 画师档案与审核状态 |
| works_artwork | painter -> auth_user | 画师作品集,用于入驻审核 |
| orders_commissionorder | buyer, painter -> auth_user | 约稿订单主表 |
| orders_quote | order, painter | 画师对订单的报价 |
| orders_deliveryrecord | order | 订单交付记录,多轮版本 |
| orders_message | order, sender | 订单留言沟通 |
我在开发时用 PyCharm 的 Database 面板可视化看过 ER 图,整体比较清晰。建表时给 order 的状态字段、style_tag、created_at 加了联合索引,需求大厅按标签和时间排序时性能很好。
4. 后端接口开发:认证、需求发布、报价与验收的代码级实现
4.1 基于JWT的登录认证与权限控制
前后端分离后,Session 那套在跨域场景下比较麻烦,我用 SimpleJWT 签发访问令牌。核心配置是在 Django settings 里引入 JWT 认证类:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), }约稿平台有角色权限问题,普通用户不能替画师接单,我自定义了一个权限类:
from rest_framework.permissions import BasePermission class IsApprovedPainter(BasePermission): message = '需要通过画师入驻审核后才能操作' def has_permission(self, request, view): user = request.user if not user or not user.is_authenticated: return False profile = getattr(user, 'painter_profile', None) return profile is not None and profile.status == 'approved'然后在报价、接单、提交交付物这些视图上直接写permission_classes = [IsApprovedPainter]。这样登录和权限逻辑就分开了。
4.2 需求大厅与订单创建接口
创建需求接口的核心是参数校验。我没有用复杂的序列化器嵌套,直接在 create 方法里显式校验:
class OrderCreateSerializer(serializers.ModelSerializer): class Meta: model = CommissionOrder fields = ['title', 'description', 'style_tag', 'budget_min', 'budget_max', 'deadline'] def validate(self, attrs): if attrs['budget_min'] > attrs['budget_max']: raise serializers.ValidationError('最低预算不能大于最高预算') if attrs['deadline'] < timezone.now().date(): raise serializers.ValidationError('截止日期不能早于今天') return attrs需求大厅列表接口要支持多条件组合筛选,比如按风格标签、预算上限、是否有人报价。我没在 URL 里堆复杂参数,而是接受?tag=厚涂&budget_max=800这样的查询参数,用 queryset 链式筛选。这里注意一点:所有筛选字段都做成可选,不要因为某个参数缺失就报 500。我在前端也会做对应的空状态提示。
4.3 订单状态流转的事务与并发控制
这是全项目最容易写错的地方。如果直接order.status = 'in_progress'然后 save,两个画师同时报价、买家同时确认时,状态就会互相覆盖。很多人测试环境单用户没发现问题,一上线并发就翻车。
我处理报价选中时,用事务和行锁保证同一时间只有一个画师能被选中:
from django.db import transaction from rest_framework.exceptions import ValidationError @transaction.atomic def accept_quote(order_id, quote_id, user): order = CommissionOrder.objects.select_for_update().get(pk=order_id) if order.status != CommissionOrder.Status.PENDING: raise ValidationError('当前订单不在可接单状态') if order.buyer_id != user.id: raise ValidationError('只有需求发布者可以确认报价') Quote.objects.filter(order_id=order_id, is_selected=True).update(is_selected=False) Quote.objects.filter(pk=quote_id, order_id=order_id).update(is_selected=True) order.painter_id = Quote.objects.get(pk=quote_id).painter_id order.status = CommissionOrder.Status.IN_PROGRESS order.save() return orderselect_for_update()会锁住这一行直到事务结束,第二个并发的请求只能等前面的提交完成。类似地,画师提交交付物时我会校验order.status == IN_PROGRESS,买家验收时校验order.status == SUBMITTED。把状态流转全部收进后端事务,前端只是展示按钮的显隐,不能作为业务判断依据。
4.4 文件上传与作品预览的处理
上传交付文件和作品集图片,我用 Django 的 FileField + 本地文件存储。这里有一个非常容易踩的性能坑:原始设计稿可能是几个 GB 的 PSD,但页面只展示预览图时不能直接输出原图。我在交付时用 Pillow 生成一份 JPEG 缩略图存在 preview 字段,列表页只读缩略图,详情页给下载链接。
MEDIA 配置我写在 settings 里:
MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'本地开发时 Django 可以临时服务媒体文件,但生产环境一定要交给 nginx 直接托管,不能经 Django 转发大文件。我把文件校验也放进了序列化器:单个文件不能超过 200MB,图片格式白名单为 jpg、png、webp。PSD 这类源文件我允许上传,但不在线打开,才积内存抗住了 5MB 以上文件的考验。
5. Vue3前端的搭建与接口联调实践
5.1 页面结构与路由设计
前端我按业务页拆了 8 个主要路由,页面数量不多,但每个页面都有明确的职责:
| 路径 | 组件 | 功能 |
|---|---|---|
| / | OrderList.vue | 需求大厅,按标签、预算筛选 |
| /orders/:id | OrderDetail.vue | 需求详情,报价入口 |
| /login | Login.vue | 登录注册 |
| /painter/apply | PainterApply.vue | 画师入驻申请 |
| /painter/works | WorkManage.vue | 画师作品集管理 |
| /me/publish | PublishOrder.vue | 发布约稿需求 |
| /me/orders | MyOrders.vue | 我的订单(买家视角/画师视角切换) |
| /me/quotes | MyQuotes.vue | 我发出的报价记录 |
路由全部走 Vue Router 的 createWebHashHistory 还是 createWebHistory?我的建议是首期用 hash 模式,省去服务器配置。后续部署如果想美化 URL 再改 history,同时配 nginx 的try_files回退。这里我在第六节会专门讲一次踩坑。
5.2 状态管理、Axios二度封装和接口分层
Pinia 我只存两类全局信息:用户 token 和当前登录用户的基本资料。不要试图把所有服务器数据都塞进 store,那会让状态变得不可控。我的封装思路是:
useUserStore负责 login/logout/fetchUserInfo;api/order.js、api/quote.js、api/upload.js按模块导出请求函数;- Axios 实例统一加 baseURL 和拦截器。
拦截器代码是联调的关键,直接贴出来:
import axios from 'axios' import router from '../router' import { useUserStore } from '../stores/user' const service = axios.create({ baseURL: '/api', timeout: 20000, }) service.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers.Authorization = 'Bearer ' + userStore.token } return config }) service.interceptors.response.use( res => res.data, err => { if (err.response && err.response.status === 401) { const userStore = useUserStore() userStore.logout() router.push('/login') } return Promise.reject(err) } )统一处理 401 的价值是:token 过期后用户不用手动清缓存,全局自动被踢回登录页。
5.3 核心复用组件的拆分逻辑
我拆了几个高频复用组件:
OrderCard.vue需求卡片,列表页和我的订单页复用;TagSelect.vue风格标签多选,发布页和筛选栏复用;FileUploader.vue图片/文件上传,画师作品集和交付物上传共用;StatusTag.vue根据订单状态渲染不同颜色的标签。
一开始我把上传逻辑写死在 WorkManage 页面里,后来发现交付物上传也要用,就抽成了 FileUploader。组件化之后,一处改了上传进度条、格式校验,所有页面同步生效,维护成本直线下降。Element Plus 的 el-upload 组件本身支持自定义 http-request,我把接口方法传进去就能完全控制上传行为。
6. 联调与部署阶段踩过的四个坑(排查链路记录)
6.1 跨域配置引发的带凭证请求失败
现象:前端登录接口能通,但一请求需要携带 Cookie 的接口就报 CORS 错误,浏览器控制台出现The value of the 'Access-Control-Allow-Origin' header ... not equal to the supplied origin。
我排查的过程是:先看 Django 的日志,请求根本没到视图层。再用 Postman 测同一个接口,正常返回 200,说明后端接口没问题。问题出在前端 Axios 设置了withCredentials: true,而我在 django-cors-headers 里配置的CORS_ALLOW_CREDENTIALS = True后,白名单里的域名必须精确匹配,不能写http://localhost:5173/带斜杠,也不能用泛域名。最后我把 CORS_ALLOWED_ORIGINS 写成http://localhost:5173就通了。
如果你也遇到类似问题,建议按这个顺序查:确认前端请求头确实带了 Authorization;确认后端 CORS 白名单没有尾部斜杠;确认不是浏览器插件拦截。另外,约稿平台既然用 JWT,其实不需要 withCredentials,把跨域简化成纯 token 传参会省很多事。
6.2 两个画师同时报价,订单状态被覆盖
现象:我用两个浏览器账号模拟并发报价,买家只确认了第一个画师,但第二个画师的报价后来却显示被选中,订单状态也乱掉了。
排查链路:我先在后台打印订单状态变更日志,发现两条请求几乎同时执行了accept_quote,都通过了if order.status != PENDING的检查,因为读的时候状态都还是 PENDING。这就是经典的"读-改-写"竞态。我加上select_for_update()后,两个请求会被串行化,第二个请求在等待锁释放后重新读取订单,发现状态已经变成 IN_PROGRESS,于是被校验拦下,返回"当前订单不可接单"。
这个坑非常典型。我强烈建议任何涉及"抢单""确认""领取"语义的接口,都必须用事务+行锁。不要用乐观锁版本号硬凑,在这个场景里行锁更直观,也不会让前端处理版本冲突重试。
6.3 上传超过8MB的图片,服务端没有响应
现象:本地小图上传正常,但画师传一张 8MB 左右的参考图,请求转圈很久后直接超时,Django 日志里出现磁盘或内存压力。
我的排查过程分三层:第一层看 nginx 配置,client_max_body_size默认只有 1m,超过直接返回 413,但本地开发没走 nginx,所以不是它;第二层看 Django 的 DATA_UPLOAD_MAX_MEMORY_SIZE,默认 2.5MB,超过后会抛出 RequestDataTooBig;第三层是我把FILE_UPLOAD_MAX_MEMORY_SIZE调大后,又发现 Pillow 对超大原图生成缩略图时内存飙升。
最终修复组合是:前端在上传前先压缩一次预览图,后端设置DATA_UPLOAD_MAX_MEMORY_SIZE = 20 * 1024 * 1024,nginx 的client_max_body_size 50m。对原始 PSD 文件不生成缩略图,只记录文件信息。个人项目不要追求服务端无限接收大文件,控制在那一个合理范围内最重要。
6.4 部署后刷新页面404
现象:部署完成后,访问根路径能看到首页,但在路由/orders/12下按 F5 刷新,页面变成 404。
原因很简单:前端用的是 history 路由,浏览器直接请求/orders/12时,nginx 去找这个路径对应的文件或目录,找不到就返回 404。必须在 nginx 配置里加一个回退规则,把所有非静态文件请求都转发到前端的 index.html:
location / { root /var/www/frontend/dist; try_files $uri $uri/ /index.html; }加上之后刷新就正常了。如果你刚开始接触部署,建议要么用 hash 路由,要么记得配置这条回退。这个问题在纯静态文件服务器上特别容易踩,我第一次部署时也懵了半天。
7. 上线前检查清单与运维护栏
7.1 gunicorn、nginx与前端部署配置
后端我用 gunicorn 跑 Django,配置文件里指定 worker 数量和 socket:
gunicorn config.wsgi:application \ --bind 127.0.0.1:8001 \ --workers 3 \ --timeout 120 \ --access-logfile -nginx 我做两层:第一层托管前端构建产物,第二层把/api和/media反向代理到 gunicorn:
location /api { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; client_max_body_size 50m; } location /media { alias /opt/comission_platform/media; }前端打包不用多说,npm run build后在 dist 目录下,直接把整个目录拷到服务器或 CI 产物里。个人项目如果不想上 Docker,这套方案最直接。
7.2 数据备份与迁移策略
上线后真正的资产不是代码,是用户和订单数据。我写了一个每日 cron 备份脚本,包含两部分:MySQL 逻辑备份和媒体目录增量同步:
mysqldump -u backup -p'xxx' commission_platform > /backup/mysql/$(date +\%F).sql rsync -av --delete /opt/comission_platform/media/ /backup/media/MySQL 的备份文件保留最近 30 天,媒体文件用 rsync 做异地同步。不要等订单量大了才想起备份,最便宜的后悔药就是备份脚本。我还把 SECRET_KEY 和数据库密码全部放到环境变量里,settings 里只读取os.environ.get(),保证代码仓库里不出现真实密钥。
7.3 安全配置要点
上线前我过了一遍安全配置,列个清单供你自查:
DEBUG = False,这个必须改,否则会输出完整堆栈给用户,泄露路径和代码结构;ALLOWED_HOSTS设置成自己的域名/IP,不写通配符;- 接口层统一用 DRF 的权限类,不能只在前端隐藏入口;
- 文件上传类型白名单,不能只依赖前端校验;
- 敏感操作比如确认报价、删除作品,写日志记录操作者、时间、内容。
我在实际运维中还加了一个简单限流:对登录接口和报价接口限制频率,防止脚本刷单。DRF 自带 Throttle,配置两个类就能覆盖大部分场景。这个约稿平台做下来,我最深的体会是:业务状态设计和并发控制远比页面好看重要。如果你是做毕设或练手,哪怕系统小,也一定把订单状态机、权限分层、异常处理写清楚,这些才是真正能写进简历和讲出深度的部分。最后再说一点个人建议:上线前找几个朋友实际约稿几轮,把买家、画师两种身份都体验一遍,很多状态流转漏洞都是这么暴露出来的,比你反复看代码高效得多。