新闻详情

新闻详情

首页 / 资讯中心 / 详情

Django+Vue企业流程控制实战:状态机、文件流与Windows部署

发布时间:2026/9/20 2:29:37来源:尧图网络
Django+Vue企业流程控制实战:状态机、文件流与Windows部署
今年做了好几套企业内部的流程系统最有代表性的一套是用 Django Vue 前后端分离搭起来的工程流程控制系统。整个项目从需求梳理到上线部署前后大概花了一个半月踩了不少坑也沉淀了不少可以直接复用的经验。这篇文章就是围绕这套系统展开讲讲我当时是怎么设计技术方案、怎么写后端核心逻辑、怎么搞定前端那些“看起来简单、做起来麻烦”的事儿以及最后在 Windows Server 上怎么用 waitress nginx 跑起来的。如果你正准备用 Python 做企业级应用或者是一个刚要接触 Django 和 Vue 的开发者这篇文章应该能帮你少走很多弯路。里面涉及的内容从 Django 的 MTV 模式到 ORM 查询删除对象从 StreamingHttpResponse 的两个关键参数到 Vue 播放 m3u8再到 PyCharm 的环境配置和部署上线基本上是一条完整的实操链路。1. 整体设计与技术选型为什么是 Django Vue1.1 企业工作流系统的核心痛点企业工作流、工程流程控制系统听上去很高大上拆开来看其实就是三件事流程怎么走、数据怎么管、权限怎么控。我做的这套系统面向的是施工单位的工程报审场景包括了施工方案审批、材料进场报验、质量巡检整改、竣工验收申请等十几个流程类型每个流程还有若干个审批节点比如班组提交、项目经理审核、监理复核、甲方确认。这种系统最容易踩的坑有三个。第一是流程状态容易乱尤其是多人同时操作同一个审批单的时候状态被覆盖的情况非常常见。第二是文件处理麻烦工程领域的单据经常要挂很多附件有 PDF 图纸、有 Excel 材料清单、还有现场拍摄的视频这些文件的存储和在线预览如果设计得不好后期会非常痛苦。第三是权限模型复杂同一个用户在不同流程里可能是提交人、审批人、抄送人甚至是某个节点的代理审批人权限判断稍微写草率一点就会出现越权操作。所以我做这套系统的时候没有急着写代码而是先花了两天时间把流程建模和权限模型定下来。流程引擎这块我并没有用现成的第三方库比如 django-workflows 或者 viewflow而是自己写了一套轻量的状态机。原因也很简单企业的审批流程看上去通用实际上每个客户的规则都不一样现成框架的定制成本有时候比直接写一个还高。自己写状态机其实不复杂就是把流程实例的状态、当前节点、操作记录这三块表设计好然后每次审批动作都走同一个入口函数。1.2 为什么后端选 Django、前端选 Vue后端选择 Django主要看中它三个能力。第一个是 ORM 对复杂查询的支持确实省事工程流程系统里报表和台账类的查询特别多比如按时间段统计某类审批单的数量、按部门汇总待办事项Django 的 ORM 用 annotate 和 aggregate 写起来非常顺手两行代码能搞定的事情用原生 SQL 要写一大段。第二个是 Django Admin 自带的后台管理在项目初期价值巨大流程配置、用户权限初始化、测试数据录入全部可以在 Admin 后台里快速完成省掉了搭建管理后台的时间。第三个是 Django 的生态成熟文件上传、缓存、任务队列这些在企业应用中高频出现的需求都有现成的方案可以参考。前端选择 Vue说实话选的不是某个技术而是一种开发体验。Vue 的单文件组件结构、渐进式的上手曲线、以及中文社区的大量资料让团队里原本以 Python 为主的开发人员能快速参与前端开发。更重要的一点是 Vue 的路由和状态管理方案在前端工程化方面足够清爽处理工作流系统里那些“同一个页面不同状态”的场景非常合适。前后端分离是我一开始就定下的方案。Django 只提供 JSON API前端是独立的 Vue 工程通过 axios 调接口。这样做的好处是前后端可以并行开发我在写后端流程引擎的时候前端的同事已经在搭工作台页面了联调阶段再统一对接整体节奏快不少。1.3 MTV 模式在工作流项目里的实际应用说到 Django 就绕不开 MTV 模式M 是 Model、T 是 Template、V 是 View。很多人一上来就背概念但放在我这个项目里理解 MTV 最好的方式是看它解决了什么问题。Model 层负责把业务对象映射成数据表。工作流系统里最常见的 Model 是流程实例ProcessInstance和流程节点Task流程实例存的是某个报审单的完整信息流程节点存的是当前流转到了哪一步。我建了两张主表和三张开销类子表每张表都通过 ForeignKey 关联起来查询的时候用 select_related 和 prefetch_related 合理控制查询次数这套 Model 设计是整个系统的地基。View 层在传统 Django 里是负责把 Model 查出来再渲染到 Template但在前后端分离的项目里Template 层已经不需要 Django 管了。你可以理解成 Vue 工程替代了 MTV 里的 TemplateDjango 的 View 只负责接收前端的请求、调用 Model 层逻辑、返回 JSON 给前端渲染。所以在这个项目里MTV 变成了一个更贴合实际的“M V 前端模板”的组合各层各司其职职责边界反而更清晰了。Value 这个东西等真正联调起来你会发现把 Django 的 View 保持“瘦”非常重要。流行业务的权限验证、状态流转判断、附件大小校验这些逻辑我全部搬进了 Model 层的方法里面View 里只做参数校验和结果返回。这样做的好处是任何入口进来的请求都走同样的业务逻辑不管是 API 调用还是 Admin 后台触发的操作行为都是一致的不会出现“这里改了那里没改”的诡异问题。2. 后端必须写好的三个关键环节模型、ORM 查询与文件流下载2.1 创建 App 与模型设计流程节点表怎么建Django 项目一创建我做的第一件事就是拆分 App。代码上我用python manage.py startapp workflow创建了 workflow 这个 App专门放流程引擎相关的代码另外还有一个 accounts App 管用户和权限一个 files App 管附件和预览。流程相关的模型是核心我给大家看一个简化版的设计实际生产环境我还在这个基础上加了 UUID 主键、创建时间、更新时间这些公共字段class ProcessInstance(models.Model): 流程实例一张报审单就是一个实例 name models.CharField(max_length200, verbose_name单据名称) flow_type models.CharField(max_length50, verbose_name流程类型) status models.CharField(max_length20, defaultdraft, verbose_name当前状态) submitter models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, related_namesubmitted_instances, verbose_name提交人 ) current_node models.CharField(max_length50, blankTrue, verbose_name当前节点) version models.IntegerField(default1, verbose_name乐观锁版本号) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: db_table workflow_process_instance indexes [ models.Index(fields[flow_type, status]), models.Index(fields[submitter, created_at]), ] class NodeRecord(models.Model): 审批节点记录每一步操作都会在这里留下痕迹 instance models.ForeignKey( ProcessInstance, on_deletemodels.CASCADE, related_namenode_records, verbose_name所属流程实例 ) node_name models.CharField(max_length100, verbose_name节点名称) operator models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.PROTECT, verbose_name操作人 ) action models.CharField(max_length20, verbose_name动作) comment models.TextField(blankTrue, verbose_name审批意见) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table workflow_node_record ordering [created_at]这里边有个on_deletemodels.PROTECT的细节很多人习惯不管关联关系直接写 CASCADE但在审批系统里用户和审批记录绝对不能级联删除。如果哪天有人要从系统里清掉一个离职员工CASCADE 会把他审批过的所有记录一起删光整个流程台账就毁了。所以我在用户关联的审批记录上用了 PROTECT宁可在删除的时候报错也不能让数据静默丢失。version字段是我解决并发操作的关键后面有一章我会详细讲怎么用乐观锁防止多人同时审批导致的状态覆盖。2.2 查询、过滤与删除对象ORM 操作的正确姿势工程流程系统每天被点得最多的是“我的待办”和“我的已办”两个接口。待办列表的查询条件其实挺绕的首先要找出所有当前节点指向当前用户的实例其次要排除掉自己提交的实例因为自己不能审批自己提交的单子最后还要按紧急程度和提交时间排序。Django ORM 写这类查询非常舒服下面是我待办查询的核心代码逻辑基本和自然语言一样def get_pending_instances(user): today timezone.localdate() return ( ProcessInstance.objects .filter(current_nodepending_approval, statusapproving) .exclude(submitteruser) .annotate( wait_daysValue(today) - Func( F(updated_at), functionDATE, ) ) .order_by(-priority, updated_at) .select_related(submitter) .prefetch_related(node_records) ).select_related(submitter)和.prefetch_related(node_records)这两个必须写否则列表页渲染的时候会产生十几条甚至几十条重复 SQLDjango 的 ConnectionQueries 会告诉你差距有多大。一个页面 50 条待办不优化是 1 50 条查询优化完是 L 50 条查询里只有固定条数性能完全两码事。再来说说删除。Django 里单对象删除是obj.delete()批量删除是queryset.delete()但这里有个很容易被忽略的差异obj.delete()返回的是(总删除数, {模型名: 删除数})这样的元组queryset.delete()也是同样的返回结构。很多人第一次接触以为返回的是布尔值直接拿来做判断会出问题。我在这个项目里基本没做过真正的物理删除。审批系统的数据是有法务效力的单据只能作废不能删除。所以我在流程实例上统一加了is_active字段需要隐藏数据的时候执行的是伪删除def soft_delete_instance(self): self.is_active False self.save(update_fields[is_active, updated_at])所有查询默认加.filter(is_activeTrue)过滤条件。这样做的好处是流程历史不会断比如一张单据被误操作作废之后管理员还能查得到原始记录审计的时候不用翻日志。2.3 StreamingHttpResponse 的两个关键参数content_type 和 content-disposition工程流程系统里有一个需求特别常见在线下载施工方案附件包括 PDF、Excel、图片甚至视频。最开始我图省事直接用 Django 的 HttpResponse 把文件读进内存再返回小文件没问题但一旦遇到几百 MB 的现场视频服务器内存直接被吃满前端也迟迟等不到响应。后来改成 StreamingHttpResponse 流式返回性能问题迎刃而解。但这里有个坑就是很多教程只讲了怎么用 StreamingHttpResponse没有讲透content_type和content-disposition这两个参数到底该怎么传。先看一个完整的下载接口实现import os import mimetypes from django.http import StreamingHttpResponse from urllib.parse import quote def download_attachment(request, attachment_id): att Attachment.objects.get(pkattachment_id) def file_iterator(file_path, chunk_size8192): with open(file_path, rb) as f: while True: chunk f.read(chunk_size) if not chunk: break yield chunk file_path att.file.path file_name os.path.basename(att.file.name) content_type, _ mimetypes.guess_type(file_path) if content_type is None: content_type application/octet-stream response StreamingHttpResponse( file_iterator(file_path), content_typecontent_type, ) filename quote(file_name) response[Content-Disposition] ( fattachment; filename*UTF-8{filename} ) return response这里面讲究很多。content_type是告诉浏览器响应体的媒体类型常见的有application/pdf、application/vnd.openxmlformats-officedocument.spreadsheetml.sheet、video/mp4等等。它直接决定了浏览器收到文件之后是打开预览还是直接下载比如 PDF 配application/pdf会在浏览器标签页里直接展示如果你希望用户只能下载不能预览就把它改成application/octet-stream。至于content-disposition是 HTTP 响应头里的一个字段不通过参数传入而是直接对 response 对象赋值image/svgxml这类情况浏览器还会对下载的文件名做额外的安全检查。用attachment关键词告诉浏览器这个响应体应该被作为一个文件保存而不是在页面里打开。文件名这里一定要用filename*UTF-8这种格式因为如果文件名带中文直接在 Content-Disposition 里写中文极有可能乱码。urllib.parse.quote会把中文转成 URL 编码浏览器拿到之后再解码回正确文件名这是我在 Windows 和移动端都测试过最稳的方案。如果你用的是 Django 的 FileResponse很多逻辑它会帮你处理但文件前缀、文件名编码这些细节依然要自己留意。我实际项目里是这样做的如果附件允许预览就用inline的 Content-Disposition如果不允许就用attachment强制下载。3. 前端 Vue 实战路由、响应式与 m3u8 播放3.1 Vue 路由如何承载工作流的不同节点状态后端把状态流转的逻辑处理完了前端最核心的工作其实是“状态的可视化”。同一个流程实例不同的人打开看到的操作按钮完全不一样。提交人看到的是撤回和催办审批人看到的是同意和驳回抄送人只能只看不能点。Vue Router 在我这项目里承担了页面级别的路由跳转但流程状态之间的切换更多依赖的是组件内部的状态判断。路由上我主要设计了四个页面待办列表页、已办列表页、流程详情页、流程发起页。详情页是整个系统最复杂的页面因为它要根据后端返回的status和current_node动态渲染不同的操作按钮和审批流程时间线。这里有一个经验可以分享路由参数的传递方式不要混着用。Vue Router 有两种传参方式一种是通过name params传参比如this.$router.push({ name: flow-detail, params: { id: workflowId } })另一种是通过path query传参比如this.$router.push({ path: /flow/detail, query: { id: workflowId } })。区别在于params方式刷页面后参数会丢失query方式参数会保留在 URL 上但容易被用户手动篡改。工作流详情页这种需要刷新后保持定位的页面我统一用query方式那种一次性的跳转参数比如表单预填数据才用params。如果整站统一用query带参路由配置里还应该写上props: true这样组件可以直接声明式接收参数测试的时候也方便单独 mock。3.2 从 Vue 源码到手动实现响应式Proxy 的本质工作流系统里最常见的交互是“表单状态驱动按钮状态”比如申请单填完必填项才能提交审批意见填了才能点同意按钮。这背后其实就是 Vue 的响应式系统在起作用数据改了页面自动更新。之前有一段时间我在分析 Vue 的响应式原理为了搞懂它还自己用原生 Proxy 手写过一个包含reactive、ref、effect、computed的最小实现。核心代码非常简洁原理也很直接function reactive(target) { return new Proxy(target, { get(obj, key) { track(obj, key) return Reflect.get(obj, key) }, set(obj, key, value) { const result Reflect.set(obj, key, value) trigger(obj, key) return result } }) } function ref(initialValue) { return reactive({ value: initialValue }) } function computed(getter) { let cached let dirty true const runner effect(getter, { lazy: true, scheduler() { dirty true } }) return { get value() { if (dirty) { cached runner() dirty false } return cached } } }effect负责记录当前依赖的 key 和被依赖的副作用函数之间的映射关系数据变化时再重新执行。computed是在effect之上加了一层缓存控制依赖没变就用缓存值变了才重新计算。搞懂这套逻辑之后再看 Vue 源码里的ref为什么偏偏要把值包在.value里就完全明白了因为原始类型没办法被 Proxy 代理只有对象才有捕获 get 和 set 的能力。这也是为什么业务代码里每次都要写formData.value newData而不是直接赋值写多了你反而会感谢这套规定因为它让响应式边界变得清晰了。3.3 Vue 播放 m3u8从拿到地址到画面出现工程流程系统里的视频巡检模块需要在线播放施工现场上传的监控视频。录像文件普遍采用 HLS 协议也就是.m3u8索引文件加一堆.ts切片文件的组合。问题在于浏览器默认不支持 m3u8 格式Safari 是个例外可以原生播放Chrome 和 Edge 必须要引入 hls.js 才能解码。一开始试过直接用video标签硬播 m3u8结果要么黑屏要么只有声音没有画面。后来老老实实上了 hls.js用法其实很简单template video refvideo controls autoplay muted/video /template script setup import Hls from hls.js import { onMounted, ref } from vue const video ref(null) const src https://your-domain/media/videos/check-record.m3u8 onMounted(() { const videoEl video.value if (Hls.isSupported()) { const hls new Hls() hls.loadSource(src) hls.attachMedia(videoEl) hls.on(Hls.Events.MANIFEST_PARSED, () { videoEl.play().catch(() {}) }) } else if (videoEl.canPlayType(application/vnd.apple.mpegurl)) { videoEl.src src } }) /script这里最容易翻车的不是前端代码而是后端配置。hls.js 播放的时候会向服务器发起很多 Range 请求来分段拉取.ts切片如果 Django 后端没有正确处理 Range 请求头视频就会出现“能加载但拖不动进度条”的现象。解决方式是后端设置好响应头支持 Range 分段请求from django.http import FileResponse def stream_video(request, video_path): file_path os.path.join(settings.MEDIA_ROOT, video_path) response FileResponse(open(file_path, rb)) response[Accept-Ranges] bytes return response另外跨域问题也要注意如果 m3u8 在前端域名下后端在另一个域名Django 必须配置 CORS 才能让视频正常请求。我的做法是生产环境用 nginx 把视频文件路径直接代理到前端的同域名下彻底绕开 CORS这样简单又稳定。4. PyCharm 环境配置与调试技巧4.1 从安装到环境配置一个干净的 Django 开发环境这个项目是在 PyCharm 里开发的团队里有几位同事是刚接触 Python 的新人光是环境配置就问了无数问题所以这里单独说一下。第一步是 Python 的安装去官网下载对应系统版本安装的时候勾选 Add Python to PATH这一项经常被漏掉漏掉之后命令行里输入 python 没有任何反应后面基本没法玩。PyCharm 建议直接装专业版Django 支持、数据库工具、远程调试都在专业版里社区版搞 Django 也能写但没有模板补全和调试配置的图形化入口效率会低不少。正式团队开发专业版功能确实值得教育验证或者公司采购都有正规的授权途径不要碰破解版。项目环境我统一用虚拟环境在 PyCharm 新建项目的时候选择 VirtualenvPython Interpreter 会自动指向 venv 目录下的 python.exe。这样做的核心价值是依赖隔离每个项目的第三方库互不影响。有些同事直接用全局 Python 装了一堆包后面升级版本的时候才发现所有项目都崩了那就是环境没隔离的经典事故。Django 项目配好之后还要在 PyCharm 里配置 Run Configuration。机器上用了 develop 环境。在项目的根目录右键配置好 Python Interpreter脚本路径指向 manage.py参数填runserver 0.0.0.0:8000这样点击运行按钮就能直接启动服务。4.2 常见环境坑Microsoft Visual C 14.0 is required用 PyCharm 给项目装依赖的时候最容易遇到的一个报错是执行安装时提示error: Microsoft Visual C 14.0 is required. Get it with Microsoft Visual C Build Tools。这个报错出现的原因和 Python 本身没关系而是因为很多包含 C 扩展的第三方包在 Windows 上没有预编译的 wheel 文件pip 只能现场下载源码编译编源码就要用到 VC 编译工具链。我统计过最容易触发这个报错的是pandas、lxml、pycryptodome、psycopg2这几个包其中lxml和psycopg2在 HTTP 解析和数据库连接场景里非常常见。解决办法有两个层面。第一个办法是优先安装预编译版本在 PyCharm 的 Terminal 里执行安装的时候带上--only-binary :all:参数强制要求 pip 只安装预编译的 wheel 包这样就不会触发源码编译流程。psycopg2 的话直接装 psycopg2-binary 这个包绝大多数场景够用。第二个办法是安装 Visual C Build Tools但这玩意安装时间很长体积也大我不建议为了一个包专门去装。如果项目必须用源码编译安装可以先去查看对应包的文档找预编译版下载地址能用 wheel 就用 wheel。4.3 断点调试工作流状态机的技巧PyCharm 的调试器是我排查工作流逻辑问题最依赖的工具。状态机这种代码最难查的是“状态在某一跳转里被写错”。我举一个真实发生的 bug审批流程从“项目经理审核”节点流到“监理复核”节点时前端页面显示已经到监理了但数据库里的 current_node 字段还是项目经理。这个问题用 PyCharm 断点调试非常好定位。做法是在流转函数transition()里打上断点然后启动调试模式用一个测试账号模拟提交审批逐步追踪next_node变量的赋值过程。最后发现是代码里把instance.current_node的赋值语句写到了save()之后导致保存的还是旧值。这种错误靠代码审查不容易发现但断点一打变量值变化清晰可见。PyCharm 的 Evaluate Expression 窗口在调试状态机时也特别好用可以在断点暂停处直接执行instance.status、record.action之类的表达式快速确认当前实例的真实状态。配合 F8 单步跳过、F7 步入函数整个流转逻辑可以像放电影一样在脑子里过一遍排查效率极高。5. Windows Server 上部署 Djangowaitress nginx5.1 为什么选 waitress 而不是默认 runserver开发阶段用python manage.py runserver完全可以服务会自动重载代码改完保存就能生效非常方便。但生产环境如果还这么干就等着被吐槽吧runserver 是 Django 的开发服务器性能和并发能力都不太行也不建议暴露到公网。在 Windows Server 上部署 Django我推荐 waitress它是一个纯 Python 实现的 WSGI 服务器不需要编译 C 扩展安装只有一个 pip 命令的事在 Windows 环境下的表现比较稳定。之前我也试过 gunicorn但它在 Windows 上支持有问题官方文档明确说了建议只在 Unix 上使用这就是我选 waitress 最直接的原因。安装和启动方式非常简单pip install waitress waitress-serve --listen127.0.0.1:8000 myproject.wsgi:applicationmyproject.wsgi:application是项目 wsgi.py 文件里暴露的 application 对象让 waitress 加载整个 Django 应用。启动之后在浏览器访问http://127.0.0.1:8000能看到系统说明 waitress 已经接管了 Django 服务。为了让 waitress 在后台稳定运行我还把它做成了一个 Windows 服务用的是 NSSM 这个小工具配置好启动命令和日志路径之后服务器重启 waitress 也会自动启动省掉了很多运维精力。5.2 Django 静态文件与媒体文件的处理部署阶段有个问题特别容易卡住新手前端 Vue 构建出来的 dist 目录、Django 上传的媒体文件、Django 自带的 admin 静态资源这三类文件在开发环境各归各的一到生产环境就乱了。Django 自身的静态资源处理不复杂先在 settings.py 里配置STATIC_URL /static/ STATIC_ROOT os.path.join(BASE_DIR, staticfiles) MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)然后在服务器上执行python manage.py collectstatic这个命令会把所有 App 里的静态资源复制到STATIC_ROOT指定的目录下方便 nginx 直接分配。Vue 前端构建产物的处理路径要单独说。我在本地把前端工程构建好产物是 dist 目录整个目录放到服务器上nginx 用它直接充当静态文件服务器不需要 Django 参与。这样分工最清晰nginx 负责 HTML、JS、CSS、图片的静态访问waitress 负责所有 API 请求nginx 再把/api/开头的请求反向代理给 waitress。5.3 nginx 配置统一入口与前端路由回退nginx 的配置是整个部署环节里最值得花时间的部分配好了系统运行流畅配不好各种 404、跨域、白屏问题全来了。下面是我这套系统实际使用的 nginx 配置核心内容都有中文注释可以直接拿走改改用自己的。server { listen 80; server_name your-domain.com; # 前端 Vue 构建产物 root /www/vue-dist; index index.html; # 前端路由回退避免刷新子路由页面 404 location / { try_files $uri $uri/ /index.html; } # 静态资源缓存策略 location /assets/ { expires 7d; add_header Cache-Control public; } # Django 媒体文件直接走 nginx不经过 waitress location /media/ { alias /www/django-project/media/; } # Django admin 静态资源 location /static/ { alias /www/django-project/staticfiles/; } # API 和 admin 后端请求反向代理到 waitress 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; proxy_set_header X-Forwarded-Proto $scheme; # 视频文件播放必须保留这个请求头 proxy_request_buffering off; } }try_files $uri $uri/ /index.html这一行是前端路由刷新不白屏的关键。Vue 是单页应用路由切到/flow/detail?id123时服务器上根本没有这个物理文件如果不加这行nginx 会直接返回 404加了这行所有不存在的路径都会回退到 index.html交给 Vue 自己来判断路由。proxy_request_buffering off这一行是我踩过坑之后加上的处理视频流和文件上传请求时如果 nginx 默认开了请求缓冲大文件上传容易卡死关掉之后直接把请求透传给后端实测对超大附件的上传稳定很多。这样配置完之后整个系统的访问路径是统一的用户只访问 80 端口前端资源拉取和 API 请求都在同一个域名下从源头规避了跨域问题也省去了配置 CORS 的麻烦。6. 常见问题与排查技巧实录6.1 流程状态不同步并发审批导致的状态覆盖这个 bug 是我在系统上线试运行的时候发现的两个审批人同时打开同一个单子一个人点了同意另一个人随后点了驳回按理说后操作的应该提示“该单已审批”但实际结果是第二个操作把第一个操作的结果覆盖了流程直接跳到驳回分支状态数据全乱。本质原因是典型的“读改写”并发冲突两个请求进来的时候都读到了旧状态然后各自修改后写入数据库后写入的把先写入的覆盖了。解决方案是乐观锁在流程实例表里加一个 version 字段更新之前先校验版本号def approve(instance_id, user_id, comment): instance ProcessInstance.objects.select_for_update().get(pkinstance_id) updated ProcessInstance.objects.filter( pkinstance_id, versioninstance.version ).update( statusapproved, versioninstance.version 1, current_nodefinished ) if not updated: raise ConflictError(该单据已被其他用户处理请刷新页面)这里select_for_update()在数据库层面锁住了这一行防止两个事务同时读到同一版本。如果更新失败说明有其他请求已经改过了直接返回冲突提示。上线之后这个并发问题就再也没出现过。6.2 附件下载文件名乱码与响应头丢失附件下载的乱码问题有一部分是后端 Content-Disposition 没处理好另一部分其实出在前端。axios 默认不会把响应体当二进制处理直接下载会得到一堆乱码的文本文件。正确做法是在请求头里加上响应类型声明axios.get(/api/attachments/download, { params: { id: 123 }, responseType: blob }).then(response { const url window.URL.createObjectURL(new Blob([response.data])) const link document.createElement(a) link.href url link.download 施工方案.pdf link.click() window.URL.revokeObjectURL(url) })写着容易实际这个方案踩了一个坑如果下载接口出错比如文件不存在后端会返回一个 JSON 错误信息但是前端因为写了responseType: blob拿到的 response.data 是一个 Blob 对象里面包着一串 JSON 文本导致错误提示变成乱码。后来我是这样处理的后端把错误响应也统一用 JSON 返回并在响应头里加一个X-Error-Code: file_not_found前端先读取这个响应头非 200 状态就直接从 Blob 里解析 JSON 文本拿出来用。6.3 部署后页面白屏和接口访问失败第一次用 nginx 部署完 Vue 项目打开页面是一片纯白控制台报了一堆资源 404。排查下来是 Vue 构建资源路径的问题。默认情况下 Vue 会把静态资源路径写成绝对路径比如/js/app.js如果 nginx 的 root 配得不对或者前端资源没有放在域名根路径下资源就会加载失败。解决办法是在 vue.config.js 里把 publicPath 改成相对路径或者精确匹配 nginx 的部署路径module.exports { publicPath: ./, outputDir: dist, productionSourceMap: false }publicPath: ./会让构建出来的 HTML 里资源路径变成相对路径这样不管整个 dist 目录挂到哪个子路径下都能正常工作。不过要注意的是如果同时用了 vue-router 的 history 模式相对路径和路由路径组合起来可能会有问题这种场景下还是建议用绝对路径把 nginx root 配置检查清楚。接口访问失败的问题大多数是反向代理配置不对。检查的时候先看 waitress 单独访问是否正常再用 curl 测试 nginx 代理后的接口返回逐层定位。大部分问题的根源都是proxy_pass后面的端口和服务地址填错了或者后端没有监听到 nginx 所连接的那个 IP。这套工程流程系统从设计到上线整个过程其实就是一个“把复杂业务拆成简单模块”的练习。用户在界面上看到的是几个简单的按钮操作背后是流程状态机、权限模型、文件存储、前端路由、服务器部署这一整条链路在支撑。Django 负责稳定地处理数据和业务逻辑Vue 负责把流程状态和交互体验做顺畅PyCharm 把开发调试的效率拉满最后 waitress nginx 组合让系统老老实实地跑在 Windows 服务器上。对于一个做工程领域软件开发的人来说这套组合最大的价值就是“可掌控”。Django 的生态足够成熟Vue 的上手曲线足够平缓PyCharm 的调试能力足够专业把这几样工具用好企业级的流程系统完全可以靠一个小团队保质保量地交付。希望这篇文章里的代码和踩坑经历能帮你省下一点在深夜里查 bug 的时间。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

ESP32+MAX30102+OLED:DIY心率血氧监测仪全攻略 2026/9/20 3:08:46

ESP32+MAX30102+OLED:DIY心率血氧监测仪全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Flutter跨端开发OpenHarmony实战:图书搜索应用全程解析 2026/9/20 3:08:46

Flutter跨端开发OpenHarmony实战:图书搜索应用全程解析

这两年鸿蒙生态起来之后,很多做跨端开发的朋友都在观望一件事:Flutter 到底能不能在 OpenHarmony 上跑得稳。我自己的答案是能,但过程里有不少弯弯绕绕。这篇文章就拿我最近做的一个教育类小应用——图书搜索来当例子,把从环境配置…

阅读更多 →
Ray 内存管理完全指南:ObjectRef 引用计数、ray memory 调试与内存感知调度 2026/9/20 3:08:46

Ray 内存管理完全指南:ObjectRef 引用计数、ray memory 调试与内存感知调度

人工智能分布式训练强化学习任务调度模型推理服务 【免费下载链接】ray Ray is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads. 项目地址: https://gitcode.com/gh_mirrors/ra/ray 点…

阅读更多 →
DeviceNet从站转SPI调试全攻略:从时序验证到故障排查 2026/9/20 3:08:46

DeviceNet从站转SPI调试全攻略:从时序验证到故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
first-contributions 实战指南:从 Fork 到 Pull Request 完成你的第一次开源贡献 2026/9/20 3:08:46

first-contributions 实战指南:从 Fork 到 Pull Request 完成你的第一次开源贡献

first-contributions 实战指南:从 Fork 到 Pull Request 完成你的第一次开源贡献 【免费下载链接】first-contributions 🚀✨ Help beginners to contribute to open source projects 项目地址: https://gitcode.com/gh_mirrors/fi/first-contribution…

阅读更多 →
Meteor 私有资源目录 `private/` 与 `Assets` API 完全指南 2026/9/20 3:05:45

Meteor 私有资源目录 `private/` 与 `Assets` API 完全指南

后端前端开发工具移动开发 【免费下载链接】meteor Meteor, the JavaScript App Platform 项目地址: https://gitcode.com/gh_mirrors/me/meteor 点击查看 免费下载 private/ 是 Meteor 应用根目录下的一个特殊顶层目录:其中的所有文件只对服务端代码可…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞