ARTICLE DETAIL

资讯详情

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

基于Python Flask的码头船只货柜管理系统实战全解析

基于Python Flask的码头船只货柜管理系统实战全解析 先别被“码头”“货柜”这两个词唬住把它往下拆就是一套典型的Web管理后台。我在做这套基于Python Flask的码头船只货柜管理系统时最大的感受是业务复杂度全在状态两个字上——一个集装箱从船到堆场再到闸口中间隔着装卸作业、时间窗口和操作记录。这篇文章把我从业务梳理、数据库设计、页面联调到部署上线的完整路径写出来适合正在找Flask实战项目、或者需要交毕业设计/课程设计的人参考。网上搜“基于Python Flask的码头船只货柜管理系统”能搜出来一堆同名的源码包免费源码大全里这类项目尤其多。但大部分打开之后只有几张表和几个增删改查页面没有把到港、装卸、出场这条主线讲清楚。我这次做的时候刻意把重点放在业务闭环上也把踩过的坑一并写出来。项目本身不算大但五脏俱全船只管理、货柜管理、装卸作业、状态流转、统计查询都有代码结构上用了蓝图拆分数据库用SQLAlchemy页面用Jinja2模板加Bootstrap前后端不分离。这套组合非常适合作为Flask实战练手项目。1. 先把业务想清楚货柜状态流转比页面数量重要得多1.1 从一次到港作业看懂系统要管什么码头货柜管理的核心流程是这样的一艘船靠泊船公司提前把舱单发过来上面写着每个柜子的柜号、尺寸、箱型和装船位置。船靠岸后开始卸船作业每个柜子从船上吊到堆场堆场管理员给它分配一个位置之后客户来提柜柜子装上拖车通过闸口出场。反过来还有装船作业出口柜子从堆场运到码头前沿吊装上船。这个流程里系统要回答的核心问题只有几个这艘船装了多少柜子还有多少柜子没卸某个柜子现在到底在船上还是在堆场今天出了多少柜子每一个问题都依赖两个东西数据记录准确和状态实时更新。所以我在动手写代码之前先把业务流程画了一遍不是流程图文档就是自己梳理清楚然后把系统的功能边界确定下来。1.2 系统边界哪些功能必须做哪些先不做很多毕设项目失败在功能贪多。我当时列了两份清单。必须做的第一版功能船只管理建立船只档案、到港登记、离港标记货柜管理柜号、尺寸、箱型、所在位置、当前状态装卸作业卸船登记、装船登记、出场登记查询统计按船查柜、按状态查柜、按时间段统计作业量明确不做、留到二期再说的功能计费结算码头收费和堆存费计算涉及费率体系很复杂设备管理岸桥、拖车、正面吊的调度排班属于另一个系统船公司EDI对接电子数据交换是标准协议需要申请测试环境不适合本地开发堆场可视化像游戏地图一样显示柜子位置看着酷但实用价值有限这样一划系统边界就清楚了。后来证明这个决策很明智第一版上线用时短业务方用起来也顺手后续扩展需求再迭代加功能不会一上来就被大而全的框架拖死。2. 框架选型对比Flask在这类管理系统里赢在哪里2.1 Flask、Django、FastAPI的取舍我在选型的时候其实对比过三个框架Flask、Django、FastAPI。很多人一碰到这种项目就纠结我直接说结论管理系统用Flask是性价比最高的选择。拿三个框架横向对比一下对比项FlaskDjangoFastAPI学习曲线平缓核心就一个路由和视图陡峭ORM、Admin、中间件概念多平缓但要理解异步和类型标注内置功能少需要自己组装全自带Admin后台和ORM少偏向API开发ORM能力SQLAlchemy灵活可控Django ORM集成度高但重SQLAlchemy但异步生态更复杂适合场景中小型Web应用、服务端渲染大型内容站点、快速全栈后台高并发API服务、微服务社区资料极多踩坑容易搜到答案多英文资料质量高增长快但中文资料偏少为什么最终选了Flask三个理由第一这个系统的瓶颈根本不在Web框架本身。码头管理系统的并发量很低一线操作员几十个人主要是表单提交和列表查询。把性能精力花在数据库查询优化上收益远大于换一个“高性能异步框架”。第二Flask是组装式框架每一个组件都是你主动选进来的。用了Flask-SQLAlchemy、Flask-WTF、Flask-Login出了问题你清楚知道是哪一层的问题。Django太全它的Admin后台确实漂亮但如果你就三四张表反而被框架绑住。第三用FastAPI做服务端渲染页面其实并不顺手。FastAPI的强大在Pydantic校验和异步API但它不像Flask那样有成熟的Jinja2表单渲染体系。如果只是给前端提供接口FastAPI确实爽但做传统Web管理系统Flask更稳。2.2 用蓝图把系统拆成五个模块项目一开始我就决定用蓝图Blueprint拆分模块而不是把所有路由写在一个文件里。拆的好处是业务边界清晰多人协作不碰同一文件后期加功能不会把app.py变成几千行的大泥球。实际的项目结构是这样的port_cargo/ ├── app/ │ ├── __init__.py # 创建app注册蓝图 │ ├── models.py # 数据库模型 │ ├── ships/ # 船只管理模块 │ │ ├── __init__.py │ │ ├── views.py # 路由 │ │ ├── forms.py # 表单 │ │ └── templates/ # 模板文件 │ ├── containers/ # 货柜管理模块 │ │ ├── __init__.py │ │ ├── views.py │ │ └── forms.py │ ├── operations/ # 装卸作业模块 │ │ ├── __init__.py │ │ ├── views.py │ │ └── forms.py │ ├── stats/ # 统计模块 │ │ ├── __init__.py │ │ └── views.py │ └── auth/ # 登录认证模块 │ ├── __init__.py │ ├── views.py │ └── forms.py ├── run.py # 启动入口 └── requirements.txt每个蓝色模块内部自己管自己的视图和表单模块之间通过模型层通信。比如operations模块要改Container状态直接引用models里的类不跨模块调用视图。这样模块之间的耦合度降下来了。2.3 依赖清单与Python环境准备依赖这块踩过一个小坑直接pip install flask装的是Flask 3.x但网上很多教程还停留在Flask 1.x的写法。Flask 3.x有几个小变化需要注意比如before_first_request被移除了app.run()调试模式的写法也变了。所以我直接固定依赖版本保证环境一致。requirements.txt里的核心依赖Flask3.0.3 Flask-SQLAlchemy3.1.1 Flask-WTF1.2.1 Flask-Login0.6.3 Flask-Migrate4.0.7Python环境配置本身不难难的是很多新手在Windows上环境变量没配好。我建议直接用虚拟环境避免把包装到系统Python里污染环境python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux/macOS pip install -r requirements.txt如果你还在为装Python苦恼记住一点装的时候勾选“Add Python to PATH”后面能省一大部分时间。3. 数据库模型设计三张表就把业务闭环撑起来3.1 船只表一切业务的起点码头所有作业都围绕“船”展开所以船只表是整个系统的根基。我当时设计字段的时候参考了船公司舱单的习惯写法保留了船名、IMO编号、船籍、泊位这些核心字段没加太多花哨字段。from datetime import datetime from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() class Ship(db.Model): __tablename__ ships id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), nullableFalse, comment船名) imo db.Column(db.String(20), uniqueTrue, indexTrue, commentIMO编号) nationality db.Column(db.String(32), comment船籍) berth db.Column(db.String(16), comment泊位号) arrived_at db.Column(db.DateTime, comment到港时间) departed_at db.Column(db.DateTime, comment离港时间) status db.Column(db.String(16), defaultdocked, comment在港/离港) containers db.relationship(Container, backrefship, lazydynamic)IMO编号是国际海事组织给船的唯一编号就像船的身份证号。这字段我加了uniqueTrue防止重复建档。status字段用字符串而不是布尔值因为业务上还有“已到港未靠泊”“作业中”“已离港”等状态字符串扩展性更好。3.2 货柜表状态字段的设计思路货柜表是整个系统数据最密集的表。每个柜子有唯一的柜号container_no这是现场作业员扫码枪扫的那个编号。状态字段我设计了三个值on_ship在船、in_yard在堆场、left已出场后续还留了damaged破损待检等扩展空间。class Container(db.Model): __tablename__ containers id db.Column(db.Integer, primary_keyTrue) container_no db.Column(db.String(16), uniqueTrue, nullableFalse, indexTrue, comment柜号) size db.Column(db.String(8), default40HQ, comment尺寸/箱型) box_type db.Column(db.String(16), comment货类) status db.Column(db.String(16), defaulton_ship, comment当前状态) current_location db.Column(db.String(32), comment当前位置) ship_id db.Column(db.Integer, db.ForeignKey(ships.id), comment所属船只) in_yard_at db.Column(db.DateTime, comment进场时间) left_at db.Column(db.DateTime, comment出场时间) created_at db.Column(db.DateTime, defaultdatetime.utcnow) updated_at db.Column(db.DateTime, defaultdatetime.utcnow, onupdatedatetime.utcnow) operations db.relationship(Operation, backrefcontainer, lazydynamic, cascadeall, delete-orphan)这里有一个设计上的关键取舍Container表同时存当前状态和作业时间Operation表存历史流水。为什么不全靠流水表推导当前状态因为实时查询太慢了——要查一个柜子状态得把它的全部操作记录按时间排序再捞出来。而状态冗余在当前表里查询只要走索引就能命中速度是毫秒级的。风险也理解冗余数据可能和流水不一致。解决办法不是不冗余而是用数据库事务保证两个表同时更新业务层绝不能分两次提交。3.3 作业记录表把“发生过什么”存成流水作业记录表Operation记录每一次动作卸船、装船、出场。这张表只做追加不做修改和删除保证流水可追溯。每个码头操作员都能查到“某月某日某时某柜号被谁做了哪个操作”。class Operation(db.Model): __tablename__ operations id db.Column(db.Integer, primary_keyTrue) container_id db.Column(db.Integer, db.ForeignKey(containers.id), nullableFalse, indexTrue, comment货柜ID) ship_id db.Column(db.Integer, db.ForeignKey(ships.id), nullableTrue, comment关联船只ID) action_type db.Column(db.String(16), nullableFalse, comment操作类型) location db.Column(db.String(32), comment作业位置) operator db.Column(db.String(32), comment操作人) remark db.Column(db.String(128), comment备注) happened_at db.Column(db.DateTime, defaultdatetime.utcnow, indexTrue)设计这张表的时候我特意把ship_id也放进来而不是只通过外键关系从Container反查。原因是实际业务中有一种情况某个柜子被误登记到错误的船下面后来虽然纠正过来了但历史作业记录还是需要能看到“原登记船只是什么”。流水表冗余这个字段可以在查“某条船的历史作业”时不依赖Container当前状态直接按船过滤流水。索引设计上operations表的container_id加索引happened_at加索引这两个字段是查询频率最高的。4. 核心功能实现从表单提交到页面回显的完整链路4.1 船只到港登记的完整实现以船到港登记为例我把表单验证、路由处理、模板渲染这条完整链路写一遍。表单用Flask-WTF写好处是CSRF防护、字段校验一套到位不用自己手工写。from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired, Length class ShipForm(FlaskForm): name StringField(船名, validators[DataRequired(), Length(max64)]) imo StringField(IMO编号, validators[DataRequired(), Length(max20)]) nationality StringField(船籍) berth StringField(泊位号) submit SubmitField(保存)视图层逻辑很简单GET请求渲染空表单POST请求验证后入库。但有几个细节必须处理否则生产环境会出问题from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_required from app.models import Ship, db from app.ships.forms import ShipForm ship_bp Blueprint(ship, __name__) ship_bp.route(/ships/create, methods[GET, POST]) login_required def create_ship(): form ShipForm() if form.validate_on_submit(): ship Ship( nameform.name.data.strip(), imoform.imo.data.strip(), nationalityform.nationality.data.strip() or None, berthform.berth.data.strip() or None, arrived_atdatetime.utcnow(), statusdocked ) db.session.add(ship) try: db.session.commit() flash(船只登记成功, success) return redirect(url_for(ship.list_ships)) except IntegrityError: db.session.rollback() flash(IMO编号已存在请核对后再登记, danger) return render_template(ships/create.html, formform)两个细节值得多说一句。第一form.name.data.strip()——前端做了必填校验但后端还要再做一次防止用户输入全空格。这属于后端不容一丝偷懒的原则。第二用try/except捕获IntegrityError而不是提前查一遍再判断。为什么不先查数据库因为查询和插入之间存在时间窗口并发下两个请求同时查到“不存在”然后同时插入靠查询挡不住。唯一索引加上异常捕获才是双保险。4.2 装卸作业如何改变货柜状态装卸作业是整个系统最核心的操作逻辑上就是校验当前状态执行状态迁移写流水三件事在一个事务里完成。以“卸船登记”为例操作员输入柜号选择动作类型为卸船系统先判断这个柜子当前状态必须是on_ship才能改成in_yard。如果状态不对直接拒绝操作并给出提示这样可以防止现场误操作把已经出场的柜子又卸一遍。op_bp.route(/operations/register, methods[GET, POST]) login_required def register(): form OperationForm() if form.validate_on_submit(): container Container.query.filter_by( container_noform.container_no.data.strip() ).first() if not container: flash(柜号不存在请先去货柜管理页面新增, danger) return render_template(operations/register.html, formform) action form.action_type.data # 状态机校验 allowed { unload: [on_ship], # 卸船柜子必须在船 load: [in_yard], # 装船柜子必须在堆场 leave: [in_yard], # 出场柜子必须在堆场 } if container.status not in allowed.get(action, []): flash(f当前柜子状态为 {container.status}不能执行 {action} 操作, danger) return render_template(operations/register.html, formform) # 状态迁移和时间记录 now datetime.utcnow() if action unload: container.status in_yard container.in_yard_at now elif action load: container.status on_ship container.in_yard_at None elif action leave: container.status left container.left_at now op Operation( container_idcontainer.id, ship_idcontainer.ship_id, action_typeaction, locationform.location.data.strip() or None, operatorcurrent_user.username, remarkform.remark.data.strip() or None, happened_atnow ) db.session.add(op) db.session.commit() flash(作业登记成功, success) return redirect(url_for(operations.list_operations)) return render_template(operations/register.html, formform)这个状态校验有点像电梯门锁逻辑不是任何状态都能切到任何状态必须按预定义的迁移路径走。我把状态迁移关系写成了allowed字典改起来一目了然。后续要加“破损待检”状态只需在这个字典里加一条映射其他代码不用动。顺带说一句operator字段我从current_user.username取而不是让操作员自己填名字。原因很现实现场谁登录就是谁操作日志追责才有效。如果让操作员自己填填错名字日志就没意义了。4.3 列表搜索、分页与状态筛选列表页看着简单但实际做过的人都懂真正的坑在分页和搜索条件组合上。我封装了一个通用的查询写法直接在视图层处理ct_bp.route(/containers) login_required def list_containers(): page request.args.get(page, 1, typeint) keyword request.args.get(q, ).strip() status request.args.get(status, ).strip() query Container.query if keyword: query query.filter(Container.container_no.contains(keyword)) if status: query query.filter(Container.status status) pagination query.order_by(Container.id.desc()).paginate( pagepage, per_page20, error_outFalse ) containers pagination.items return render_template(containers/list.html, containerscontainers, paginationpagination, keywordkeyword, statusstatus)error_outFalse这个参数一定要加否则用户手动改URL里的?page999会直接报404。加上之后页码超范围就自动返回空列表体验好得多。分页模板我复用了一个宏用Jinja2渲染页码{% macro render_pagination(page_endpoint, pagination, keyword, status) %} nav ul classpagination {% for p in pagination.iter_pages(left_edge1, right_edge1, left_current2, right_current2) %} {% if p %} li classpage-item {{ active if p pagination.page else }} a classpage-link href{{ url_for(page_endpoint, pagep, qkeyword, statusstatus) }} {{ p }} /a /li {% else %} li classpage-item disabledspan classpage-link.../span/li {% endif %} {% endfor %} /ul /nav {% endmacro %}注意搜索和筛选条件也要通过q和status参数带进分页链接否则点击第2页的时候搜索条件就丢了。这个坑我一开始犯过翻页之后列表变成了全量数据排查了半天才发现是分页链接没带参数。4.4 模板层用Bootstrap快速搭出可用后台既然不前后端分离模板就是系统的门面。我直接用Bootstrap 5的CDN没有自己写CSS。基础布局模板里把导航栏、Flash消息区域、内容块三部分固定好所有页面继承这一个layout整体风格统一。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}码头货柜管理系统{% endblock %}/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css relstylesheet /head body nav classnavbar navbar-expand-lg navbar-dark bg-dark div classcontainer a classnavbar-brand href{{ url_for(ship.list_ships) }}码头货柜管理系统/a div classnavbar-nav a classnav-link href{{ url_for(ship.list_ships) }}船只管理/a a classnav-link href{{ url_for(ct.list_containers) }}货柜管理/a a classnav-link href{{ url_for(operations.register) }}作业登记/a a classnav-link href{{ url_for(stats.index) }}统计看板/a /div /div /nav div classcontainer mt-3 {% with messages get_flashed_messages(with_categoriestrue) %} {% for category, message in messages %} div classalert alert-{{ category }}{{ message }}/div {% endfor %} {% endwith %} {% block content %}{% endblock %} /div /body /html这种开发方式很“老派”但胜在简单直接。现场操作员用的是内部局域网电脑浏览器基本都是Chrome或EdgeBootstrap渲染稳定没有兼容性问题。如果非要上Vue/React还得多一套接口联调和权限管理对一个小团队来说不划算。5. 踩坑复盘开发中真正耗时间的四个问题5.1 时间字段为什么一律存UTC这是第一个踩的坑也是最容易忽略的。刚开始我用datetime.now()存操作时间本地开发一切正常。后来把应用放到测试服务器上服务器时区是UTC和本地时区差8个小时。结果操作员早上8点的卸船记录在系统里显示成凌晨0点整个统计全乱了。解决方案一句话数据库一律存UTC展示层再转本地时区。SQLAlchemy模型里所有时间字段默认用datetime.utcnow而不是datetime.now我在前面模型代码里已经是这么写的了。模板展示时用一个Jinja2过滤器做时区转换small{{ op.happened_at.replace(tzinfotimezone.utc).astimezone()|datetime_format }}/small更规范的做法是用zoneinfo库按Asia/Shanghai转from zoneinfo import ZoneInfo from datetime import datetime def to_local(dt): if dt is None: return return dt.replace(tzinfoZoneInfo(UTC)).astimezone(ZoneInfo(Asia/Shanghai))所以关于时间字段我的建议是存UTC是铁律展示本地化是按需。数据库里的时间统一了将来无论服务器搬到哪个区域数据都不会乱。5.2 重复登记并发下如何保证货柜不重不漏货柜表有一个场景很典型现场两台中控电脑同时录入同一船卸下来的同一个柜号。如果代码逻辑是“先查有没有没有再插入”两个请求同时查到不存在双双插入成功就会出现重复柜号而这种错误一旦混进生产数据非常难清理。这个问题靠业务代码根本解决不了唯一可靠的方法是数据库层的唯一约束。container_no字段加了uniqueTrue之后第二个插入请求必然抛IntegrityError。业务代码要做的是捕获这个异常给用户友好提示try: db.session.commit() flash(保存成功, success) except IntegrityError: db.session.rollback() flash(柜号已存在请确认是否重复录入, danger)很多人只在应用层校验不在数据库层加约束这是本末倒置。应用层校验是体验问题数据库约束才是数据正确性的底线。这个项目的所有唯一业务字段柜号、IMO编号我都加了数据库级唯一索引。5.3 查询性能N1和外键索引怎么排查货柜列表页一开始非常慢几十条数据页面加载要一两秒。打开SQLAlchemy的日志看到列表查询30个柜子竟然执行了60多条SQL。原因就是我在模板里直接用了container.ship.name每个柜子都要查一次船表这就是典型的N1查询问题。解决办法是用joinedload一次性把关联对象查出来from sqlalchemy.orm import joinedload containers Container.query.options( joinedload(Container.ship) ).all()这样两条SQL变成一条SQL加上JOIN速度提升立竿见影。如果你的表关联玩法更多直接用上一节课讲的paginate也没问题在query上加options就行。另一个性能坑是operations表的happened_at字段。一开始没有索引数据量到了几万条后按日期统计就明显变慢。后来在模型定义里给这个字段加了indexTrue加上外键字段自动创建的索引查询速度从几百毫秒降到几十毫秒。排查SQL性能我推荐一个小工具在开发环境打开Flask的SQL日志观察每次页面请求的SQL数量。一眼就能看出哪些页面存在N1、有没有走索引比盲目优化效率高得多。5.4 表单字段和模型字段不一致的趁手处理最后说一个很细但很常见的问题。项目中有一个表单ModelForm和模型字段直接关联修改了模型字段但忘记同步更新表单结果提交时一直校验失败。这类问题隐蔽且浪费时间我的习惯有两条。第一表单和模型放同一个模块目录下改模型字段的时候自然看到表单文件不容易忘。我的结构里forms.py和models.py离得很近就是这个用意。第二所有下拉选项集中管理不散落在模板里。比如状态和动作类型class ContainerStatus: ON_SHIP on_ship IN_YARD in_yard LEFT left CHOICES [ (ON_SHIP, 在船), (IN_YARD, 在堆场), (LEFT, 已出场), ] STATUS_LABEL dict(CHOICES)模板和视图都用这个类来引用状态值避免到处写死字符串。比如统计页展示状态时直接查ContainerStatus.STATUS_LABEL.get(status)就能显示中文不会出现states值不一致的混乱局面。6. 部署上线开发完之后的真实落地过程6.1 服务器上的Python环境怎么搭开发环境跑通了不等于部署没问题。我这次部署到一台Ubuntu 22.04服务器上从零配置Python环境。如果你还没接触过Linux下面的命令可以直接照抄。安装Python虚拟环境和pipsudo apt update sudo apt install -y python3-venv python3-pip然后是项目代码上传创建虚拟环境并安装依赖cd /opt/port_cargo python3 -m venv venv source venv/bin/activate pip install -r requirements.txt这里提醒一句生产环境不要用root直接跑应用。我创建了一个专用用户www-data或者新建port用户把项目目录的所有者改成它用这个用户启动服务。好处是应用被攻破后权限也限制在普通用户不会把服务器整个暴露出去。Flask自带的开发服务器app.run()是绝对不能用于生产的它并发能力弱还有安全警告。我用的是gunicorn用下面的命令启动gunicorn -w 2 -b 127.0.0.1:8000 run:app-w 2表示开两个worker进程。Flask应用是IO密集型配合数据库查询两个worker足够应付几十个同时在线的小团队。-b 127.0.0.1:8000表示只监听本机8000端口不直接暴露到公网。6.2 gunicorn nginx 配置要点为什么不直接把gunicorn绑定到公网IP?因为有个环节要做反向代理。Nginx处理静态文件和HTTP细节比gunicorn强得多也更安全。我安装Nginx后配置了一个反向代理server { listen 80; server_name your-server-ip; location / { 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; } location /static { alias /opt/port_cargo/app/static; expires 7d; } }/static目录单独用Nginx直接服务不走Python进程这对减轻应用压力帮助很大。之后重启Nginxsudo nginx -t sudo systemctl reload nginx这时候在浏览器输入服务器IP就能看到系统登录页了。为了让应用在服务器重启后能自动拉起我还写了一个systemd服务文件/etc/systemd/system/port_cargo.service[Unit] DescriptionPort Cargo Flask App Afternetwork.target [Service] Userport WorkingDirectory/opt/port_cargo ExecStart/opt/port_cargo/venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 run:app Restartalways [Install] WantedBymulti-user.target启用并启动服务sudo systemctl daemon-reload sudo systemctl enable port_cargo sudo systemctl start port_cargo这样即使服务器莫名其妙重启了应用也会自动恢复不需要人工干预。6.3 SQLite还是MySQL以及数据迁移建议开发的时候为了省事默认用的是SQLite。部署后我评估了一下码头系统几十个人用数据量一个月也就几千条操作记录SQLite完全扛得住。这里有个常见的认知误区觉得生产环境必须换MySQL其实取决于数据规模和并发量。两者场景对比我直接给结论场景用SQLite用MySQL/PostgreSQL并发写入少、单机部署完全够用杀鸡用牛刀数据量十万级以下够用够用多应用并发写、团队协作不建议推荐复杂报表、高可用要求不建议推荐如果你后续确实要换数据库有个平滑迁移的路径项目里用了Flask-Migrate管理数据库表结构变更。这样从SQLite切到MySQL只需改一行数据库连接字符串然后跑迁移脚本表结构就对齐了flask db init flask db migrate -m init tables flask db upgrade数据库连接配置放在config.py里切库时只改这里class Config: SQLALCHEMY_DATABASE_URI os.environ.get( DATABASE_URL, sqlite:///port_cargo.db ) # 生产环境用 # mysqlpymysql://user:passwordlocalhost/port_cargo?charsetutf8mb4用环境变量方式动态配置是部署时的一个好习惯。代码里不写死任何账号密码敏感信息全放环境变量或单独配置文件里。6.4 上线后还能扩展的方向第一版上线后码头管理员提了几个后续需求我觉得很典型写出来供做扩展时参考统计看板用ECharts做一个仪表盘展示今日作业量、在场柜数、船舶靠泊数。这需要把统计数据做成JSON接口前端用Ajax拉数据。做起来其实不复杂就是给stats模块加几个返回JSON的路由。批量导入到港船有几百个柜子的时候一个一个手工登记太慢。做CSV批量导入可以大大提升效率。实现思路是服务端解析CSV逐行校验后入库配合事务保证要么全部成功要么全部失败。扫码枪录入现场操作员用扫码枪扫柜号等价于键盘录入。前端不用改给输入框加个autofocus即可扫码枪会把柜号回车快速输入。这个扩展很简单但现场价值极高。操作审计记录谁在什么时间删改了数据。虽然有Operation流水但操作员的登录操作日志还没录。可以用Flask-Login的user_loader回调配合before_request钩子顺手记录访问日志。这几个方向目前我最有信心的一个建议是先做接口化的数据输出能力。把所有统计和查询先整理成JSON格式的API前端消费也好、对接外部系统也好后续扩展都容易。我在这套系统上线后最深的体会是Flask写管理系统难点从来不在框架而在模型设计。把Container状态和Operation流水这对关系设计好后面所有查询和页面都顺了反过来如果只图快把所有字段塞进一张大表后期加业务肯定要重构。项目源码我同步维护在Git仓库里部署脚本和迁移脚本都在想直接跑起来的可以照着文档一步步来。如果之后有人问我要扩展建议我最先推荐做统计看板和批量导入这两个是码头管理员最常开的需求而且跟写原有增删改查是同一套思路不会引入额外复杂度。
返回列表