ARTICLE · 1001929
海上工程船舶综合服务平台 MVP源码













海上工程船舶综合服务平台 —— 项目分析
一、项目定位
这是一个面向海上工程船舶行业的综合业务平台。行业的典型特征是:船舶造价高、作业窗口受海况制约、单船单项目周期长、船员资质受严格监管、跨境作业涉及多币种结算。因此平台要解决的核心问题是"船、项目、人、钱四类资源的匹配与调度"。
平台把业务拆成三条主线:
一是资源台账,即把船东、船舶、船员、公司等静态资源登记成可检索、可核验的档案;二是项目调度,即把船舶和船员按时间窗口分配到具体工程项目上,并防止档期冲突;三是运营监控,即通过位置数据持续掌握船舶在海上的实时动态,并结合维护记录、交易记录形成运营闭环。
从用户角色看,平台预设四类使用者:船东(ship_owner,资源的供给方)、供应商(supplier,设备与服务提供方)、船员(crew_member,被调配的劳动力)、管理员(admin,平台运营方)。平台本身不区分"前台/后台两套账号体系",而是同一套用户表,通过 user_type 字段区分,管理员额外获得后台入口。
二、技术栈与运行方式
后端使用 Python 的 Flask 框架,配合 Flask-SQLAlchemy 做 ORM、Flask-APScheduler 做定时任务调度,数据库为单文件的 SQLite(data.sqlite,位于项目根目录),密码哈希使用 Werkzeug 内置的 generate_password_hash / check_password_hash。依赖清单极简,仅四个包(Flask、Flask-SQLAlchemy、Flask-APScheduler、Werkzeug),没有前端构建工具链。
好处是零构建、可直接跑;代价是前端资产需要自行 vendor 或走 CDN。
运行入口是根目录的 run.py:它把 app/ 加入 sys.path,调用 create_app() 建应用,然后从环境变量读取 HOST(默认 0.0.0.0)和 PORT(默认 5000)启动,debug=False 且开启 threaded=True。此外还有一个 run_dev.py(用 5001 端口)。
需要注意:本项目实际运行的 Flask 装在系统 Python 3.14(C:\Python314\python.exe)下,而非 workbuddy 托管的 Python 3.13(那个环境没装 Flask)。
配置文件 config.py 里的关键项:数据库 URI 可通过 DEV_DATABASE_URL 环境变量覆盖,否则指向项目根目录的 data.sqlite;SECRET_KEY 默认是硬编码的 'hard to guess string'(生产环境必须改);SQLALCHEMY_TRACK_MODIFICATIONS 关闭以省内存;SQLALCHEMY_COMMIT_ON_TEARDOWN 开启;SCHEDULER_API_ENABLED 为真,因此 Flask-APScheduler 会自动暴露 /scheduler/* 系列管理接口。
三、整体架构与目录结构
项目采用 Flask 蓝图(Blueprint)分层,create_app() 工厂函数负责装配。
顶层目录说明:根目录放配置(config.py)、入口(run.py / run_dev.py)、数据库文件(data.sqlite)、数据初始化脚本(init_data.py)、以及若干测试与验证脚本。真正的业务代码全部在 app/ 包内。
app/ 内部按职责划分:
app/__init__.py 是装配中心。它在这里实例化 db 和 scheduler 两个全局扩展对象,然后依次注册四个蓝图:api(前缀 /api)、main(无前缀,占根路径)、admin(前缀 /admin)、crud(前缀 /admin,与 admin 共用前缀但作为独立蓝图)。调度器只在非测试模式且 SCHEDULER_ENABLED 为真时才初始化并启动。
app/models.py 是唯一的数据模型定义文件,包含全部 11 个模型类,是整个平台的数据字典。
app/api/ 是 REST 接口层,每个业务实体一个模块。app/main/ 是前台页面路由(渲染页面 + 处理登录注册)。app/admin/ 是后台,分为 views.py(固定几页:登录、仪表板、用户、交易)和 crud.py(一套通用 CRUD 框架,覆盖全部 11 个实体)。app/templates/ 放 Jinja 模板,app/static/ 放前端资产。
app/db_init.py 与根目录 init_data.py 是两个功能重叠的数据初始化脚本,都会清空并重建样例数据。
四、数据模型(11 张表)
理解这个平台的关键在于理解 models.py。11 张表可以分成四组。
第一组:主体与档案
**users(用户表)**是整个平台的账号中心。字段包括用户名、邮箱(均唯一且必填)、密码哈希、用户类型、所属公司、创建与更新时间、是否激活。user_type 是枚举,只允许 ship_owner(船东)、supplier(供应商)、crew_member(船员)、admin(管理员)四个值,默认船员。模型上挂了 set_password 和 check_password 两个方法封装哈希逻辑,另有三个关联:所属公司、船员档案(一对一)、名下船舶(一对多)。
**companies(公司表)**记录企业主体。字段含名称、唯一注册号、地址、联系人三件套(人/电话/邮箱)、company_type 枚举(shipping 航运、oil_gas 油气、wind_power 风电、service_provider 服务商、equipment_supplier 设备供应商)、许可证文件路径、认证等级、成立日期、是否认证通过。
**crew_profiles(船员档案表)**是平台上字段最多、业务含义最密的表。它以 user_id 外键挂接用户,个人信息含姓名、国籍、出生日期、性别、地址、电话、紧急联系人。专业资质部分是重点:海员证号与有效期、适任证书号(COC)与有效期、基本安全培训证书(BST)与有效期、体检证书与有效期,另有 special_training_certificates 用 JSON 存特殊培训证书列表。工作经历部分含工作年限、偏好职位(JSON)、上次服务船舶、上次离船日期、availability_status 枚举(available 可雇佣、on_board 在船、on_leave 休假、suspended 停职)、当前位置、到岗通知期天数。
这套设计的业务意图很明确:证书到期日和可雇佣状态是船员能否被派工的两个硬约束。
第二组:核心资源
ships(船舶表)是平台最核心的资源实体,字段分四类。身份与识别:船名、IMO 编号(唯一)、MMSI 编号(唯一)、呼号。类型与登记:ship_type 枚举(wind_installation 风电安装、platform_supply 平台供应、heavy_lift 重吊、anchor_handling 拖轮/起锚、dredger 挖泥、subsea_support 水下支持、tanker 油轮、special_purpose 特种)、船旗国、注册港。主尺度与技术参数:总吨位、载重量、总长、型宽、型深、最大吃水、最大航速、定员、燃油舱容、起重能力、是否具备动态定位能力——其中起重能力和动态定位能力是海工船作业能力的决定性指标。营运信息:船东(外键到 users,必填)、营运商(外键到 companies)、status 枚举(operational 营运中、maintenance 维护中、offline 离线、decommissioned 退役)、母港、上次/下次坞修日期、证书到期日、保险到期日。
**ship_equipment(船舶设备表)**登记船上的关键设备,含设备类型、制造商、型号、序列号(唯一)、安装日期、上次与下次维护日期、operational_status 枚举(operational / maintenance_required / out_of_service)、技术规格(JSON)、安装单位、保修到期日。
第三组:业务流转
**projects(工程项目表)**描述一个海上工程任务。字段含项目名、描述、project_type 枚举(offshore_wind 海上风电、oil_gas 油气、subsea_construction 水下施工、pipelay 铺管、well_intervention 修井)、位置坐标、水深、作业海域、客户公司(外键)与承包公司(外键)。时间安排有计划开始/结束与实际开始/结束四个日期;status 枚举为 planning 规划、tendering 招标、ongoing 进行中、completed 完成、cancelled 取消。另有三个 JSON 字段表达需求:required_ship_types(需要的船舶类型)、required_equipment(需要的设备)、crew_requirements(人员要求)。
这三个 JSON 字段加上 crew 的 preferred_positions,就是后续"匹配"逻辑的输入。
ship_assignments(船舶分配表)是项目与船舶之间的连接表,也是整个平台调度逻辑的落点。它把某条船派给某个项目,字段含 assignment_type 枚举(primary 主力、support 辅助、backup 备选)、角色描述、计划起止日期、实际起止日期、assignment_status 枚举(planned 计划、ongoing 进行中、completed 完成、delayed 延误、cancelled 取消)、日租金、总费用、币种(默认 CNY)。
**maintenance_records(维护记录表)**记录船舶或具体设备的维保。含船舶外键、设备外键(可空,表示整船级维护)、maintenance_type 枚举(routine 例行、corrective 修复、preventive 预防、overhaul 大修)、描述、执行人/公司、起止日期、费用、备件(JSON)、status 枚举(planned / in_progress / completed / delayed / cancelled)。
**ship_transactions(船舶交易表)**记录船舶层面的商业行为,transaction_type 枚举覆盖 rental 租赁、lease 承租、purchase 购买、sale 出售、charter 船舶租赁、contract 承包。含船舶外键、买方/承租方(外键到 users)、卖方/出租方、起止日期、金额、币种、status 枚举(pending / confirmed / in_progress / completed / cancelled / disputed 争议),以及合同文件路径、备注、付款条款、保险详情、违约条款五个文本字段。
第四组:监控与资金
**ship_positions(船舶位置表)**是时序数据表,由船载 GPS 或物联网设备写入。字段含船舶外键、纬度、经度、航向、速度、航迹、海拔、时间戳、位置来源(默认 gps)、定位精度。这张表数据量会随时间线性膨胀,是典型的"只增不改"表。
**financial_transactions(财务交易表)**是平台层面的资金流水,与船舶交易区分开。含转出/转入用户外键、金额、币种(默认 USDT)、区块链交易哈希、状态、创建时间。这张表保留了项目早期(似为区块链预约卡类业务)的痕迹——transaction_hash 和 USDT 币种与海工船舶业务并不完全自洽。
五、功能模块划分
5.1 API 层(/api 前缀,共 56 条路由)
API 蓝图在 app/api/__init__.py 中定义,并逐个 try-import 各业务模块(每个都打印一行 DEBUG 日志以便定位导入失败)。实际被导入并注册的模块共 8 个:ships、companies、crews、projects、assignments、positions、maintenance、transactions。蓝图自身另提供两个公共接口。
按业务域梳理接口能力:
船舶域:列表查询(支持 ship_type、status、keyword 三个筛选参数,keyword 会在船名、IMO、呼号上做模糊匹配,分页返回)、单船详情、新建、更新、删除。此外有四个下钻接口:某船的设备清单、位置历史、维护历史、交易历史。
项目域:列表与详情、新建、更新、删除,以及一个按条件检索的 POST /projects/search。还有 GET /projects/<id>/assignments 取该项目的全部派工。
船员域:列表与详情、新建、更新、删除,以及 POST /crews/search 条件检索。
公司域:列表与详情、新建、更新、删除,以及 GET /companies/<id>/users 取该公司下的用户。
派工域:列表、详情、新建、更新、删除。另有反查接口:GET /ships/<id>/assignments(某船的所有派工)与 GET /projects/<id>/assignments(某项目的所有派工)。
位置域:位置列表、单条详情、POST /positions 写入新位置(供设备上报)、GET /positions/latest(全部船舶的最新位置)、GET /ships/<id>/positions/track(某船轨迹)、GET /ships/<id>/current-position(某船当前位置)、GET /positions/area(指定矩形/区域内船舶)。
维护域:列表、详情、新建、更新、删除,以及两个调度看板接口 GET /maintenance/upcoming(未来 30 天内到期的计划维护)与 GET /maintenance/scheduled(所有 planned/in_progress 的维护)。
交易域:/ship-transactions 的列表、详情、新建,以及 /financial-transactions 的列表与新建。
公共域:GET /api/stats 返回船舶数、船员数、项目数、公司数四个平台公开统计;GET /api/search?q= 做跨实体全局搜索,同时在船舶(船名/IMO/呼号)、项目(名称/描述)、船员(用户名/姓/名)上检索,每类最多返回 5 条摘要。
5.2 前台页面层(main 蓝图)
app/main/views.py 提供 9 个路由,全部是无前缀的根路径:
首页 / 渲染落地页,会尝试从 session 取当前用户注入模板;仪表板 /dashboard、船舶管理 /ships、项目管理 /projects、船员管理 /crews、实时监控 /monitoring 这五个业务页都做了登录校验——未登录时 302 跳转(仪表板跳首页,其余四个跳登录页)。
/login 同时承担页面渲染(GET)与登录提交(POST,接收 JSON)。/register 同理,处理注册。/admin 只是个便捷跳转,重定向到 /admin/。/logout 是 GET 请求,清空 session 后回首页。
值得注意的设计特点:前台页面本身只负责渲染骨架,所有数据都由浏览器端 JavaScript 通过 fetch 调用 /api/* 获取。这正是前端模板(dashboard.html、ships.html 等)可以做得比较厚的原因。
5.3 后台管理层(admin 蓝图 + crud 蓝图)
后台有两套并存的东西,需要分清。
第一套是 app/admin/views.py 的固定页面,共 8 个路由。核心是 admin_required 装饰器:先看 session 有没有 user_id,没有就跳 /admin/login;再看该用户的 user_type 是否等于 admin,不是就踢回前台首页。在它的保护下提供了仪表板 /admin/(统计用户数、公司数、船舶数、船员数、项目数、交易数,并列出最新的用户/船舶/项目/交易各 5 条)、用户管理 /admin/users(分页 10 条)、交易记录 /admin/transactions(分页 10 条),以及配套的两个 JSON 接口 /admin/api/users、/admin/api/transactions。这三个页面使用 Element UI + Vue 2 模板(*_element.html),本地 vendor 在 app/static/templates/unpkg/。
第二套是 app/admin/crud.py 的通用 CRUD 框架,这是本项目最有工程价值的设计。它用一个 ENTITY_REGISTRY 字典声明式地注册了全部 11 个实体(users、companies、ships、equipment、crews、projects、assignments、positions、maintenance、ship_transactions、financial_transactions),每个实体只需给出模型类和中文名。
框架的核心是 build_entity_config(slug):它会反射模型的表结构,自动把每一列分类成 pk / fk / bool / enum / datetime / date / json / int / float / textarea / text 等类型,据此决定前端用什么控件渲染(下拉、开关、日期选择器、数字框、JSON 文本域),并算出该列是否进列表、是否进表单、是否必填、列宽多少。外键列会自动去目标表取标签(FK_LABEL_FIELDS 定义了每张表用哪个字段做显示名,如 users 用 username、companies 用 name、crew_profiles 用姓+名),生成下拉选项。
配套的 populate() 负责把前端提交的 JSON 写回模型,并按列类型做转换与校验:枚举值必须在允许集合内(否则报错并跳过)、日期按格式解析、JSON 文本尝试 json.loads、数值做 int/float 转换。serialize_row() 反向把模型序列化成 JSON,其中 SERIALIZE_HIDDEN 会过滤掉密码哈希等敏感列。
于是三个路由加上五个 API 就支撑起 11 个实体的完整增删改查:/admin/entities 是数据管理入口页(卡片式导航),/admin/entities/<slug> 是某个实体的列表+表单页,/admin/api/crud/<slug> 的 GET 支持分页与关键字模糊搜索(自动在模型所有字符串/文本列上做 OR LIKE),POST 新建,/admin/api/crud/<slug>/<pk> 的 GET/PUT/DELETE 做详情、更新、删除。
这套框架还包含若干业务约定:FORM_HIDDEN 把 password_hash、created_at、updated_at 排除出表单;针对 users 实体额外注入了一个虚拟列 password(仅出现在表单、不回显),新建时必填且长度不少于 6 位并调用 User.set_password() 哈希存储,编辑时留空表示不修改密码——这是解决 password_hash 为 NOT NULL 却不可编辑这一矛盾的方案。
5.4 定时任务层
app/__init__.py 会在启动时初始化并启动 Flask-APScheduler,Flask-APScheduler 自带的管理 API 会自动暴露在 /scheduler 下(含 /scheduler/jobs、/scheduler/pause、/scheduler/resume、/scheduler/run 等)。
但需要点明一个现状:当前没有任何定时任务被真正注册。代码库里存在 app/api/schedules.py,里面定义了 check_match_deadlines(检查匹配截止)与 execute_matching(执行匹配算法)两个任务函数以及对应的包装函数,可这个模块没有被任何地方 import,因此不会执行。整个调度层目前是"空转"状态。
六、核心业务逻辑要点
平台里真正体现业务规则的地方,集中在以下几处。
一是派工的时间冲突检测,这是调度功能的核心。POST /api/assignments 在创建派工前,会先校验 project 和 ship 是否存在(不存在返回 404),然后做冲突检测:查询该船所有 assignment_status 既不是 cancelled 也不是 completed 的派工记录,再用 planned_from <= 新计划的 planned_to AND planned_to >= 新计划的 planned_from 做区间重叠判断。只要存在重叠,就拒绝创建并返回 "该船在此时段已被分配到其他项目"。这个"排除已取消/已完成 + 区间相交"的写法是排期系统的标准做法。
二是船员的多条件检索。POST /api/crews/search 支持按特殊培训证书、可雇佣状态(可多选)、当前位置、最低工作年限、最长通知期、偏好职位等条件组合筛选,查询以 CrewProfile 关联 User 为基表,逐条件叠加 filter。代码注释也坦承证书匹配用的是简化的 JSON 包含判断,实际生产需要更复杂的资质比对。
三是项目条件检索。POST /api/projects/search 与之对称,用于按项目类型、状态、船舶类型需求等条件找项目。
四是维护预警。GET /api/maintenance/upcoming 以"当前日期 + 30 天"为界,筛出状态为 planned 且开始日期不晚于该界限的记录,按开始日期升序返回——这是维保到期的预警看板;/api/maintenance/scheduled 则放宽到 planned 与 in_progress 两种状态,作为整体维护计划视图。
五是实时位置的最新值查询。GET /api/positions/latest 需要解决"每船只取最新一条"的经典问题,实现方式是先按 ship_id 分组求 max(timestamp) 生成子查询,再与主表按 ship_id 和时间戳双条件 join 回原表。这是标准且正确的写法。轨迹接口则按船舶取历史点序列用于地图绘制。
六是全局搜索。GET /api/search 一个关键字同时打三张表,返回结构化的三类摘要(船舶给 id/name/imo/type,项目给 id/name/type/status,船员给 id/name/user),供前端顶栏的下拉搜索面板使用。
七是鉴权与会话。前台登录校验直接落在 main 蓝图:User.query.filter_by(username=...) 取出用户后调用 check_password 校验哈希,通过则把 session['user_id'] 写入。后台的 admin_required 在此基础上叠加 user_type == 'admin' 的角色判断。注册接口 /register 会校验用户名与邮箱唯一性,然后建 User 并 set_password 落库。
八是通用 CRUD 的类型安全写入。前面提到的 populate() 是防止脏数据入库的唯一关口,它会拒绝非法枚举值、解析日期与 JSON、做数值转换。反过来说,任何绕过 API 直接改数据库的操作,都可能写入非法枚举值,导致 SQLAlchemy 在加载整行时抛 LookupError,进而拖垮所有相关查询——这一点在项目历史上确实发生过。
七、前端实现与交互
前台采用一套自建的设计系统,不走任何前端框架。核心三件套是:app/static/css/platform.css(海洋主题、玻璃拟态风格的样式系统)、app/static/js/platform.js(共享工具库)、app/templates/base.html(统一布局,左侧边栏 + 顶部栏)。
platform.js 暴露了一个 App 全局对象,封装了 api/get/post/put/del 等 fetch 封装、toast 轻提示、confirm 确认框、statusBadge 状态徽章映射、renderPager 分页渲染、登出、侧边栏高亮等常用能力。各业务页面继承 base.html 后,只需关注自己的数据加载与渲染逻辑,交互风格天然统一。
侧边栏导航共 6 项:首页、仪表板、船舶管理、项目管理、船员管理、实时监控(管理员额外看到"管理后台"入口)。顶部栏提供全局搜索(带分组下拉结果)与用户区(未登录显示登录按钮,已登录显示用户名与登出图标)。
各业务页面的功能:仪表板聚合统计卡片、最近船舶/项目/派工列表、项目状态分布;船舶管理提供列表、搜索、按类型/状态筛选、分页,以及详情弹窗(展示规格、设备、维护、交易四个维度)和增删改表单;项目管理提供列表、筛选、详情弹窗(含该项目的派工列表)与增删改;船员管理提供列表、筛选、资质证书详情(证书过期会高亮)与增删改;实时监控页面用 Leaflet 加载地图,展示全部船舶的最新位置、支持查看某船轨迹回放,并以 15 秒间隔自动刷新。
视觉资产方面,站点 favicon、品牌 logo、落地页英雄主视觉、登录页背景四张图都是通过图像生成能力真实产出的位图,放在 app/static/images/;功能性 UI 图标(导航、按钮、状态)则统一使用矢量图标字体。
后台页面使用 Element UI + Vue 2,采用经典的 el-container / el-aside / el-main 布局。这里存在一个贯穿后台模板的技术约束:由于模板同时被 Jinja 和 Vue 处理,凡是 Jinja 需要渲染的变量(如 {{ label }}、{{ slug }}、{{ config_json|tojson }})必须保留在 {{ }} 中,而所有 Vue 表达式(如 {{ e.label }}、{{ scope.row.x }})必须包进 {% raw %}...{% endraw %}。漏包会直接导致 Jinja 求值未定义变量抛 UndefinedError(页面 500),或渲染出非法 JavaScript 导致 Vue 挂载失败、界面错位。
八、总结
这是一套架构骨架完整、数据模型专业、但业务实现深浅不均的 Flask 单体应用。
它的长处在于:数据模型对海工船舶行业的刻画相当到位(船型分类、主尺度与技术参数、船员四类证书与到期日、派工的时间窗口与费率、项目对船型/设备/人员的需求表达),说明建模时是照着真实业务来的;通用 CRUD 框架的设计也很聪明,用模型反射把 11 个实体的增删改查压成一份配置加三个路由,显著降低了后续扩展成本;前台自建的设计系统与共享 JS 工具让页面风格统一、维护简单。
它的短板在于:一半的精力沉淀在了与当前业务无关的历史遗留上(预约卡/匹配/区块链交易那套),API 层缺乏鉴权,定时任务层空转,船员、设备、维护、交易四个业务域有接口无数据、有页面无内容,船员与项目的智能匹配这一真正有业务价值的能力尚未落地。
如果要继续推进,优先级最高的三件事是:给 /api/* 补上会话或令牌鉴权并收紧注册接口的角色选择;把空转的定时任务层用起来,实现船舶证书/保险到期、设备维保到期、船员证书到期的主动预警;补齐船员、设备、维护、交易四个业务域的样例数据与完整交互,让项目从"骨架"变成"可演示的完整系统"。