☰
船舶信息管理系统实战:Django与Vue全栈开发指南
2026/10/8 2:36:50 网站建设 项目流程

做船舶相关的信息管理系统,这几年需求一直挺稳定。无论是港务集团、船代公司,还是做船舶租赁、船员派遣的团队,基本都逃不开一套能管船舶档案、船员信息、航行记录、证书到期提醒的系统。市面上买成品不便宜,定制又贵,所以很多团队会选择用 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.2,B 项目用的 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 vite@latest 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_length=100) ship_type = models.CharField('船舶类型', max_length=50) port_of_registry = models.CharField('船籍港', max_length=50) gross_tonnage = models.FloatField('总吨位') net_tonnage = models.FloatField('净吨位') build_date = models.DateField('建造日期') hull_material = models.CharField('船体材料', max_length=50) main_engine_power = models.FloatField('主机功率') length = models.FloatField('船长') width = models.FloatField('船宽') remark = models.TextField('备注', blank=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) 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_delete=models.CASCADE, verbose_name='所属船舶', related_name='certificates') cert_type = models.CharField('证书类型', max_length=50) cert_number = models.CharField('证书编号', max_length=100) issue_date = models.DateField('签发日期') expire_date = models.DateField('有效期至') file = models.FileField('证书文件', upload_to='certificates/%Y/%m/', blank=True, null=True) 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_delete=models.CASCADE,那关联的记录会一并删除。比如证书关联了船舶,你删掉一条船舶记录,它的所有证书记录也会跟着消失。如果这是业务上不允许的,删船的时候想保留历史证书数据,那就得在设计外键时把 on_delete 改成 models.SET_NULL,并且把外键字段设为 null=True。这个决策一定要在模型设计阶段就跟业务方确认清楚,不然上线后误删数据就是事故级别的。

再补充一个 ORM 查询的实用技巧。做船舶列表页的搜索功能,经常需要按多种条件过滤。Django ORM 的链式查询可以优雅地处理:

ships = Ship.objects.all() if keyword: ships = ships.filter(name__icontains=keyword) if ship_type: ships = ships.filter(ship_type=ship_type) if min_tonnage: ships = ships.filter(gross_tonnage__gte=min_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(r'ships', ShipViewSet, basename='ship') 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/?page=2&page_size=20 就能翻页,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 class="modal-mask" v-if="visible" @click.self="close"> <div class="modal-container"> <div class="modal-header"> <slot name="title">默认标题</slot> </div> <div class="modal-body"> <slot name="content"></slot> </div> <div class="modal-footer"> <button @click="close">取消</button> <button @click="confirm" class="btn-primary">确定</button> </div> </div> </div> </template>

插槽的几个写法都要掌握:具名插槽(slot name="title")、默认插槽、作用域插槽。作用域插槽稍微难理解一些,但它是处理表格中自定义列渲染的利器。举个例子,船舶列表的每一行都有一个状态列,不同状态要显示不同的样式和操作按钮,靠组件内部判断会写死业务逻辑,用作用域插槽把当前行的数据暴露给调用方,调用方拿到数据后根据自己的逻辑渲染,复用性和灵活性都高很多。

管理系统的核心页面组件,我看下来是这套思路:页面级组件做数据请求和业务编排,把拿到的数据传给通用表格组件,表格组件负责渲染列、处理排序、触发分页。用 Element Plus 的话,直接用的是它提供的 el-table、el-pagination,重点是把 API 请求的代码收拢到一个公共的 api 模块里,不要每个页面组件都直接写 axios 请求,不然接口地址散落各处,后端改一个路径要全局搜一遍替换,维护成本很高。

3.4 前后端联调与跨域处理的几个坑

前后端分开开发,联调阶段必然遇到跨域问题。前端开发服务器跑在 localhost:5173,后端 Django 跑在 localhost:8000,端口不一样,浏览器出于同源策略默认就会拦截前端向后端发的请求。联调的第一步,先把跨域问题解决掉。

Django 后端解决跨域最简单的方式是装 django-cors-headers:

pip install django-cors-headers

在 settings.py 里配置 INSTALLED_APPS 加 'corsheaders',MIDDLEWARE 里加上 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 migrate

collectstatic 是把 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 创建库时指定 utf8mb4,Django 连接配置里加 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 这类业务系统最先要关注的不是代码执行速度,而是数据库查询次数。最典型的性能坑是 N+1 查询。比如在船舶列表页面要显示每条船关联的证书数量,如果不用 select_related 或 prefetch_related,ORM 会先查一次所有船舶,再对每一条船分别查一次证书数量,船舶有 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 做数据接口、引入图表组件库做可视化,都是水到渠成的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询