
做船舶相关的信息管理系统这几年需求一直挺稳定。无论是港务集团、船代公司还是做船舶租赁、船员派遣的团队基本都逃不开一套能管船舶档案、船员信息、航行记录、证书到期提醒的系统。市面上买成品不便宜定制又贵所以很多团队会选择用 Python 加 Vue 自己搞一套。这个组合里Django 和 Flask 是后端主力PyCharm 是日常开发环境整套技术栈成熟、招人容易、上手也不算陡是中小型团队做内部系统很务实的路线。这篇文章我就拿“船舶信息管理系统”这个具体项目把从技术选型、环境搭建、核心功能实现到部署上线的完整过程拆开讲一遍。文章里会重点聊 Django 和 Flask 怎么选、ORM 操作里那些容易翻车的细节、Vue 前端跟后端联调时的跨域问题以及我在实际项目中踩过的一些坑。打算用这套技术栈做船舶管理系统、设备管理系统、物资管理系统这类企业内部系统的朋友应该能从里面拿走不少可以直接用的东西。1. 项目梳理与技术选型思考1.1 船舶信息管理系统到底在管什么很多人一听“船舶信息管理系统”下意识觉得这就是一个简单的信息登记软件把船名、船舶类型、吨位录进去就完事了。真正接触过这个业务就会发现船舶管理远比想象中琐碎。一条船从买进来或者租进来开始就有大量的生命周期数据需要维护。基础信息包括船名、船籍港、船舶类型、总吨位、净吨位、建造日期、船体材料、主机功率、船长船宽这些静态档案。这些数据不是填一次就完事的船检证书、国籍证书、所有权证书都有有效期年检到期、保险到期、船员证书到期全靠系统提前提醒不然漏了任何一项船就可能面临停航或者罚款。除了静态档案还有动态数据。船舶每次航次的信息包括出发港、目的港、开航时间、抵港时间、货物种类和数量、吃水深度轮机日志里记录的油耗、转速、转速温度维修保养记录里的维修项目、维修单位、费用、下次保养里程或时间。这些数据如果只靠 Excel 表格管理不同的人维护不同的表格数据格式不统一查一条船的历史维修记录可能要翻好几个文件效率极低。所以这个系统的核心价值是把分散在 Excel、纸质台账、微信聊天记录里的船舶数据统一收口形成一条完整的船舶档案链路。我做的这套系统在功能模块上主要分这么几块船舶档案管理基础信息增删改查、证书管理、到期提醒船员信息管理船员基础信息、持证情况、上船记录、证书到期航次与航行记录航次信息登记、航行日志查询维修保养管理保养计划、维修工单、费用统计系统管理用户登录、权限控制、操作日志这些模块里最容易被低估的是证书到期提醒。业务方一开始提需求的时候经常不提这个等到实际用了才发现这是他们最刚需的功能。所以做系统设计的时候不要业务方说什么就只做什么要把证书、保险、年检这类带时间属性的数据单独拎出来设计一张提醒表提前 30 天、7 天、3 天分别做提醒这是这类系统真正的口碑点。1.2 为什么是 Python 加 Vue 这套组合先聊后端。船舶信息管理系统这类企业内部系统业务逻辑复杂度中等并发量不高可能同时操作的就几十个人但是数据关系比较复杂表多、字段多、各种状态流转多。这种场景下Python 的开发效率优势非常明显尤其是配合 Django 这种自带全套工具的框架。有人可能会问Java 在这个领域不是更主流吗确实很多老牌的港航信息化项目用的是 Java 系的技术栈但那是历史原因。现在从零起步做新系统选 Python 至少有三个实实在在的好处。第一是开发速度快这一点在业务方反复改需求的时候体会特别深改一个字段、加一张表Python 这边可能半小时就搞定了Java 那边光改实体类、改 Mapper、改 XML 就够折腾一阵。第二是招人容易Python 的开发者基数大尤其是能写 Web 的新人一抓一大把团队临时缺人手也容易补。第三是后期做数据分析方便船舶的油耗数据、航行轨迹数据、费用数据沉淀下来之后可以直接用 Python 的 pandas、matplotlib 做分析报表不用再跨技术栈对接。前端选 Vue 是顺理成章的事。Vue 在国内的社区活跃度最高中文资料全遇到问题搜一下基本都有答案。对于这种以表格、表单、列表为主的管理系统前端Vue 的响应式数据绑定和组件化开发模式非常合适。页面上的船舶列表、证书列表、航次记录全部可以拆成独立的组件表格组件、表单组件、弹窗组件各干各的后期维护起来脑子不用同时装下所有页面的逻辑。而且 Vue 生态里有现成的 UI 组件库Element Plus 也好、Ant Design Vue 也好拿来就能用。管理系统的页面长得都差不多左侧菜单、顶部栏、中间内容区组件库把这些基础件都做好了开发人员主要精力可以放在业务逻辑上不用从零手写一个表格的分页、排序、筛选功能。1.3 Django 和 Flask 怎么选顺带聊聊 FastAPI这是一个每次技术选型都会被翻出来讨论的问题。很多人纠结的原因是两个框架都能做项目不大好像用哪个都行。但实际上 Django 和 Flask 的设计哲学完全是两个方向选择的关键不在于框架本身谁好谁坏而在于你的项目形态和团队习惯。Django 是“全家桶”思路自带 ORM、Admin 后台、表单处理、认证系统、模板引擎。它的核心理念是“开箱即用”你创建一个项目默认就有一套完整的管理后台数据库迁移工具也是内置的连数据库表都不用手写 SQL定义好模型之后一条命令就自动建表。这种设计非常适合数据模型多、关系复杂、后端管理需求重的项目。船舶信息管理系统就是典型的这种项目船舶、船员、航次、维修、证书这些实体之间全是外键关系用 Django 的 ORM 表达起来非常顺手。Flask 是“微框架”思路核心只保留路由和视图其他一切都可以通过扩展来加。ORM 你可以选 SQLAlchemy认证你可以用 Flask-Login表单可以用 Flask-WTF总之什么都需要自己拼。好处是灵活想用什么装什么项目结构完全可以自己掌控坏处是项目稍微大一点选型和集成的成本就开始显现而且团队每个成员对扩展的理解不一致的话代码风格容易乱。再顺带说说 FastAPI最近几年热度很高性能比 Flask 好自带 Swagger 文档基于 Pydantic 做数据校验也很舒服。但我要说句实在话对于船舶管理系统这种内部业务系统FastAPI 的性能优势基本发挥不出来你不会有那么高的并发反而它的异步生态和 Pydantic 的数据校验模型在团队不够熟练的情况下会增加不必要的复杂度。FastAPI 更适合的是对外提供 API 服务、前后端彻底分离并且要求高并发吞吐的项目。如果是做企业内部管理系统Django 依然是我首推的方案。这套系统我自己最终选的是 Django 做主体后端核心原因就是数据模型复杂需要 Admin 后台快速支撑运营人员的数据录入需求。但我在项目里也保留了一个 Flask 写的小服务用来处理文件上传转换这种独立的小功能这样主服务不被文件处理任务拖累也算各取所长。2. 从零搭建开发环境2.1 Python 与 PyCharm 的安装配置工欲善其事必先利其器。很多新手在环境这一步就开始劝退所以我把每一步都拆细一点说。Python 版本选择上我建议用 3.8 到 3.10 之间的版本。太老的 3.6、3.7 对 Django 新版本支持不好太新的版本有时候第三方库还没跟上容易遇到编译报错。我自己用的是 Python 3.9稳了很久。到 Python 官网下载对应操作系统的安装包装的时候注意勾选 Add Python to PATH这一步漏了后面在命令行里敲 python 会提示找不到命令很多新手在这卡了半天不知道自己漏的就是这个勾选框。装完之后建议立刻把 pip 源换成国内镜像。默认的官方源在国内下载速度很慢装一个大点的库等几分钟是常事。我换的是清华大学的 PyPI 镜像在命令行执行pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple然后创建项目的虚拟环境。PyCharm 里新建项目的界面可以直接选虚拟环境工具一般选 Virtualenv 就行Python 解释器选刚才装好的那个。虚拟环境的作用是给每个项目隔离出独立的第三方依赖库A 项目用的 Django 3.2B 项目用的 Django 4.0两个互不干扰这在同时开发多个项目的时候能避免大量莫名其妙的依赖冲突问题。PyCharm 本身就是为 Python 开发而生的Django 项目在 PyCharm 里有专门的工程类型支持可以一键创建 Django 项目自动生成 manage.py、settings.py 这些文件。装 PyCharm 的时候我建议直接装 Professional 版社区版虽然免费但对 Web 开发的支持少很多Django 的调试工具、JavaScript 的支持都不是很全。版本选 2022 之后的都可以界面变化不大功能上能满足绝大部分场景。如果觉得 PyCharm 启动慢、内存占用大也可以在配置文件里调整 JVM 内存参数给它分配 2GB 到 4GB 的内存运行体验会流畅不少。2.2 Vue 前端环境准备Vue 前端环境的搭建核心是 Node.js 和包管理工具。Node.js 安装没有什么特别的门槛去官网下 LTS 版本就行。LTS 的意思是长期维护版稳定性有保障不需要追新。装好之后命令行执行 node -v 验证一下能输出版本号就说明成功了。npm 是 Node.js 自带的包管理器装第三方库全靠它国内同样建议配置镜像源npm config set registry https://registry.npmmirror.com这里有个细节我要多说一句。很多人在配置 Vue 环境的时候卡在版本兼容上尤其是 node-sass 这类需要编译原生模块的库Node 版本一变就编译失败。现在我一般不建议新建项目直接手搓 webpack 配置直接用 Vite 创建 Vue 3 项目就好启动速度快依赖也少。Vite 对 Node 版本有要求建议 Node 14.18 以上所以装 LTS 版就没问题。创建 Vue 项目的命令npm create vitelatest ship-frontend -- --template vue选 vue 模板创建一个新项目然后进入项目目录执行 npm install 装依赖。项目骨架里会自动生成 src 目录后续组件和路由文件就在这里维护。开发过程中最常用的命令是 npm run dev启动本地开发服务器默认端口一般是 5173。这个命令会监听源码变化改完代码页面自动刷新调试效率比传统手动刷新高很多。顺带提一下PyCharm 自带的前端开发支持对 Vue 项目也够用直接在 PyCharm 里打开前端项目终端窗口、代码高亮、格式化工具都有。不需要再单独装 VS Code一个 IDE 搞定前后端切换起来也方便。2.3 后端项目骨架搭建Django 项目的创建过程我已经做过很多遍了步骤可以精确到每一步。先在 PyCharm 的终端里进入打算存放项目的目录然后创建一个 Django 项目django-admin startproject ship_management这个命令会在当前目录下生成一个 ship_management 目录里面有 manage.py 和同名的配置包。manage.py 是项目后续所有操作的总入口启动服务、创建应用、执行数据库迁移全都通过它来调用。配置文件里最常用的是 settings.py项目的调试模式、数据库配置、应用注册、模板路径、静态文件路径都在这里设置。创建完项目之后还需要创建应用。Django 里一个项目可以包含多个应用比如船舶管理系统的船务管理可以是一个 app船员管理是一个 app系统管理是一个 app。这个设计可以把不同功能的代码拆分到不同的模块里避免所有代码堆在一堆非常难维护。执行命令创建第一个应用python manage.py startapp ship这个命令会生成一个新的 ship 目录里面有 models.py、views.py、admin.py 等文件。新创建的应用不会自动被项目加载需要手动到 settings.py 的 INSTALLED_APPS 列表里加上 ship不然后面做数据库迁移的时候 Django 根本不知道有这个应用的存在会直接跳过它的模型。初始配置里还有一个安全相关的点要处理。Django 默认的 settings.py 里 SECRET_KEY 是创建项目时自动生成的调试阶段可以直接用但如果要部署到服务器这个密钥千万不能泄露而且最好从环境变量里读取不要把真实密钥写死在代码仓库里。DEBUG 参数调试阶段保持 True部署时必须改成 False否则出错的时候会直接把完整的堆栈信息泄露给访问者等于给攻击者递刀子。数据库方面本地开发阶段用 SQLite 就够了零配置Django 建表、读写数据都顺畅。等要部署到服务器再把数据库切换到 MySQL 或者 PostgreSQL改一下 settings.py 里的 DATABASES 配置就好。这里别一上来就在本地装 MySQL很多新手就这么干的折腾半天下载安装、改密码、配权限结果发现本地开发阶段根本用不上。3. 核心功能落地模型、接口与页面3.1 数据模型设计与 ORM 操作细节船舶信息管理系统的数据模型是整个项目的地基地基打得稳后面所有功能都顺地基打歪了后面写接口、写页面处处难受。我把数据模型分成几个核心的模型类来讲。首先是船舶基础信息模型这是整个系统的中心表可以叫 Ship。它的字段包括船舶名称、船舶类型、船籍港、总吨位、净吨位、建造日期、船体材料、主机功率、船长、船宽以及备注信息。在 Django 里用模型类定义class Ship(models.Model): name models.CharField(船名, max_length100) ship_type models.CharField(船舶类型, max_length50) port_of_registry models.CharField(船籍港, max_length50) gross_tonnage models.FloatField(总吨位) net_tonnage models.FloatField(净吨位) build_date models.DateField(建造日期) hull_material models.CharField(船体材料, max_length50) main_engine_power models.FloatField(主机功率) length models.FloatField(船长) width models.FloatField(船宽) remark models.TextField(备注, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: db_table ship_info verbose_name 船舶信息 verbose_name_plural verbose_name def __str__(self): return self.name这里有几个细节值得强调。第一数据库表名可以通过 Meta 类里的 db_table 指定不指定的话 Django 默认用应用名_模型名的方式自动生成比如 ship_ship这样看起来不够直观指定成 ship_info 更容易维护。第二CharField 的 max_length 参数是必填的不能省略这个长度会直接映射成数据库里 VARCHAR 的长度。第三auto_now_add 跟 auto_now 的区别要搞清楚前者只在创建记录时自动写入当前时间后者每次保存记录都会更新成当前时间用于记录更新时间非常合适。船舶证书信息是另一个关键模型。证书类型包含所有权证书、国籍证书、船检证书、最低安全配员证书等每个证书记录对应的船舶外键、证书编号、签发日期、有效期还有关联的证书文件附件class ShipCertificate(models.Model): ship models.ForeignKey(Ship, on_deletemodels.CASCADE, verbose_name所属船舶, related_namecertificates) cert_type models.CharField(证书类型, max_length50) cert_number models.CharField(证书编号, max_length100) issue_date models.DateField(签发日期) expire_date models.DateField(有效期至) file models.FileField(证书文件, upload_tocertificates/%Y/%m/, blankTrue, nullTrue) class Meta: db_table ship_certificate证书模型里那个 related_name 参数很重要它决定了从 Ship 对象反向查询证书集合时的属性名。设置成 certificates 之后拿到一条船的数据直接 ship.certificates.all() 就能拿到这条船全部证书不需要再单独写一条按 ship_id 过滤的查询。不设置的话 Django 默认用模型名小写加 _set即 ship_certificate_set用起来多敲几个字母倒是小事关键是代码可读性差。再聊一个很容易踩坑的操作——删除对象。热搜词里不是有一个“django执行查询-删除对象”嘛这个确实是新手高频问题。Django 删除对象有两种方式一种是拿到对象实例后调 obj.delete()一种是按查询条件批量删除 Model.objects.filter(...).delete()。批量删除的时候要注意Django 的 delete 是级联的如果这个对象有外键指向它别的记录而且外键设置的是 on_deletemodels.CASCADE那关联的记录会一并删除。比如证书关联了船舶你删掉一条船舶记录它的所有证书记录也会跟着消失。如果这是业务上不允许的删船的时候想保留历史证书数据那就得在设计外键时把 on_delete 改成 models.SET_NULL并且把外键字段设为 nullTrue。这个决策一定要在模型设计阶段就跟业务方确认清楚不然上线后误删数据就是事故级别的。再补充一个 ORM 查询的实用技巧。做船舶列表页的搜索功能经常需要按多种条件过滤。Django ORM 的链式查询可以优雅地处理ships Ship.objects.all() if keyword: ships ships.filter(name__icontainskeyword) if ship_type: ships ships.filter(ship_typeship_type) if min_tonnage: ships ships.filter(gross_tonnage__gtemin_tonnage) ships ships.order_by(-created_at)filter 后面可以接双下划线加查询条件icontains 是模糊匹配且不区分大小写gte 是大于等于。先用一个基础查询集然后根据搜索参数逐个叠加条件最后统一排序这种方式写出来逻辑清晰也方便后续增加新的筛选条件。3.2 接口层开发Django REST framework 实战Django 自带的视图函数可以直接返回 HTML 模板但做前后端分离项目的时候需要的是返回 JSON 数据接口。业界最常用的方案是 Django REST framework也就是常说的 DRF。它把序列化、分页、权限、限流这些高频需求都封装好了开发接口的效率比手写 JSONResponse 高一个量级。先安装并注册pip install djangorestframework然后在 settings.py 的 INSTALLED_APPS 里加上 rest_framework。接着写序列化器。序列化器的作用是把 Django 模型实例转换成 JSON 格式的数据返回给前端同时也能在前端传参的时候做校验和反序列化。以船舶信息为例from rest_framework import serializers from .models import Ship class ShipSerializer(serializers.ModelSerializer): cert_count serializers.SerializerMethodField() class Meta: model Ship fields [id, name, ship_type, port_of_registry, gross_tonnage, net_tonnage, build_date, hull_material, main_engine_power, length, width, remark, created_at, updated_at, cert_count] read_only_fields [id, created_at, updated_at] def get_cert_count(self, obj): return obj.certificates.count()SerializerMethodField 是一个非常好用的东西它允许在序列化结果里增加这个模型里没有的字段值。上面这个例子在返回船舶信息的同时把这条船关联的证书数量一起算出来前端展示船舶列表的时候可以直接看到一条船名下有几本证书不用再单独调一次接口查数量。前端少一次请求页面响应就快一些后端也少一分压力。视图部分用 DRF 的 ModelViewSet它把列表、详情、新增、更新、删除五个接口都封装好了只需要指定查询集和序列化器from rest_framework import viewsets from .models import Ship from .serializers import ShipSerializer class ShipViewSet(viewsets.ModelViewSet): queryset Ship.objects.all().order_by(-created_at) serializer_class ShipSerializer pagination_class StandardResultsSetPagination路由配置from rest_framework.routers import DefaultRouter from .views import ShipViewSet router DefaultRouter() router.register(rships, ShipViewSet, basenameship) urlpatterns router.urls这样一套下来/api/ships/ 是列表接口/api/ships/1/ 是单条数据接口POST、PUT、PATCH、DELETE 方法分别对应增改删。DRF 的接口还有一个额外好处它自动生成可交互的 API 文档页面浏览器里打开 /api/ships/ 就能看到接口的字段说明和参数格式可以直接在页面上测试接口后端调试效率提升非常明显。分页配置我单独说一句。列表接口在大数据量下必须分页不然一次返回几千条数据前端渲染会卡网络传输也慢。DRF 分页有几种模式管理系统的表格页面我用的是 PageNumberPagination按页码分页class StandardResultsSetPagination(PageNumberPagination): page_size 20 page_size_query_param page_size max_page_size 100前端传 /api/ships/?page2page_size20 就能翻页page_size 不传的话用默认的 20。分页的响应格式里会带上总条数、当前页数据、上一页下一页的 URL前端表格分页器直接消费这些字段就行。3.3 Vue 页面与组件的实现思路前端这边我把整个系统搭成了几个核心的页面结构船舶列表页、船舶详情页、证书管理页、船员管理页、航次记录页和系统设置页。页面虽然多但代码组织起来其实是有规律可循的。第一个必聊的就是路由。Vue 3 搭配 Vue Router 4路由的作用是把 URL 和页面组件对应起来。船舶列表页的路由长这样import { createRouter, createWebHistory } from vue-router const routes [ { path: /ships, name: ShipList, component: () import(../views/ship/ShipList.vue) }, { path: /ships/:id, name: ShipDetail, component: () import(../views/ship/ShipDetail.vue), props: true } ] const router createRouter({ history: createWebHistory(), routes }) export default router这里用了懒加载的方式引入组件即 component: () import(...)好处是首屏加载不用一次性下载所有页面的代码访问到哪个路由才加载哪个页面的 JS管理系统页面多的时候提速效果很明显。船舶详情页的路由用到了动态路径参数 :id页面组件可以通过 route.params.id 拿到当前查看的是哪一条船。我习惯在组件里配合 watch 监听 id 变化这样用户从详情页跳转到另一条船的详情页时组件能感知到参数变化并重新拉取数据不会出现页面内容没跟着变的诡异现象。组件复用这块Vue 的插槽是一个特别实用的工具。管理系统里弹窗是高频场景新增船舶、编辑船员信息、确认删除都需要弹窗。如果每个弹窗都单独写一遍会有大量重复的 HTML 结构和逻辑。我用一个基础弹窗组件封装了一层把确定按钮、取消按钮、遮罩层、动画全部做好用插槽把弹窗内容区域留给调用方填充template div classmodal-mask v-ifvisible click.selfclose div classmodal-container div classmodal-header slot nametitle默认标题/slot /div div classmodal-body slot namecontent/slot /div div classmodal-footer button clickclose取消/button button clickconfirm classbtn-primary确定/button /div /div /div /template插槽的几个写法都要掌握具名插槽slot nametitle、默认插槽、作用域插槽。作用域插槽稍微难理解一些但它是处理表格中自定义列渲染的利器。举个例子船舶列表的每一行都有一个状态列不同状态要显示不同的样式和操作按钮靠组件内部判断会写死业务逻辑用作用域插槽把当前行的数据暴露给调用方调用方拿到数据后根据自己的逻辑渲染复用性和灵活性都高很多。管理系统的核心页面组件我看下来是这套思路页面级组件做数据请求和业务编排把拿到的数据传给通用表格组件表格组件负责渲染列、处理排序、触发分页。用 Element Plus 的话直接用的是它提供的 el-table、el-pagination重点是把 API 请求的代码收拢到一个公共的 api 模块里不要每个页面组件都直接写 axios 请求不然接口地址散落各处后端改一个路径要全局搜一遍替换维护成本很高。3.4 前后端联调与跨域处理的几个坑前后端分开开发联调阶段必然遇到跨域问题。前端开发服务器跑在 localhost:5173后端 Django 跑在 localhost:8000端口不一样浏览器出于同源策略默认就会拦截前端向后端发的请求。联调的第一步先把跨域问题解决掉。Django 后端解决跨域最简单的方式是装 django-cors-headerspip install django-cors-headers在 settings.py 里配置 INSTALLED_APPS 加 corsheadersMIDDLEWARE 里加上 CorsMiddleware并且把它尽量放在前面。CORS_ALLOWED_ORIGINS 设置成允许访问的前端开发地址CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]这个配置的意思是说只允许 5173 端口的前端页面跨域调用接口其他域名的请求依然会被拦截。很多人图省事直接配 CORS_ALLOW_ALL_ORIGINS True上线之后谁都能跨域调这个接口在内部系统里还好如果是暴露在公网的服务这就是一个安全隐患不建议这么干。前端这边axios 的封装也有讲究。我习惯建一个 request.js 统一做 baseURL 配置和请求拦截器import axios from axios const request axios.create({ baseURL: http://localhost:8000/api/, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { window.location.href /login } return Promise.reject(error) } ) export default request把请求地址统一收口到 baseURL页面组件里请求接口只需要写 url:ships/后端 API 路径变更的时候只需要改一个地方。响应拦截器里做了两个重要的处理一个是直接返回 response.data前端拿到的是干净的 JSON 数据不用每次 .then 里再取一层 data另一个是 401 统一跳转到登录页用户登录过期的时候不用等所有接口逐一报错弹窗直接踢回登录页重新登录体验干净利落。联调过程中还有一个非常隐蔽但常见的坑——列表接口带查询参数时前端传的中文关键词出现乱码。axios 在 GET 请求传 params 的时候会自动做 URL 编码正常情况下不会乱码但如果你手动拼了带中文的 URL 字符串浏览器和 Django 对编码的处理方式可能不一致导致后端拿到的关键词变成乱码。解决办法是用 axios 的 params 对象传参数让 axios 统一处理编码不要自己手动拼接查询字符串。4. 打包部署与高频问题排查4.1 从开发机到服务器的部署流程开发环境下系统能跑通了接下来就是部署上线。部署方案我推荐的是一个经典组合Nginx Gunicorn Django Vue。Nginx 负责接收外部 HTTP 请求托管前端打包后的静态文件同时把 /api/ 开头的请求反向代理给后端的 Gunicorn。Gunicorn 是 Python 的 WSGI 服务器用来跑 Django 应用比 Django 自带的 runserver 稳定得多runserver 只适合开发调试并发能力很差上线部署必须换 Gunicorn。前端打包先来npm run build这条命令会在项目目录下生成 dist 文件夹里面是编译压缩后的纯静态文件。把 dist 里的所有文件传到服务器的某个目录比如 /var/www/ship_frontend/Nginx 把这个目录作为站点根目录就行。后端这边先在服务器上把依赖装齐了把项目代码传上去之后pip install -r requirements.txt python manage.py collectstatic python manage.py migratecollectstatic 是把 Django 的自带静态文件收集到一个统一目录Django Admin 页面能正常显示样式全靠它。migrate 是把模型变更同步到数据库新环境第一次部署这一步必不可少。然后安装 Gunicorn 并启动gunicorn ship_management.wsgi:application -w 4 -b 127.0.0.1:8000-w 4 表示启动 4 个 worker 进程具体数量一般按 CPU 核心数的两倍左右配置。-b 指定监听 127.0.0.1:8000注意这里只监听本机地址Nginx 就在同一台机器上通过本机把请求转发给 Gunicorn。不要直接让 Gunicorn 监听公网地址没有 Nginx 在前面做静态文件处理和限流后端直接暴露在公网下是不安全的。要是用 systemd 管理 Gunicorn 进程还能实现开机自启和进程崩溃后自动重启生产环境推荐这么干别用 nohup 挂在后台完事。Nginx 的关键配置server { listen 80; server_name your_domain.com; root /var/www/ship_frontend; index index.html; location / { try_files $uri $uri/ /index.html; } 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 /static/ { alias /var/www/ship_static/; } }location / 里的 try_files 指令特别关键它确保前端路由在刷新页面的时候不会 404。因为 Vue Router 用的是 history 模式URL 是 /ships/1 这种真实路径而服务器上并没有这个文件try_files 会在找不到对应文件时回退到 index.html再由前端路由接管页面就正常显示了。如果漏了这一行用户点进详情页后按刷新浏览器直接 404这是部署 history 模式前端项目时最典型的一个坑。4.2 实战中踩过的坑和排查方法做这类系统前端后端加起来代码量不小运行环境也不是干干净净的我把实战中真正遇到过的高频问题整理成了一张排查速查表。这些问题分布的规律很明显很多是环境差异、版本差异、配置遗漏造成的并不一定是代码逻辑写错了所以排查的时候思路要先从环境层开始筛再去查代码层。问题现象可能原因排查与解决方法前端页面白屏控制台报错Vue 打包后静态资源路径不对检查 vite.config.js 里的 base 配置部署到子路径时改为 ./ 相对路径接口 404但后端路由存在Nginx 代理路径没匹配上检查后端实际路由前缀确保 Nginx location 和 Django 路由一致列表接口返回 500ORM 查询字段名写错或数据库缺字段看 Django 日志文件查具体报错堆栈必要时执行 makemigrations 和 migrate请求接口返回 CORS 错误跨域配置没生效或配置顺序有问题确认 corsheaders 中间件位置确认 CORS_ALLOWED_ORIGINS 是否包含当前域名中文数据乱码数据库字符集不是 utf8MySQL 创建库时指定 utf8mb4Django 连接配置里加 OPTIONS charset 参数Gunicorn 启动失败端口被占用或依赖缺失用 lsof -i:8000 查占用进程pip list 确认依赖完整刷新详情页 404Nginx try_files 配置缺失检查 location / 里是否配置 try_files $uri $uri/ /index.html这里面我要单独展开说两个值得记的坑。第一个是 Vue 打包部署后页面白屏的问题。开发模式一切正常npm run build 也成功但放到服务器上打开是白屏控制台报一堆资源 404。这几乎可以肯定是 vite.config.js 里的 base 配置问题。默认情况下打包后的资源路径是绝对路径 /assets/xxx.js如果你的前端站点不是部署在域名根目录而是部署在比如 http://ip:8080/ship/ 这个子路径下浏览器去访问 /assets/xxx.js 自然就找不到了。解决办法是把 base 改成 ./让资源路径变成相对路径这样不管站点部署在哪个子路径下都能正确加载。这个坑非常常见尤其是第一次用 Vite 部署到服务器的朋友。第二个是 MySQL 中文乱码问题。本地开发用 SQLite 不会遇到切到 MySQL 之后如果建库的时候没有指定 utf8mb4 字符集写入的中文数据在页面上显示成一堆问号。解决方案是创建数据库时明确指定字符集CREATE DATABASE ship_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Django 连接的配置也要对应调整DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: ship_db, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }utf8 和 utf8mb4 的区别简单说utf8mb4 是 utf8 的超集能完整支持中文和 emoji 字符。这个在系统初期不显眼但一旦录入的数据里带了个特殊字符整个字段写入失败排查起来非常费劲倒不如一开始就统一用 utf8mb4。4.3 安全与性能的小优化管理系统用的人不多但数据敏感度高安全这块不能省。在最基本的层面用户密码必须加密存储Django 自带的 User 模型用的 PBKDF2 算法加密直接使用没有问题不要自己造轮子存明文。登录认证推荐用 JWT办公系统前后端分离JWT 无状态、跨域友好实现起来也省事。权限控制上Django REST framework 自带的权限类能满足大部分场景。比如修改船舶信息只允许管理员操作普通用户只读视图里指定class ShipViewSet(viewsets.ModelViewSet): queryset Ship.objects.all() serializer_class ShipSerializer def get_permissions(self): if self.action in [create, update, partial_update, destroy]: return [IsAdminUser()] return [IsAuthenticated()]这样设置之后未登录用户连列表都看不了已登录的普通用户只能查看只有 admin 用户才能增删改。这是企业内部系统很合理的一种权限模型。如果后续业务上还要更细的区分——比如船员管理只有人事部门能操作——就用 DRF 的 ObjectPermissionLevel 或者自定义权限类按部门角色做思路是一脉相承的。性能优化方面Django 这类业务系统最先要关注的不是代码执行速度而是数据库查询次数。最典型的性能坑是 N1 查询。比如在船舶列表页面要显示每条船关联的证书数量如果不用 select_related 或 prefetch_relatedORM 会先查一次所有船舶再对每一条船分别查一次证书数量船舶有 50 条数据库就要被查 51 次。正确做法是在查询集里预先声明需要关联加载的外键关系queryset Ship.objects.all().prefetch_related(certificates)prefetch_related 会用一条额外 SQL 把所有船的证书数据一次性查出来在内存里做匹配数据库查询次数从 51 次降为 2 次。系统运行一段时间、数据量上来之后这类优化带来的体验提升是肉眼可见的。再一个是接口加缓存。船舶基础信息不是高频变化的数据修改频率很低但列表页和详情页会被反复查看。这种接口非常适合加一层缓存比如用 Django 的 cache_page 装饰器对视图做响应缓存或者用 Redis 缓存 ORM 查询结果。访问这种接口时首次查询数据库之后直接取缓存响应对数据库的压力能降一个量级。船舶数据有几万条的时候缓存的作用会非常明显。另外图片和证书文件这类静态资源不要直接存到数据库的字段里建议文件路径存数据库文件本身存到服务器磁盘或者对象存储服务。我用 Django 的 FileField 配合 MEDIA_ROOT 设置上传的证书会自动存到指定目录数据库里只保存相对路径。这样数据库的体积能控制住备份恢复的效率也高得多。部署上线之后记得把 Django 的 DEBUG 关掉SECRET_KEY 换成随机长字符串并且从环境变量读取ALLOWED_HOSTS 改成实际的域名或 IP。这几个配置不调整的话会留下非常明显的信息泄露和安全隐患。除此之外加上日志记录Django 的操作日志和错误日志分开存出问题的时候按日志查比靠回忆查代码快太多了。回到这套系统本身。从最初的数据模型设计到接口层开发再到前端的 Vue 页面搭建最后到服务器上的 Nginx 加 Gunicorn 部署这套走下来船舶档案、证书提醒、航次记录这些核心功能都能跑得稳稳当当了。我在这个项目里最深的体会是技术栈选型不用追求花哨Django 加 Vue 这套组合做企业内部管理系统已经是非常成熟和高效的答案了关键是把数据模型设计清楚、把 ORM 操作的细节拿捏住、把部署配置的坑提前绕开。后面如果业务有新的需求比如加一个船舶油耗分析的大屏看板或者接卫星定位数据做轨迹展示这套系统的基础架构也完全撑得住那时候再引入 FastAPI 做数据接口、引入图表组件库做可视化都是水到渠成的事。