资讯动态

Python与Vue全栈实战:从零构建动漫画家约稿平台

发布时间:2026/10/15 7:29:20 来源:尧图企业网站定制
这话题我太有发言权了。这几年我在网上约稿、也帮朋友做过画师作品集网站最大的感受就是约稿需求明明很旺盛交易方式却还停留在群里吼一声、私聊发图、转账打款的原始阶段。买家找不到可靠的画师画师也疲于应付各种白嫖咨询。所以我自己动手写了一个基于 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 赢在起跑线如果只是写一个几百行就能跑通的简单 APIFlask 更轻快。约稿平台属于典型的前一种。我对比过两条路线对比维度Django DRFFlask 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.10Django 4.2Django REST Frameworkdjangorestframework-simplejwtdjango-cors-headersPillow 处理图片。数据库MySQL 8.0。如果你本地图省事也可以用 SQLite 起步但上线前我建议换成 MySQL因为约稿平台的并发读写和文件元数据管理比想象中多。前端Vue 3 Vite Pinia Vue Router Axios Element Plus。Element Plus 让我省了至少三天的样式工作量。开发 IDEPyCharm Professional。它自带数据库工具和前端支持我在一个窗口里就能改后端模型、看数据库表结构、调前端断点。有人会用PyCharm 太重来质疑但我对这个项目是重度使用图它的代码跳转、重构能力和内置 HTTP Client。建议你别用社区版硬扛专业版对 Django 和 JavaScript 的支持完全是两个体验。2.3 PyCharm里的初始化步骤与工程结构我在 PyCharm 里初始化项目时按下面顺序操作避免环境混乱用 PyCharm 新建项目选择虚拟环境 venvPython 版本选 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 vitelatest初始化 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_deletemodels.CASCADE, related_namepainter_profile) display_name models.CharField(max_length50, blankTrue) avatar models.ImageField(upload_toavatars/, blankTrue) bio models.TextField(blankTrue) style_tags models.CharField(max_length200, blankTrue, help_text如厚涂、日系、Q版逗号分隔) status models.CharField(max_length10, choicesStatus.choices, defaultStatus.PENDING) created_at models.DateTimeField(auto_now_addTrue)把 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_length100) description models.TextField() style_tag models.CharField(max_length50) budget_min models.DecimalField(max_digits10, decimal_places2) budget_max models.DecimalField(max_digits10, decimal_places2) deadline models.DateField() buyer models.ForeignKey(User, on_deletemodels.CASCADE, related_nameposted_orders) painter models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_nameaccepted_orders) status models.CharField(max_length20, choicesStatus.choices, defaultStatus.PENDING) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) finished_at models.DateTimeField(nullTrue, blankTrue)这里有个设计重点painter 字段一开始为空订单是悬赏式需求谁被选中后才会被赋值。状态流转我严格控制为一条单向链路待接单 - 创作中 - 已交付 - 已验收期间只允许取消。不允许已验收之后跳回创作中也不允许跳过交付直接验收。这样虽然在极端情况不够灵活但对首期平台来说规则简单不会扯皮。3.3 沟通、报价与交付记录的细节订单不能只有一张表就完事报价和交付记录必须拆出来。报价表Quote记录每个画师对某个订单的竞争class Quote(models.Model): order models.ForeignKey(CommissionOrder, on_deletemodels.CASCADE, related_namequotes) painter models.ForeignKey(User, on_deletemodels.CASCADE) price models.DecimalField(max_digits10, decimal_places2) description models.TextField(blankTrue) is_selected models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue)交付记录表DeliveryRecord单独存在的理由是约稿经常要交多轮改稿。如果我把交付文件直接挂在订单表一个字段上那草稿、修改稿、终稿就没法区分了。我设计成class DeliveryRecord(models.Model): order models.ForeignKey(CommissionOrder, on_deletemodels.CASCADE, related_namedeliveries) version_no models.IntegerField(default1) comment models.CharField(max_length200, blankTrue) file models.FileField(upload_todeliveries/%Y/%m/) preview models.ImageField(upload_topreviews/, blankTrue) created_at models.DateTimeField(auto_now_addTrue)站点内留言我用了一张极简的 Message 表外键指向订单、发送者、内容、时间。首期没有做已读未读和实时推送只按时间正序展示够用。3.4 核心表关系一览我把核心关系总结成一张表方便你对照建表表名关键外键说明auth_user无Django 自带用户表users_painterprofileuser - auth_user画师档案与审核状态works_artworkpainter - auth_user画师作品集用于入驻审核orders_commissionorderbuyer, painter - auth_user约稿订单主表orders_quoteorder, painter画师对订单的报价orders_deliveryrecordorder订单交付记录多轮版本orders_messageorder, 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_max800这样的查询参数用 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(pkorder_id) if order.status ! CommissionOrder.Status.PENDING: raise ValidationError(当前订单不在可接单状态) if order.buyer_id ! user.id: raise ValidationError(只有需求发布者可以确认报价) Quote.objects.filter(order_idorder_id, is_selectedTrue).update(is_selectedFalse) Quote.objects.filter(pkquote_id, order_idorder_id).update(is_selectedTrue) order.painter_id Quote.objects.get(pkquote_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/:idOrderDetail.vue需求详情报价入口/loginLogin.vue登录注册/painter/applyPainterApply.vue画师入驻申请/painter/worksWorkManage.vue画师作品集管理/me/publishPublishOrder.vue发布约稿需求/me/ordersMyOrders.vue我的订单买家视角/画师视角切换/me/quotesMyQuotes.vue我发出的报价记录路由全部走 Vue Router 的 createWebHashHistory 还是 createWebHistory我的建议是首期用 hash 模式省去服务器配置。后续部署如果想美化 URL 再改 history同时配 nginx 的try_files回退。这里我在第六节会专门讲一次踩坑。5.2 状态管理、Axios二度封装和接口分层Pinia 我只存两类全局信息用户 token 和当前登录用户的基本资料。不要试图把所有服务器数据都塞进 store那会让状态变得不可控。我的封装思路是useUserStore负责 login/logout/fetchUserInfoapi/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 * 1024nginx 的client_max_body_size 50m。对原始 PSD 文件不生成缩略图只记录文件信息。个人项目不要追求服务端无限接收大文件控制在那一个合理范围内最重要。6.4 部署后刷新页面404现象部署完成后访问根路径能看到首页但在路由/orders/12下按 F5 刷新页面变成 404。原因很简单前端用的是 history 路由浏览器直接请求/orders/12时nginx 去找这个路径对应的文件或目录找不到就返回 404。必须在 nginx 配置里加一个回退规则把所有非静态文件请求都转发到前端的 index.htmllocation / { root /var/www/frontend/dist; try_files $uri $uri/ /index.html; }加上之后刷新就正常了。如果你刚开始接触部署建议要么用 hash 路由要么记得配置这条回退。这个问题在纯静态文件服务器上特别容易踩我第一次部署时也懵了半天。7. 上线前检查清单与运维护栏7.1 gunicorn、nginx与前端部署配置后端我用 gunicorn 跑 Django配置文件里指定 worker 数量和 socketgunicorn config.wsgi:application \ --bind 127.0.0.1:8001 \ --workers 3 \ --timeout 120 \ --access-logfile -nginx 我做两层第一层托管前端构建产物第二层把/api和/media反向代理到 gunicornlocation /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 -pxxx 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配置两个类就能覆盖大部分场景。这个约稿平台做下来我最深的体会是业务状态设计和并发控制远比页面好看重要。如果你是做毕设或练手哪怕系统小也一定把订单状态机、权限分层、异常处理写清楚这些才是真正能写进简历和讲出深度的部分。最后再说一点个人建议上线前找几个朋友实际约稿几轮把买家、画师两种身份都体验一遍很多状态流转漏洞都是这么暴露出来的比你反复看代码高效得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑