恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
社区维修平台全栈开发实战:Django+Vue从建模到部署
首页
资讯中心
/
社区维修平台全栈开发实战:Django+Vue从建模到部署
社区维修平台全栈开发实战:Django+Vue从建模到部署
发布时间:2026/10/13 7:31:06
社区维修平台这个题目这几年在毕业设计和全栈练手项目里出现频率很高。原因不复杂它业务链路清晰——住户报修、后台派单、维修工接单、维修完成、双方评价该有的前后端交互全都有又不会像电商项目那样庞大到做不完。用 Python 做后端、Vue 做前端开发环境落在 PyCharm 里恰好是很多教程和需求说明里最常见的组合。但真正动手之前大多数人卡在第一个问题上后端到底用 Django 还是 Flask标题里两个都写了查资料也是各执一词。我做完整个项目之后的体会是选型本身没有对错关键是你想用多少框架内置能力、愿意承受多少自行组装成本。这篇就把我从业务梳理、数据库设计、接口开发、前端联调一直到部署上线的完整过程捋一遍里面的模型、代码和踩坑记录都是实际跑过的给准备做同类项目的人一份可以直接照着走的参考。1. 社区维修平台的业务底盘先搞清楚谁在用、流程怎么走1.1 平台解决的核心痛点传统的社区报修场景大多数还停留在电话报修、物业微信群接龙、楼栋下面贴纸质通知单。故障描述靠截图维修进度靠追问有没有修好靠复查服务质量更是没有记录。这个平台要解决的就是把这条散乱的链路收拢成一条可在系统里追踪的线上流程住户提交报修时能上传现场照片写清楚故障类型和期望上门时间。管理员能统一审核、分类、派单不再靠电话来回确认。维修工能查看分派给自己的工单接单后随时更新维修状态。完工之后住户能确认验收并评价管理员能按维修工维度做统计。这四点分别对应四个角色视角住户要便捷管理员要可控维修工要清晰后续要能考核。先把这个业务底盘想明白后面的数据表设计和接口划分才有依据。很多项目做到一半烂尾问题不在技术而是没有在建模之前把这条链路写清楚。1.2 三类角色的完整操作链路社区维修平台的用户一共三类住户、维修工、管理员。用文字把核心流程串起来是这样的住户注册登录后选择报修分类水电、家电、门窗、管道疏通等填写问题描述、定位地址、联系人信息上传现场照片提交后生成维修单。管理员在后台看到待审核工单核对信息无误后将工单派给指定维修工。维修工收到派单通知查看工单详情确认接单并与住户联系约定上门时间。维修完成后维修工在系统里录入处理结果、更新状态。住户确认维修结果可对维修工打分评价。工单的状态流转就是这条链路的核心骨骼。我第一版设计了这几个状态状态值含义谁可以触发触发后的结果pending待审核住户提交工单进入管理员待办列表assigned已派单管理员工单出现在指定维修工列表accepted已接单维修工工单锁定给该维修工completed已完成维修工工单变成可评价状态cancelled已取消住户或管理员工单终结这套状态机只保留最必要的路径没有搞并行审批、转派那些复杂场景。第一版够用后续要加“已驳回”或“改派”也很方便状态值本身就是可扩展的。1.3 项目边界第一版做到什么程度不会烂尾做全栈项目最容易犯的错误就是一上来想把所有功能都塞进去。在线支付、实时聊天、地图定位派单、消息推送这些听起来很加分的功能实际上会把你拖进完全没有必要的大坑。第一版我的功能清单长这样模块功能点优先级用户认证注册、登录、角色区分、个人信息必须报修管理提交报修、工单列表、工单详情、状态跟踪必须派单管理管理员审核、指派维修工、查看待处理必须维修工端我的工单、接单、完工回传必须评价体系评分、评价内容建议管理后台分类管理、用户管理、工单统计建议明确不做的东西在线支付涉及资金安全和第三方对接、实时聊天需要 WebSocket 或第三方 IM、自动派单需要分配算法。把这些切掉之后项目的核心就是一个多角色的 CRUD 状态机管理系统复杂度完全可控。先跑通主链路后面任何一个分支想加都可以在不推翻现有数据结构的前提下加进去。2. Django还是Flask这个项目里我为什么选DjangoPyCharm怎么配合2.1 两个框架的真实差异很多文章把 Django 和 Flask 的对比写成“重量级 vs 轻量级”这个概括太粗了。对这个项目来说真正的差异体现在三件事上。第一是内置能力。Django 自带 Admin 后台、ORM、迁移工具、用户认证体系我几乎不需要组装任何额外组件就能开始写业务代码。Flask 本身只有路由和模板你要的每一样东西都得自己挑插件凑ORM 用 SQLAlchemy迁移用 Alembic认证用 Flask-Login 或 Flask-JWT-Extended表单校验自己写或者接 WTForms。组件选型本身就要花不少时间而且插件之间的兼容性和文档质量参差不齐。第二是 API 开发体验。用 Django 配 Django REST FrameworkDRF序列化器、视图集、分页、权限、认证一整套都是配套好的一个 ListCreateAPIView 就能把列表和新增两个接口做完。用 Flask 做 API 也能做而且写起来很爽快但到后期需要自己处理全局异常、统一返回格式、请求参数校验这些基础能力时组装成本会逐渐暴露出来。第三是项目维护性。社区维修平台的模型不算少用户、报修分类、工单、状态日志、评价彼此有关联。Django 的 Admin 后台可以直接可视化管理这些表对前期联调和后期演示都极其方便。Flask 的灵活性在这类业务型项目里并不能带来明显收益反而会因为缺乏约定而增加纠结成本。2.2 我的选型结论与理由这个项目我最终选择了 Django 作为主框架理由很直接项目里有明确的“管理后台”需求Django Admin 几乎是白送的ORM 自带的迁移工具在前期模型频繁调整时救了我很多次DRF 对认证和权限有现成的 TokenAuthentication permission_classes不需要自己设计一套安全体系。那 Flask 是不是不行不是。它适合的场景是你只需要暴露少量纯 API、服务端几乎不做页面渲染、团队对 Flask 生态非常熟悉。如果强行给它套一个业务管理后台你需要自己拼的东西会多一点。为方便对比我列一个 Flask 实现创建工单接口的示意代码# Flask 版本核心路由示意 from flask import Blueprint, request, jsonify, g from .models import RepairOrder, db api Blueprint(api, __name__) api.post(/orders) def create_order(): data request.get_json() order RepairOrder( creator_idg.current_user.id, titledata[title], descriptiondata.get(description, ), category_iddata[category_id], ) db.session.add(order) db.session.commit() return jsonify({id: order.id}), 201路由很简洁但你会注意到用户身份从哪来g.current_user 需要中间件、参数校验谁来做、错误处理谁来兜底、统一返回格式怎么保证这些 Flask 都不会替你回答。Django 这边 DRF 的序列化器天然承担了参数校验认证类天然承担了身份解析开发体验完整很多。2.3 PyCharm在开发流程里的正确角色标题里带了 PyCharm说明不少人是拿它做主力 IDE 的。我的使用方式是这样的创建项目时让 PyCharm 自动建 venv 虚拟环境解释器直接选项目内的 python避免全局环境被搞乱。Run Configuration 里配置 Django Server管理端用 PyCharm 的 Manage.py 任务执行 makemigrations、migrate、createsuperuser比在终端敲命令更直观。PyCharm 的 Database 工具直接连 SQLite 或 MySQL能实时看到表结构和数据变化联调时调试效率很高。前端 Vue 部分PyCharm 内置了 Vue 插件可以识别 .vue 文件、补全模板语法但我不建议把前端开发服务器也塞进 PyCharm 的同一套 Debug 配置里。前端仍然用独立的 npm run dev 跑在另一个端口两边独立调试。调试接口时用 PyCharm 自带的 HTTP Client写 .http 文件直接调接口比在 Postman 和 IDE 之间来回切换顺手很多。一个提醒PyCharm 社区版对 Django 语言模板的支持有限专业版才有完整的 Django 面板和数据库工具。如果你用的是社区版也没关系核心功能通过命令行和普通文件编辑也能完成只是少了些可视化便利。3. 数据库与模型设计让维修单的状态流转有据可依3.1 核心表结构与字段说明数据库设计是整个项目成败最关键的部分。我最终建了 5 张表用户表、报修分类表、维修单表、状态日志表、评价表。用户表直接继承 Django 的 AbstractUser加上业务字段from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): ROLE_CHOICES ( (resident, 住户), (worker, 维修工), (admin, 管理员), ) role models.CharField(角色, max_length10, choicesROLE_CHOICES, defaultresident) phone models.CharField(手机号, max_length11, uniqueTrue) avatar models.ImageField(头像, upload_toavatars/, nullTrue, blankTrue) def __str__(self): return f{self.username}({self.get_role_display()})报修分类表和维修单表这样设计class RepairCategory(models.Model): name models.CharField(分类名称, max_length32, uniqueTrue) description models.TextField(描述, blankTrue) sort_order models.IntegerField(排序, default0) class Meta: ordering [sort_order] class RepairOrder(models.Model): STATUS_CHOICES ( (pending, 待审核), (assigned, 已派单), (accepted, 已接单), (completed, 已完成), (cancelled, 已取消), ) order_no models.CharField(工单编号, max_length32, uniqueTrue) title models.CharField(报修标题, max_length128) description models.TextField(问题描述) category models.ForeignKey(RepairCategory, on_deletemodels.PROTECT, verbose_name报修分类) images models.JSONField(现场图片, defaultlist) address models.CharField(报修地址, max_length256) contact_name models.CharField(联系人, max_length32) contact_phone models.CharField(联系电话, max_length11) creator models.ForeignKey(User, on_deletemodels.CASCADE, related_namecreated_orders, verbose_name报修人) assignee models.ForeignKey( User, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_nameassigned_orders, verbose_name维修工 ) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: ordering [-created_at]几个字段值得解释一下。order_no 是展示给住户和维修工看的业务编号比如按日期生成的字符串不等同于自增 id这样做是为了让用户在电话沟通时能报出一个有意义的编号。images 用 JSONField 存现场照片的相对路径列表对于第一版来说比单独建一张图片表简单而且读取工单详情时一次就能拿到全部图片。assignee 设置成可空因为工单在派单之前没有维修工。category 用 PROTECT 删除策略避免删了分类导致历史工单失去关联。状态日志表和评价表class OrderStatusLog(models.Model): order models.ForeignKey(RepairOrder, on_deletemodels.CASCADE, related_namestatus_logs) from_status models.CharField(原状态, max_length20, blankTrue) to_status models.CharField(新状态, max_length20) operator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name操作人) remark models.CharField(备注, max_length256, blankTrue) created_at models.DateTimeField(操作时间, auto_now_addTrue) class Meta: ordering [created_at] class OrderEvaluation(models.Model): order models.OneToOneField(RepairOrder, on_deletemodels.CASCADE, related_nameevaluation) score models.IntegerField(评分, default5) comment models.TextField(评价内容, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue)3.2 为什么状态需要单独日志表很多新手会把状态设计成工单表里的一个字段就收工了这没错但你会在后面遇到两个痛点第一前端想展示“进度时间线”时你只有最终状态没有中间过程第二一旦住户和管理员对维修过程有争议你无法还原每一步是谁在什么时间做了什么操作。单独建状态日志表之后每次状态变更都插入一条记录。工单详情页只需要把日志列表按时间倒序排出来就是一个现成的进度时间线。这不仅帮助了前端也帮助了你自己排查问题某条数据状态不对时直接看日志表就知道哪里流转错了。如果不想一开始就建这么多表有一个妥协方案在 RepairOrder 里加一个 JSONField 字段存状态变更数组。好处是模型少一张表坏处是没法按操作人、状态做 SQL 统计数据也会越积越大。我的建议是既然都做全栈项目了状态日志表花不了多少工作量但带来的规范性和扩展性很值。3.3 权限模型角色字段还是用户组社区维修平台的权限需求其实很清晰住户只能操作自己创建的工单维修工只能操作分派给自己的工单管理员可以看全量数据。我就在 User 上直接加 role 字段接口层再做校验。这里不推荐用 Django 自带的 Group 权限做细粒度控制。原因是项目的权限维度是“按角色 按归属”不是“按权限码”。用 role 字段最直白from rest_framework import permissions class IsOwnerOrAdmin(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.user.role admin: return True return obj.creator request.user视图层按角色过滤 queryset 是同样的思路。你会发现这种模型在面对“住户改自己的工单”“维修工接自己片区的单”这类需求时代码写起来都是同一个套路非常一致。把权限逻辑集中在一两个 Permission 类里尽量别散落在各个视图函数中。4. 后端接口实现从创建报修到接单完成的完整闭环4.1 接口清单与统一返回格式业务梳理清楚后接口列表其实很好列。第一版我设计了这些接口方法路径说明权限POST/api/auth/login/登录获取 token公开POST/api/users/register/住户注册公开GET / PUT/api/users/me/当前用户信息登录用户GET/api/categories/报修分类列表登录用户GET / POST/api/orders/工单列表 / 新建工单登录用户GET/api/orders/{id}/工单详情含状态日志登录用户POST/api/orders/{id}/accept/维修工接单维修工POST/api/orders/{id}/complete/维修工完工维修工POST/api/orders/{id}/evaluate/住户评价住户GET/api/orders/report/工单统计管理员统一返回格式用的是 DRF 默认结构成功直接返回数据失败返回{detail: 错误信息}。前端 axios 拦截器统一处理错误提示这样后端不需要每个接口都手动包一层 code/msg。4.2 报修单创建接口的完整实现创建接口是整个后端最核心的一环。序列化器负责参数校验视图负责身份对齐核心逻辑写在序列化器的 create 方法里# serializers.py from rest_framework import serializers from .models import RepairOrder, OrderStatusLog class RepairOrderCreateSerializer(serializers.ModelSerializer): class Meta: model RepairOrder fields [category, title, description, images, address, contact_name, contact_phone] def create(self, validated_data): user self.context[request].user order RepairOrder.objects.create( creatoruser, order_noself._generate_order_no(), **validated_data, ) OrderStatusLog.objects.create( orderorder, from_status, to_statuspending, operatoruser, remark住户提交报修, ) return order def _generate_order_no(self): from django.utils import timezone from datetime import datetime prefix datetime.now().strftime(%Y%m%d) count RepairOrder.objects.filter(order_no__startswithprefix).count() 1 return f{prefix}{count:04d}几个设计细节用户身份不从请求体里传而是从self.context[request].user拿。登录用户由 DRF 的 TokenAuthentication 解析避免伪造他人身份。创建工单时同时写入状态日志。这是保证流程可追溯的关键必须在同一个事务里完成。虽然这里没显式加 transaction.atomic但create方法内部本身处于 DRF 的请求事务上下文中为了更正规可以加上。工单编号用日期加计数的格式比如 202405120023住户在电话沟通时也能念得清楚比纯数字 id 友好很多。视图层就是一个标准视图# views.py from rest_framework import generics, permissions from .models import RepairOrder from .serializers import RepairOrderCreateSerializer, RepairOrderListSerializer class RepairOrderListCreateView(generics.ListCreateAPIView): permission_classes [permissions.IsAuthenticated] def get_serializer_class(self): if self.request.method POST: return RepairOrderCreateSerializer return RepairOrderListSerializer def get_queryset(self): user self.request.user if user.role resident: return RepairOrder.objects.filter(creatoruser) if user.role worker: return RepairOrder.objects.filter(assigneeuser) return RepairOrder.objects.all()这里最值得注意的就是get_queryset的角色过滤。数据隔离不放在前端页面控制而是后端在查询源头就把不该看到的数据过滤掉这是安全底线。4.3 接单和完工并发场景下的数据安全接单接口是后端最容易写漏的地方。想象一个场景一个工单被派给了某个片区结果片区内两位维修工几乎同时看到并点击“接单”。如果不做保护两个人都可能接单成功虽然工单表里最终只有一个 assignee但被抢单的那位体验极差而且逻辑上违背了“一人接单其他不可见”的规则。解决办法是悲观锁加事务from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response from rest_framework import permissions from django.db import transaction api_view([POST]) permission_classes([permissions.IsAuthenticated]) def accept_order(request, pk): if request.user.role ! worker: return Response({detail: 只有维修工可以接单}, status403) with transaction.atomic(): order ( RepairOrder.objects .select_for_update() .filter(pkpk, statusassigned) .first() ) if order is None: return Response({detail: 工单不存在或已被其他维修工接走}, status400) order.assignee request.user order.status accepted order.save() OrderStatusLog.objects.create( orderorder, from_statusassigned, to_statusaccepted, operatorrequest.user, remark维修工接单, ) return Response({detail: 接单成功})select_for_update()会在事务期间锁住这行记录。第二个维修工再去查时会因为状态已经不是 assigned 而查不到自然就返回“已被接走”。完工接口的思路完全一样只不过要把状态从 accepted 改成 completed同时记录完工备注和实际耗时。这两个接口代码不长但并发正确性全靠那块数据库锁。5. Vue前端页面规划、路由守卫和报修交互的实现5.1 项目初始化与目录规划前端我用 Vue 3 Vite 起步组件库选了 Element Plus状态管理用 Pinia请求用 axios。初始化和安装依赖就几条命令npm create vuelatest cd frontend npm install npm install element-plus axios vue-router pinia目录规划很关键直接决定后面写页面不迷路src/ ├── api/ # 所有接口请求函数 ├── router/ # 路由配置与守卫 ├── stores/ # Pinia 状态比如用户信息、token ├── views/ # 页面级组件登录、住户工作台、维修工工作台、管理后台 ├── components/ # 可复用组件工单卡片、状态标签、图片上传 └── utils/ # axios 实例、格式化工具api 目录单独建一层的好处是组件里不直接出现 axios 调用统一走封装好的函数。这样后端接口地址需要调整时只需要改一个文件。axios 实例的封装也在这里处理。请求拦截器自动把 localStorage 里的 token 拼到 Authorization 头响应拦截器统一处理 401 跳登录和错误弹窗// utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 15000, }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Token ${token} } return config }) service.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.response?.data?.detail || 请求失败) return Promise.reject(error) } )5.2 路由守卫让用户进不到不属于自己的页面前端路由分三大块住户端、维修工端、管理端。不可能让一个住户通过改 URL 直接进到管理后台所以路由守卫必须做两层登录校验和角色校验。// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/layout/MainLayout.vue), meta: { requiresAuth: true }, children: [ { path: resident, component: () import(/views/ResidentHome.vue), meta: { role: resident } }, { path: worker, component: () import(/views/WorkerHome.vue), meta: { role: worker } }, { path: admin, component: () import(/views/AdminHome.vue), meta: { role: admin } }, ], }, ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) return } if (to.meta.role to.meta.role ! role) { next(/login) return } next() })注意前端路由守卫只是体验上的引导真正的安全边界始终在后端接口的角色与归属校验里。前端隐藏按钮、拦截路由都是锦上添花后端漏了校验才是真事故。5.3 报修表单与工单列表的实现报修表单是这个项目里交互最丰富的组件。分类选择、问题描述、图片上传、联系信息四块内容对应后端创建接口的字段。Element Plus 的表单校验写起来很省心template el-form :modelform :rulesrules refformRef label-width100px el-form-item label报修分类 propcategory el-select v-modelform.category placeholder请选择故障类型 el-option v-foritem in categories :keyitem.id :labelitem.name :valueitem.id / /el-select /el-form-item el-form-item label问题描述 propdescription el-input typetextarea :rows4 v-modelform.description placeholder尽量写清楚故障现象、发生时间、是否影响正常生活 / /el-form-item el-form-item label现场照片 propimages el-upload :actionuploadUrl :headersuploadHeaders :on-successhandleUploadSuccess list-typepicture-card :limit3 el-iconPlus //el-icon /el-upload /el-form-item /el-form /template提交逻辑里最容易踩的坑是——图片上传返回的数据要回填进 form.images而且要在提交请求前确认它是数组形式。后端 JSONField 接收一个路径字符串列表前端在处理 on-success 时直接把返回的相对路径 push 进数组这样和接口字段完全对齐。工单列表页接下来就好写了无非是表格加状态筛选。每个状态是一个带颜色的 tag把后端返回的状态值和前端展示映射对应上const statusMap { pending: { label: 待审核, type: warning }, assigned: { label: 已派单, type: info }, accepted: { label: 已接单, type: primary }, completed: { label: 已完成, type: success }, cancelled: { label: 已取消, type: danger }, }详情页不只是展示工单信息还要把状态日志列表渲染成一条时间线。因为后端有 OrderStatusLog 表前端只需要按时间排一下每一行显示状态变化、操作人、时间、备注整个维修过程就一目了然。用户看得明白演示给导师或客户看也有说服力。6. 前后端联调CORS、时间格式和图片上传这几个坎6.1 开发环境跨域一次完整的排查过程前后端分离开发时前端跑在 localhost:5173后端跑在 localhost:8000端口不同就必然触发浏览器同源策略。我第一次联调时打开页面请求工单列表控制台直接报错Access to XMLHttpRequest at http://localhost:8000/api/orders/ from origin http://localhost:5173 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource.排查链路是这样的先直接在后端浏览器地址栏访问http://localhost:8000/api/orders/发现接口本身返回正常说明问题不在后端逻辑而在浏览器拦截了跨域响应。再看 Network 面板发现请求发出去了但响应头里没有Access-Control-Allow-Origin。根因就定位到了后端没有处理 CORS。解决办法是装 django-cors-headers 并在 settings 里配置允许的源INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # ... ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ]这里有个经验开发阶段不要把CORS_ALLOW_ALL_ORIGINS直接设成 True虽然省事但等于对任意网站开放接口调用。线上如果走反向代理同域部署甚至可以把 CORS 配置删掉交给 Nginx 处理。6.2 时间、金额和布尔值前后端字段对齐联调时最隐蔽的问题往往不是什么大报错而是字段格式对不上。DRF 默认把 DateTimeField 序列化成类似2024-05-12T10:30:00.12345608:00的 ISO 字符串前端如果不处理页面上出现的就是一串带 T 和毫秒的机器时间非常难看。我处理的办法是后端自定义时间格式化字段from rest_framework import serializers from django.utils import timezone class LocalDateTimeField(serializers.DateTimeField): def to_representation(self, value): local timezone.localtime(value) return local.strftime(%Y-%m-%d %H:%M)序列化器里涉及时间显示的字段统一用这个类前端拿到就是 2024-05-12 10:30 这种可直接展示的格式。如果后端不想改前端也可以用 dayjs 格式化但那样每个页面都要处理一次不如源头统一。另一个字段对齐问题是金额。维修平台第一版没做在线支付但如果工单有费用字段务必用 DecimalField不要用 FloatField。浮点数在计算时会出幺蛾子比如 0.1 0.2 不等于 0.3 这种经典问题。前后端传输时金额用字符串或整数分别直接用小数。6.3 图片上传本地存储与前端组件的配套写法图片上传是整个项目中容易做到“能用但不舒服”的部分。开发环境最简单的方案是 Django 的 MEDIA 配置MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media主路由在 DEBUG 模式下挂载媒体文件访问from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)上传接口可以单独写一个视图用 ImageField 接收文件保存后返回相对路径api_view([POST]) permission_classes([permissions.IsAuthenticated]) def upload_image(request): file request.FILES.get(file) if not file: return Response({detail: 未接收到文件}, status400) # 用 django 的 ImageField 校验图片类型 from django.core.files.storage import default_storage path default_storage.save(fuploads/{file.name}, file) return Response({url: f{settings.MEDIA_URL}{path}})前端的 el-upload 把 action 指向上传接口然后通过 headers 带上 token。上传成功后的 on-success 回调里把返回的url字段 push 进 form.images。联调时要特别注意后端返回的路径是相对路径还是完整 URL必须在一开始就约定统一。我用的约定是后端返回相对路径前端展示时拼上 baseURL避免开发和生产环境域名不一致导致图片裂开。7. 部署上线与项目收尾从本地跑起来到真实环境能看7.1 分离部署方案gunicorn nginx 的最小配置本地开发结束后总要面临“把项目真实跑起来给别人看”这一步。前后端分离项目最省心的部署方式就是后端用 gunicorn 跑 Django前端 npm run build 打成静态文件Nginx 负责接管静态资源并把 /api 转发给后端。后端构建pip install gunicorn python manage.py collectstatic --noinput gunicorn config.wsgi:application --bind 127.0.0.1:8000前端构建npm run build # 产物在 dist/ 目录Nginx 配置核心片段server { listen 80; server_name example.com; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /media/ { alias /path/to/project/media/; } location /admin/ { proxy_pass http://127.0.0.1:8000; } location / { root /path/to/project/dist; try_files $uri $uri/ /index.html; } }try_files $uri $uri/ /index.html这行是有讲究的Vue Router 用 history 模式时用户直接访问example.com/resident服务器上并没有这个物理文件必须回退到 index.html 让前端路由接管否则会白屏或 404。这是部署阶段最容易漏的一步。依赖的第三方服务里数据库生产环境我会换成 MySQL 或 PostgreSQL不再用 SQLite。改动其实不大settings 里配置好数据库连接跑一次 makemigrations 和 migrate 就行。7.2 做完这个项目之后可以继续扩展的能力第一版跑通主链路之后如果还想往上加东西我的建议是按性价比排序不要全都要维修费用与结算。工单表加费用字段完工时记录工时和配件消耗住户确认后生成账单。这个扩展对现有表结构影响最小。消息通知。每次状态变化时通过邮件或短信通知相关方。Django 自带邮件框架短信接第三方服务。工作量不大体验提升明显。数据统计报表。管理后台按维修工统计单量、按时完成率、平均评分。用 Django ORM 的聚合查询就能做前端接一个图表库。地图派单。如果维修工按片区管理可以给工单加经纬度接单列表按距离排序。这个需要引入地图服务复杂度会高一些。小程序端。业务逻辑已经在后端了小程序只是多一层客户端适配但需要处理登录态和授权属于另一个量级的活。这些扩展方向都有一个前提第一版的数据建模没有走偏。加了状态日志表后面做时间线和统计都省事角色字段清晰后面接小程序端也是同一套后端接口。做这种全栈项目我的体会是把业务链路翻译成数据结构的能力远比背下某个框架的 API 重要。状态机设计对了后面写接口、写页面都像按图纸施工状态机设计糊了后面每一步都在打补丁。社区维修平台这个题目之所以经典就是因为它用最小的复杂度逼迫你把“从用户操作到数据流转”这条链路完整想一遍。希望这份从业务到部署的完整记录能帮你少走几段弯路。