ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python Django+Vue实战:从零开发小区报修管理系统

Python Django+Vue实战:从零开发小区报修管理系统 开头别再让报修靠吼我是去年开始琢磨这个小区的故障报修系统的。当时我们小区的水电工手机常年占线业主报修全靠微信群接龙一条三楼漏水后面跟着十几条我家也漏最后谁处理了、处理完没有、业主满不满意全凭一张嘴。做物业系统的朋友听了直摇头说这就是典型的报修管理缺失——工单没有台账、状态不可追踪、服务无法评价。后来我花了三周时间用 Python Vue 把整套流程跑通了后端基于 Django也对比了 Flask前端用 Vue 3 Element Plus开发全程在 Pycharm 里完成。从业主提交报修、物业派单、维修工接单处理到业主确认完工并评价整个闭环我在系统里完整走了好几遍也顺手把部署方案理清了。这篇内容适合两类人一是零基础想学 Web 开发的想看看 Django Vue 这种前后端分离的项目到底怎么从零长出来二是真的有小区物业、后勤管理需求想用最小成本搭一个能用的报修平台的。你不需要一开始就懂所有概念跟着我走过一遍完整流程你就知道这类表单 状态流转的管理系统该怎么设计、怎么写、怎么避坑。1. 拆需求业主、物业、维修工三条线怎么走1.1 一个工单从提交到完结的生命周期我在写第一行代码之前先把整个报修流程在纸上画了一遍。这件事看着简单——不就是报个修嘛——但真拆开里面的角色和状态转换比想象中多。先说角色。整个系统里最少有三类人业主普通用户发起报修、查看处理进度、确认完工、评价。物业管理员电话确认、给维修工派单、处理延期或投诉。维修工接单、上门、填写处理结果。围绕这三类人一个工单的生命周期可以拆成七个状态待审核 → 待派单 → 已派单 → 处理中 → 待确认 → 已完成 → 已取消。这里多说一句为什么不是更简单的已提交 → 处理中 → 已完成三个状态因为没有物业审核环节的话业主随手提交一个我家灯泡坏了维修工就得跑一趟结果发现是灯罩松了这种根本不需要上门的事。加上审核和派单环节物业就能在电话里把问题前置过滤一遍维修工的有效工时能提高一截。状态流转规则也要提前定死业主提交后工单进入待审核此时业主可取消。物业审核通过工单进入待派单。物业指定维修工工单进入已派单维修工收到通知。维修工确认接单并开始处理状态变为处理中。维修工填写处理结果状态变为待确认。业主确认无误状态变为已完成此时才能评价。任何环节物业都可以强制取消但要填取消原因。这套状态机的设计思路其实跟订单系统是同一个套路唯一不同的是报修单有维修结果和评价这两个业务属性后面建模的时候要一起考虑进去。1.2 角色权限怎么控制角色对应的权限我在后端用的是 Django 自带的 Group Permission 机制前端则用路由守卫配合角色字段控制页面入口。最核心的一条原则是前端控制的是看不看得到后端控制的是能不能操作任何权限校验以后端为准。实际开发里我给每个角色单独建了组然后在接口层做装饰器校验。简单示意一下from django.contrib.auth.models import Group def is_in_group(group_name): def decorator(func): wraps(func) def wrapper(request, *args, **kwargs): if not request.user.groups.filter(namegroup_name).exists(): return JsonResponse({code: 403, msg: 无权限操作}, status403) return func(request, *args, **kwargs) return wrapper return decorator这里有个细节Django 的request.user在未登录时是AnonymousUser是没有groups属性的所以装饰器里必须判空不然匿名用户访问接口会直接 500。我在第一版就踩过这个坑后来改成了先判断is_authenticated。1.3 MVP 阶段到底要做哪些功能很多人一上来就想着把 App、短信通知、大屏监控统统做了结果一个月过去了连个能跑通的版本都没有。我做这个系统时强制给自己划了 MVP 边界必做登录注册手机号 密码即可报修单的创建、列表、详情、状态流转图片上传报修时拍一张现场照片三个角色的权限控制简单的后台管理页面二期再做消息推送短信、公众号模板消息维修工抢单模式超时未处理自动提醒报修数据可视化看板事实证明这个决策非常正确。MVP 边界内的东西我前后端加在一起写了大约 6000 多行代码三周时间正好能扎实地走完二期那些功能后来用 Flask 写了个数据可视化小面板单独补齐了没有影响主流程的稳定性。2. 技术选型Django还是Flask前端为什么非Vue不可2.1 Django与Flask的取舍标题里同时出现了 Django 和 Flask正好也是我被问得最多的问题这两个框架到底选哪个我的回答分三层。第一层两者的定位完全不同。Django 是全家桶式的重量级框架自带 ORM、Admin 后台、认证系统、迁移工具项目骨架一生成该有的基础设施都有了适合业务逻辑复杂、需要长期迭代的项目。Flask 是微框架核心只是一个路由 模板引擎数据库、表单验证、登录这些统统要自己配或者装第三方扩展灵活但依赖开发者自己整合。第二层我的选择逻辑。报修系统虽然看起来是个小项目但核心的用户认证、工单状态流转、权限控制都是非常通用的需求用 Django 的django.contrib.auth加上 DRFDjango REST Framework后面简称 DRF的视图集开发效率明显更高代码量能省下一大截。而如果用 Flask光是 ORM 选型、认证方案、序列化这几块就要反复对比权衡对新手很不友好。所以我主项目用 Django这是深思熟虑的结果不是随便选的。第三层Flask 还有用吗有而且很有用。我在二期给报修系统做数据统计看板时就是用 Flask 写了一个轻量接口专门给前端提供报修量趋势、维修耗时分布、满意度统计这些聚合数据。原因也很简单这部分逻辑复用了一个独立的 MySQL 库不想动 Django 主项目Flask 几十行就能起一个独立服务做完就丢非常轻巧。2.2 Vue在这个项目里解决了什么问题前端我选了 Vue具体是 Vue 3 Vite Element Plus Pinia 这套组合。为什么不用 jQuery 套模板因为这类管理系统的页面交互远比想象中多工单列表要筛选、状态要实时更新、表单要校验、弹窗要按角色控制用原生 JS 写这些代码量至少要翻两倍维护起来更是噩梦。Vue 的核心价值是数据驱动视图。简单说你只需要维护一个状态currentTab是pending就显示待审核的列表是processing就显示处理中的列表。界面会根据数据的变化自动更新不需要手动去操作 DOM。具体到报修系统前端我分成了几个核心页面登录/注册页手机号 密码登录成功后存 token。业主端我要报修表单 图片上传、我的报修列表 详情 确认 评价。物业端报修审核通过/驳回、派单选维修工。维修工端我的工单处理中列表、工单详情填写处理结果。通用组件状态标签、工单卡片、图片预览。这套页面用 Vue 写起来非常顺手v-for循环渲染工单卡片v-if按角色切换按钮v-model绑定表单数据几乎不需要手动写事件监听。2.3 Pycharm环境配置与开发效率开发工具我用的是 Pycharm而且是专业版。很多人纠结社区版能不能用我的看法是写 Django 项目社区版完全够用Django 和 Python 的支持都是内置的但如果你要用数据库可视化工具、前端代码调试、远程部署这些功能专业版会省事很多。我重点说一下环境层面容易踩的两个坑。第一个坑是虚拟环境。Python 的依赖管理特别是 Django 这种几十个包的项目直接装在全局环境里会乱成一团。我在项目根目录下创建虚拟环境python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate pip install django djangorestframework django-cors-headers pillow在 Pycharm 里解释器选择venv下的 Python这样项目依赖和系统 Python 完全隔离。第二个坑是Node 与 Vue 的环境。Vue 3 要求 Node.js 版本在 16 以上我建议直接用最新的 LTS 版本。装 Vue 脚手架用 Vite 就够了npm create vitelatest repair_frontend -- --template vue cd repair_frontend npm install npm install vue-router4 pinia axios element-plus装完这两套环境我开发时会在 Pycharm 的 Terminate 里同时起两个进程一个python manage.py runserver跑后端 8000 端口一个npm run dev跑前端 5173 端口。改完前后端代码浏览器刷新即可看到效果。3. 后端落地环境准备、项目结构与数据库建模3.1 从零创建Django项目与App后端我按标准的 Django 项目结构来建。先创建项目主目录再创建功能模块 app这里我用的是repair_project项目和repairs报修模块、accounts用户模块这两个 app。django-admin startproject repair_project cd repair_project python manage.py startapp repairs python manage.py startapp accounts python manage.py migrate在settings.py里把新建的 app 注册进去同时配置 DRF 和跨域。这里有个重要配置AUTH_USER_MODEL要指向自定义用户模型不然后续想给用户加楼栋号手机号这些字段会非常麻烦INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, rest_framework_simplejwt, corsheaders, accounts, repairs, ] AUTH_USER_MODEL accounts.User REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }3.2 数据模型设计与状态字段的心得数据库建模是整个项目里最值得花时间的部分。我在accounts/models.py里自定义了用户模型继承AbstractUser额外加上手机号和楼栋号class User(AbstractUser): phone models.CharField(max_length11, uniqueTrue) building models.CharField(max_length50, blankTrue) unit models.CharField(max_length50, blankTrue) room models.CharField(max_length50, blankTrue)报修单模型放在repairs/models.py核心字段如下class RepairOrder(models.Model): STATUS_CHOICES [ (pending, 待审核), (waiting_dispatch, 待派单), (dispatched, 已派单), (processing, 处理中), (waiting_confirm, 待确认), (completed, 已完成), (cancelled, 已取消), ] CATEGORY_CHOICES [ (water, 水管漏水), (electric, 电路故障), (appliance, 家电维修), (door, 门窗维修), (other, 其他), ] title models.CharField(max_length100) description models.TextField() category models.CharField(max_length20, choicesCATEGORY_CHOICES) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) reporter models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namereported_orders) assignee models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_nameassigned_orders) address models.CharField(max_length200) photos models.JSONField(defaultlist) cancel_reason models.TextField(blankTrue) result_note models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) assigned_at models.DateTimeField(nullTrue, blankTrue) completed_at models.DateTimeField(nullTrue, blankTrue)这里几个字段的设计思路我要重点说一下photos用 JSONField 而不是单独建图片表。因为在报修这个场景里图片只属于一个工单数量也有限一般 1 到 5 张单独建表还要维护外键关系反而麻烦。JSONField 直接存[/media/photo_1.jpg, /media/photo_2.jpg]查询起来一次就能取到简单高效。assignee用SET_NULL而不是CASCADE。如果维修工被删了工单还在不能连工单一起删掉。同理reporter用CASCADE是因为业主账号注销时他的报修记录没有保留价值。状态字段用字符串而不是数字。比如pending比1可读性强得多调试时一眼能看出当前状态在前后端传输时也更直观。3.3 序列化器与视图集的搭配DRF 的序列化器是前后端数据交换的桥梁。报修单的序列化器我最开始写得太粗暴直接用fields __all__结果创建工单时用户可以伪造assignee字段给自己派单。后来我改成按操作场景拆分序列化器RepairOrderCreateSerializer只允许传title、description、category、address、photos。RepairOrderDetailSerializer展示所有字段嵌套用户信息。RepairOrderAssignSerializer只允许传assignee_id供物业派单使用。RepairOrderResultSerializer只允许传result_note供维修工填写结果。class RepairOrderCreateSerializer(serializers.ModelSerializer): class Meta: model RepairOrder fields [title, description, category, address, photos] def create(self, validated_data): validated_data[reporter] self.context[request].user return super().create(validated_data)视图部分我用了 DRF 的ViewSet配合action定义自定义动作。这样工单的每个状态流转都变成一个可读性很高的接口class RepairOrderViewSet(viewsets.ModelViewSet): queryset RepairOrder.objects.all().order_by(-created_at) serializer_class RepairOrderDetailSerializer permission_classes [IsAuthenticated] def get_queryset(self): user self.request.user if user.groups.filter(name物业管理员).exists(): return RepairOrder.objects.all() if user.groups.filter(name维修工).exists(): return RepairOrder.objects.filter(assigneeuser) return RepairOrder.objects.filter(reporteruser) action(detailTrue, methods[post]) def audit(self, request, pkNone): order self.get_object() if request.data.get(action) approve: order.status waiting_dispatch order.save() return Response({msg: 已通过审核}) else: order.status cancelled order.cancel_reason request.data.get(reason) order.save() return Response({msg: 已驳回}) action(detailTrue, methods[post]) def assign(self, request, pkNone): order self.get_object() assignee_id request.data.get(assignee_id) order.assignee_id assignee_id order.status dispatched order.assigned_at timezone.now() order.save() return Response({msg: 派单成功})这里有个权限的细节get_queryset里我已经把每个角色的可见范围限制了但自定义动作assign只被物业管理员调用所以还需要额外加一层权限校验不能只依赖get_queryset的结果。我在assign方法里加了if not request.user.groups.filter(name物业管理员).exists(): return Response({msg: 无权限}, status403)。4. 核心接口实战登录认证与工单全流程4.1 JWT登录注册与基于角色的信息获取认证方案我用了 SimpleJWT它和 Django 自带的认证系统集成得很好不用自己维护 session 状态前端只需要在每次请求的Authorization请求头里带上 token 即可。注册接口我用 DRF 的APIView自己写的class RegisterView(APIView): permission_classes [AllowAny] def post(self, request): username request.data.get(username) password request.data.get(password) phone request.data.get(phone) building request.data.get(building) unit request.data.get(unit) room request.data.get(room) role request.data.get(role, owner) if User.objects.filter(usernameusername).exists(): return Response({msg: 用户名已存在}, status400) if User.objects.filter(phonephone).exists(): return Response({msg: 手机号已注册}, status400) user User.objects.create_user( usernameusername, passwordpassword, phonephone, buildingbuilding, unitunit, roomroom ) # 根据注册选择的角色加入对应分组 group_map {owner: 业主, property: 物业管理员, worker: 维修工} group, _ Group.objects.get_or_create(namegroup_map[role]) user.groups.add(group) return Response({msg: 注册成功})登录接口直接使用 SimpleJWT 自带的TokenObtainPairView然后前端登录成功后马上调一个/api/users/me/接口拿到用户名、角色、楼栋信息存到 Pinia 里。后端对应me接口class MeView(APIView): def get(self, request): user request.user groups [group.name for group in user.groups.all()] role unknown if 物业管理员 in groups: role property elif 维修工 in groups: role worker else: role owner return Response({ id: user.id, username: user.username, phone: user.phone, building: user.building, unit: user.unit, room: user.room, role: role })这里我想提醒大家一个容易忽略的点前端判断角色用的是/users/me/返回的 role 字段但真正决定接口能访问什么后端靠的是 token 里解析出来的用户身份 数据库里的分组信息。前端的 role 只是用来决定渲染哪些按钮和页面如果只靠前端隐藏按钮来保护接口别人拿 Postman 一样能调接口。4.2 报修单创建接口与图片上传图片上传是一个独立的接口前端先把图片传到服务器拿到 URL 后再把 URL 跟着表单数据一起提交给报修单创建接口。这样设计的理由很简单报修单创建是一个事务性操作如果在创建时才上传图片一旦表单校验失败图片已经传了又产生垃圾数据。先传图后提交用户就算提交失败也不至于丢图。图片上传接口class UploadImageView(APIView): parser_classes [MultiPartParser, FormParser] def post(self, request): file request.FILES.get(file) if not file: return Response({msg: 没有文件}, status400) if file.size 10 * 1024 * 1024: return Response({msg: 图片大小不能超过10M}, status400) allowed_types [image/jpeg, image/png, image/webp] if file.content_type not in allowed_types: return Response({msg: 不支持的图片格式}, status400) filename frepair_{timezone.now().strftime(%Y%m%d%H%M%S)}_{random.randint(1000, 9999)}.{file.name.split(.)[-1]} file_path os.path.join(settings.MEDIA_ROOT, repairs, filename) os.makedirs(os.path.dirname(file_path), exist_okTrue) with open(file_path, wb) as f: for chunk in file.chunks(): f.write(chunk) url f{settings.MEDIA_URL}repairs/{filename} return Response({url: url})4.3 工单流转的核心链路与日志记录我强烈建议给工单加一个RepairLog模型记录每一次状态变更。这不仅是排查问题的便利工具更重要的是当业主投诉我家的报修等了三天没人处理时你能拿出完整的时间线证明每个环节卡在了哪里这对物业来说是非常好的自证工具。日志模型长这样class RepairLog(models.Model): order models.ForeignKey(RepairOrder, on_deletemodels.CASCADE, related_namelogs) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.SET_NULL, nullTrue) action models.CharField(max_length50) detail models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue)每次状态变更时就追加一条日志。我在RepairOrderViewSet里封装了一个_log_order_action(order, request, action, detail)方法所有自定义动作里调用避免到处复制粘贴。5. 前端页面Vue3 Element Plus 把这些接口用起来5.1 路由设计按角色分流前端路由我用的是 Vue Router总共规划了这些路径const routes [ { path: /login, component: Login, meta: { public: true } }, { path: /register, component: Register, meta: { public: true } }, { path: /, component: Layout, redirect: /my-repairs, children: [ { path: my-repairs, component: MyRepairs, meta: { roles: [owner] } }, { path: new-repair, component: NewRepair, meta: { roles: [owner] } }, { path: audit-list, component: AuditList, meta: { roles: [property] } }, { path: dispatch, component: DispatchList, meta: { roles: [property] } }, { path: worker-orders, component: WorkerOrders, meta: { roles: [worker] } }, { path: order-detail/:id, component: OrderDetail }, { path: stats, component: StatsDashboard, meta: { roles: [property] } }, ]}, ]这里我给/order-detail/:id特意没限制角色因为业主、物业、维修工都要看同一个工单详情页面只是页面里根据当前用户的角色渲染不同的操作按钮而已。路由守卫控制进入页面的权限router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) const user useUserStore() if (to.meta.public) { if (token to.path /login) next(/) else next() return } if (!token) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(user.role)) { next(/login) return } next() })5.2 报修表单与图片上传组件报修表单用的 Element Plus 的el-form重点是校验规则。手机号、楼栋号、故障描述都要做必填校验描述还限制最少 10 个字符防止业主提交坏了这种没营养的描述。图片上传用的是el-upload我让它只接受图片格式限制大小 10M一次最多传 5 张每传一张就调后端的/api/upload/接口得到 URL最后汇总到表单的photos数组里。这里是组件的关键片段el-form refformRef :modelform :rulesrules label-width90px el-form-item label标题 proptitle el-input v-modelform.title placeholder例如厨房水管漏水 / /el-form-item el-form-item label故障类型 propcategory el-select v-modelform.category el-option label水管漏水 valuewater / el-option label电路故障 valueelectric / el-option label家电维修 valueappliance / el-option label门窗维修 valuedoor / el-option label其他 valueother / /el-select /el-form-item el-form-item label详细地址 propaddress el-input v-modelform.address placeholder自动带出也可修改 / /el-form-item el-form-item label问题描述 propdescription el-input typetextarea v-modelform.description :rows4 / /el-form-item el-form-item label现场照片 el-upload list-typepicture-card :http-requesthandleUpload :limit5 :on-removehandleRemove span点击上传/span /el-upload /el-form-item /el-form注意el-upload的http-request属性它是用来重写上传行为的。默认的action属性走的是标准 form 上传但我们的接口需要带 token需要自定义实现const handleUpload async (options) { const formData new FormData() formData.append(file, options.file) const { data } await axios.post(/api/upload/, formData, { headers: { Content-Type: multipart/form-data } }) form.value.photos.push(data.url) }5.3 工单列表与状态跟踪工单列表的核心逻辑就是根据用户角色拉取不同接口 按状态筛选。物业端的审核列表长这样el-tabs v-modelactiveStatus el-tab-pane label待审核 namepending RepairCard v-fororder in filteredOrders(pending) :keyorder.id :orderorder / /el-tab-pane el-tab-pane label待派单 namewaiting_dispatch RepairCard v-fororder in filteredOrders(waiting_dispatch) :keyorder.id :orderorder / /el-tab-pane el-tab-pane label处理中 nameprocessing RepairCard v-fororder in filteredOrders(processing) :keyorder.id :orderorder / /el-tab-pane /el-tabsRepairCard是一个通用卡片组件展示报修标题、分类、楼栋号、状态标签、提交时间。点击卡片进入详情页。详情页再根据角色显示不同按钮物业看到审核通过派单维修工看到开始处理填写结果业主看到确认完工评价。这里分享一个开发效率的经验前后端接口字段一定提前对齐特别是状态码和枚举值。我一开始前后端各写各的后端返回pending前端判断的是0导致列表页面一片空白花了大半天排查才发现是枚举值没对齐。后来我在前端单独建了一个constants.js文件把后端的状态枚举值全部抄进去统一从那里引用// constants.js export const REPAIR_STATUS { pending: { label: 待审核, color: warning }, waiting_dispatch: { label: 待派单, color: primary }, dispatched: { label: 已派单, color: info }, processing: { label: 处理中, color: danger }, waiting_confirm: { label: 待确认, color: warning }, completed: { label: 已完成, color: success }, cancelled: { label: 已取消, color: info } }6. 前后端联调跨域、时间格式、接口对不上的那些事6.1 CORS跨域问题的标准解法前端跑在 5173 端口后端跑在 8000 端口这就是典型的跨域场景。浏览器会拦截跨域请求后端必须明确告诉浏览器这个来源我可以接受。我在后端用了django-cors-headers配置如下CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ] CORS_ALLOW_CREDENTIALS True注意这里不要图省事直接写CORS_ALLOW_ALL_ORIGINS True生产环境会埋下安全隐患。我自己开发时确实为了省事全放过最后部署前改成白名单结果漏掉了部署域名的前端地址又是几分钟的排查。6.2 时间格式和枚举值对齐前后端联调最容易翻车的三件事我掰着指头数一下一是时间格式。Django 序列化默认输出的时间是带Z和00:00后缀的 ISO 格式比如2024-06-01T08:30:00Z。前端如果直接用new Date()解析时区不同显示的时间会差 8 个小时。我在前端做了一个统一的工具函数const formatTime (isoStr) { const date new Date(isoStr) const offset 8 * 60 const localDate new Date(date.getTime() offset * 60 * 1000) return localDate.toLocaleString(zh-CN, { hour12: false }) }二是枚举字段。后端返回status是英文字符串前端展示要转换成中文并且要根据状态显示不同颜色所以我做了REPAIR_STATUS映射表。后端返回的category同理也有映射表。三是主键与关联信息。报修单详情里的reporter和assignee字段直接返回外键 ID 其实对前端不够友好——前端拿了一个assignee_id还得再调接口查维修工叫什么名字、什么手机号。我建议序列化器里嵌套展示关键信息class RepairOrderDetailSerializer(serializers.ModelSerializer): reporter_name serializers.CharField(sourcereporter.username, read_onlyTrue) reporter_phone serializers.CharField(sourcereporter.phone, read_onlyTrue) assignee_name serializers.CharField(sourceassignee.username, read_onlyTrue, default) assignee_phone serializers.CharField(sourceassignee.phone, read_onlyTrue, default) class Meta: model RepairOrder fields [id, title, ...]6.3 调试工具与Mock数据经验联调阶段我强烈推荐两个工具Postman 和后端日志。Postman 用来单独测接口可以不经过前端快速确认问题到底出在前端还是后端。Postman 里要记得在 Authorization 里配置 Bearer token否则所有接口都会返回 401。后端的日志输出也很关键。我在settings.py里配置了简单的日志LOGGING { version: 1, handlers: { console: { class: logging.StreamHandler, } }, loggers: { django.request: { handlers: [console], level: DEBUG, } } }这样每次前端调接口后端控制台都会打印请求路径、耗时、状态码和异常信息排查问题效率能翻一倍。我遇到过的最诡异的一次 bug前端页面一直报 404但我在浏览器地址栏直接输入接口 URL 又能访问。最后检查发现是前端的 Axios 基础路径配置错了——axios.defaults.baseURL /api但因为前端跑在 5173 端口这个/api被解析成了http://localhost:5173/api根本没打到后端。正确配置应该是完整的http://localhost:8000/api当时想打自己一顿的心都有了。7. 部署与后续优化把系统放上生产环境7.1 生产服务器选的什么方案开发环境跑通之后还有一个现实的问题系统要真正给物业用就不能一直停在localhost上。我的部署方案是经典组合Nginx Gunicorn MySQL。部署流程大致是服务器装好 Python、Node把项目代码克隆过去。后端项目里创建虚拟环境安装依赖python manage.py collectstatic把静态文件收集到一个目录。Gunicorn 起 Django 服务gunicorn repair_project.wsgi:application -w 4 -b 127.0.0.1:8001。前端项目执行npm run build生成dist静态文件目录交给 Nginx 托管。Nginx 配置访问根路径/返回前端页面访问/api/前缀的请求反向代理到127.0.0.1:8001。Nginx 的关键配置片段server { listen 80; server_name your-domain.com; # 前端静态文件 root /var/www/repair_frontend/dist; index index.html; # 前端路由需要否则刷新页面会 404 location / { try_files $uri $uri/ /index.html; } # 后端接口代理 location /api/ { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 媒体文件报修图片 location /media/ { alias /var/www/repair_project/media/; } }这里面有一个部署后才会暴露的坑Django 的DEBUG False时不会自动提供静态文件服务所以图片和静态资源必须交给 Nginx 处理。如果你忘了配置/media/的 alias业主上传的图片全部裂掉检查半天才反应过来。7.2 用Flask扩展的数据看板前面提到的数据统计看板我用 Flask 单独写了。这段代码非常简单从报修库里算几个聚合指标输出 JSON 给前端图表组件from flask import Flask, jsonify import pymysql app Flask(__name__) app.route(/stats/repair-trend) def repair_trend(): conn pymysql.connect(hostlocalhost, userroot, passwordxxx, databaserepair_db, charsetutf8mb4) cursor conn.cursor() cursor.execute( SELECT DATE(created_at) AS d, COUNT(*) AS cnt FROM repairs_repairorder WHERE created_at DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY d ) rows cursor.fetchall() conn.close() return jsonify([ {date: str(row[0]), count: row[1]} for row in rows ]) if __name__ __main__: app.run(port5000)前端统计页面用 ECharts 引入折线图和柱状图直接展示近 30 天报修量、各类型占比、各楼栋报修排行。这样物业在开周会时能直接打开大屏汇报领导和同事都对系统价值有直观感知。7.3 后续值得扩展的方向系统上线之后明显能感知到几个新需求开始浮出水面一是消息推送。工单从待审核变成已派单时业主和维修工都希望第一时间收到通知。目前最简单的方案是接入企业微信/飞书群机器人 Webhook状态变更时往群里推一条消息成本几乎为零。二是超时预警。报修单长时间停留在某个状态系统应该提醒物业去干预。我建议在 Django 里加一个定时任务Celery beat 或简单点用 APScheduler每五分钟扫一遍超时工单给物业发提醒。三是服务评分体系。目前已经有业主评价功能后续可以按月统计每个维修工的平均分和完成量作为绩效考核依据。这个需求物业非常看重因为技术维修人员的服务状态很难量化评价数据就是最好的抓手。结尾的话最后说说做这个项目最深的体会一个系统能不能真正用起来关键不在于功能多炫而在于流程是否完整、状态是否有迹可循、数据是否能推动改进。我见过不少团队花大价钱做报修 App结果用一个月就弃用了就是因为工单流转设计得模棱两可卡在中间状态没人负责。这类管理系统的本质是把线下的口头约定固化成线上的状态机让每个环节的负责人和耗时都清清楚楚。如果你也想搭一个类似的系统我的建议是从最小闭环开始先把手动的工单流转跑顺再考虑自动化、可视化这些锦上添花的事。过程中遇到的具体问题欢迎随时交流——代码里的每个坑我都是踩过之后才知道深浅的希望这篇内容能帮你少走几步弯路。
返回列表