Files
docs/国内货柜履约/船期日历功能规划.md
T

673 lines
67 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 船期日历功能规划
> 文档状态:首版开发规划;2026-09-27 复核补充了数据闭环规则。本文是开发依据,不是代码完成或上线验收证明。
> 规划对象:Admin「国内头程 → 船期管理」独立船期日历。
> 目标:支持人工维护渠道和船期规律,按周或单次生成具体船期,并按渠道/船期查看收货体积。
> **当前展示版范围已重新冻结:只做船期维护和只读展示,不做自动归班、不消费预报、不改写收货审核、不自动改绑,也不改变现有智能排柜流程。** 首版只需要让管理人员看到每个航次的已收/预计货量、可用容量、按柜型初算的建议柜数、已订柜和待补柜,并据此人工判断是否合并到其他船。后续若要增加货物归班,再单独立项评审。第 15.8 节覆盖本文此前关于自动归班和异常改配的旧口径。
> **展示版统计口径**:日历仅展示上海时区今天及以后开船的计划。未离仓审核实测货量优先保留有效订舱和排柜对应的船期,未安排或原安排失效的余量按“收货仓 + 渠道”展示在最近一班尚未截仓的有效船期;各船期继续展示自身起运港和目的港。顺延只改变日历货量和订柜参考,不创建或变更船期归班、订舱或排柜事实。订柜数按统一柜型容量向上取整,已离仓柜的容量不再抵扣余货需求,最终以现有智能排柜和人工确认结果为准,详细规则以第 15.8 节为准。
## 1. 输入来源与现状边界
本规划以当前需求描述为业务输入,以用户提供的截图作为页面布局参考。截图中的红框、日期、船名、航次、数量和示例数据均不构成额外指令,也不代表需要沿用的真实业务数据。
仓库当前已有 `apps/admin-web/src/modules/port-schedules/` 和 Firstleg 的 `portschedule` 服务。现有“港务船期”记录绑定客户、订舱或柜次,主要保存船公司/码头/API 的船期观测、过期时间和海运节点查询结果;对应表为 `fl_port_schedule`。它服务于已存在的订舱和货柜履约链路,不直接承载本规划的排班模板、周循环生成或收货量统计。现有 `port-schedules/routes.ts` 也不是本规划新页面的正式入口;新日历应作为独立 Admin 页面、路由和权限接入,不能把现有隐藏/嵌入式船期组件直接改成排班管理页。
本功能建议新增“船期计划”域,与现有港务船期并列:
- 船期计划是运营人员提前安排“哪个渠道哪天开哪班船”,可以没有订舱和真实柜次;价格簿中的参考船期、最晚进仓和参考时效也不能直接当作该计划的实际开船事实。
- 港务船期是某个订舱/柜次的外部船期事实或人工观测,继续按现有接口和状态机运行。
- V1 先建立“计划实例 ↔ RELEASED 订舱”的最小关联用于订柜统计;真实柜次、港务船期和海运节点的完整关联后置,不能用计划数据覆盖已产生的运输事实。
本规划默认这是平台内部运营日历:渠道主数据平台级维护,模板和实例至少按收货仓隔离,实收汇总按“收货仓 + 渠道 + 收货日期”聚合并对平台运营人员可见。V1 不按客户拆分船期计划;如果实际业务存在客户专属船期或价格承诺,必须在开发前把客户作为模板、实例、查询和权限的正式维度,不能在上线后再用名称筛选补救。
## 2. 要解决的问题
运营人员需要在一个页面完成以下工作:
1. 维护当前可售或可安排的运输渠道,例如“MATSON / 美森”。
2. 配置固定每周哪几天有船,也可以只添加一班临时船期。
3. 根据开船时间和渠道的提前收货天数,自动算出收货截止时间。
4. 在月历上看到每天有几班船,以及每个渠道已收多少立方米、预计有多少货、容量还剩多少和建议订几个柜。
5. 对已生成的具体船期进行编辑、取消、复制和查看关联货量,并保留变更历史。
## 3. 建议的 V1 范围
### 3.1 本轮纳入
- 船期渠道的新增、编辑、启用/停用。
- 一次性船期的手工添加。
- 每周模板的配置,按指定周几生成未来具体船期。
- 每条模板/实例必填收货仓;首版不使用“全部仓”这种模糊范围。
- 开船时间、预计到港时间、收货截止时间和容量的维护与计算。
- 月历视图、月份切换、按收货仓/起运港/渠道/船名航次筛选。
- 选中日期的船期详情和渠道体积汇总。
- 船期实例的人工编辑、取消、复制下一班和变更历史。
- 货量按收货仓与渠道只读展示已收实测体积、预计体积、截止前参考量、截止后风险量、已订柜和待补柜;不产生归班事实、不提供货量改绑。
- 按现有柜型目录计算只读的“建议订柜数”,不直接创建正式订舱或货柜。
### 3.2 暂不纳入
- 船公司官网或第三方 API 自动抓取船期。
- 自动订舱、舱位分配、放舱、拖车、报关和实际出仓状态推进。
- 自动归班、预报消费、实收批次绑定、取消后自动改配和异常重新安排。
- 在收货审核、测量审核或智能排柜流程中增加回调、写入或阻断。
- 复杂的价格计算、客户报价和财务结算。
- 以计划日历替代现有订舱/柜次船期事实。
- 在没有业务确认前自动修改历史入仓数据或批量回溯全部历史货量。
- 复杂节假日规则、跨时区排班和通知中心;这些作为后续扩展点保留。
- 容量预占、超容强制处理、正常货量的批量改绑和跨客户拆分;异常货的最小重新安排按第 6.10 节纳入 V1。
## 4. 核心概念与生命周期
| 对象 | 作用 | 关键生命周期 |
| --- | --- | --- |
| 渠道 Channel | 表示一类运输服务,如 MATSON / 美森 | `ACTIVE`、`INACTIVE` |
| 船期模板 Schedule Template | 描述每周固定规律;固定添加一班直接创建实例,不再套一层单次模板 | `DRAFT`、`ACTIVE`、`INACTIVE` |
| 船期实例 Sailing | 某一具体日期的某一班船,是日历和货量统计的主体 | `DRAFT`、`OPEN`、`CLOSED`、`DEPARTED`、`CANCELLED`、`COMPLETED` |
| 归班事实 Sailing Assignment | 保存预计量/收货批次与船期的自动归班结果及历史 | `FORECAST_MATCHED`、`RECEIVED`、`LATE`、`UNMATCHED`、`SUPERSEDED` |
| 订舱关联 Sailing Booking Link | 将已放舱订舱的柜型需求关联到计划船期,用于订柜建议 | `ACTIVE`、`REVOKED` |
模板生成实例后,实例保存自己的船名、航次、港口、截仓时间和容量快照。以后修改模板不回写已经生成、已编辑或已关联货量的实例;需要调整时由运营人员编辑实例或重新生成未落地的未来实例。
## 5. 页面信息架构
### 5.1 顶部筛选和操作区
- 月份:默认当前月,提供上月、下月和“今天”。
- 收货仓:按仓库筛选;船期计划至少要绑定一个收货仓,不能只靠渠道和起运港推断。
- 出发港:按港口代码和名称筛选。
- 船期渠道:按启用渠道筛选,停用渠道仍可用于查询历史。
- 船名/航次:模糊搜索。
- 状态:全部、可收货、已截止、已取消、已开船、已完成。
- 归班状态:全部、预计可赶船、已收、晚到、未安排;用于把“漏排或赶不上”的货量变成运营待办。
- 操作:`新增船期`、`渠道与模板管理`。
另设“待安排/异常货量”入口,集中展示当前权限和仓库/渠道范围内仍待处理的货物,显示实际收货日或预计到仓日、等待天数及异常原因。未安排记录没有开船日期,不强行放进某个开船月,也不混入月历主合计;切换月份不使这些待办消失。
筛选条件应由服务端参与查询,不能先拉取整月全部记录后在浏览器本地拼装统计。
### 5.2 月历区
- 周一至周日为列,显示跨月日期但以浅色区分。
- 每个日期格显示当天船期数量和航线摘要。
- 摘要优先显示渠道名和收货体积,例如“美森 120 m³ / 200 m³”;渠道较多时显示前几项并提供“更多”。
- 当前日期、选中日期、已截止日期、容量超限日期分别使用清晰但不依赖颜色的标签/图标表达。
- 点击日期后,右侧详情区同步刷新;点击船期卡片可直接进入实例详情。
### 5.3 选中日期详情区
每个船期卡片至少展示:
- 渠道、船名、航次和船期业务编号。
- 起运港 → 目的港。
- 预计开船、预计到港、最终收货截止时间。
- 已收总量、本船可赶实收、预计可赶、晚到量、已订柜数、建议订柜数和待补柜数;各指标按第 6.6 节计算,已收总量不能直接当作可赶量。班次容量上限和剩余体积仅在已配置班次容量时展示,V1 不因容量不足自动改绑或强制拦截。
- 当前状态和风险提示:已过截止、接近满舱、超过容量、已取消。
- `编辑`、`复制为下一班`、`取消船期`、`查看货量明细`。
删除仅适用于未开船、未关联任何货量的草稿/实例;有货量或已产生履约事实的实例只能取消,不能物理删除。
### 5.4 新增/编辑弹窗
弹窗提供两个明确入口:
**按周添加**
- 选择渠道、起运港、目的港。
- 选择收货仓;同一渠道可按不同收货仓配置不同截止规则。
- 选择一个或多个周几。
- 选择开船时间、航程天数或预计到港时间。
- 设置提前收货天数、有效起止日期和默认容量。
- 设置船名固定值或船名规则;设置航次生成规则。
- 选择未来生成范围(建议 4 / 8 / 12 周)。
- 先预览将生成的实例,再确认批量生成。
**固定添加一个**
- 直接填写开船日期时间、船名、航次、起运港、目的港。
- 填写或确认收货截止时间、预计到港时间、容量。
- 保存为一条独立实例,可选关联一个模板作为来源。
周模板中的船名是生成实例的默认值;船司确认后可逐班编辑船名、航次和截止时间。当前展示版编辑实例时保留已保存的绝对截止时间,切换到“按开船前 N 天”后才重新计算;规则快照和覆盖原因待后端契约补齐后再做完整留痕。
### 5.5 区间汇总
月历上方建议提供当前筛选范围的渠道汇总卡:已收实测 CBM、预计可赶 CBM、风险/未安排 CBM、已订柜数、建议订柜数、待补柜数、晚到批次数和船期数量。V1 卡片固定标明“按开船月”,收货月仅在详情和后续报表中使用;多班同渠道同日时仍按船期实例分开,不能只显示一个渠道总数。
## 6. 业务规则
### 6.1 收货截止时间
首版把“收货截止时间”正式命名为**最晚到仓时间 `warehouseCutoffAt`**,表示货物最晚什么时候到达我司收货仓,和船公司的截单、截关、截港时间分开保存。默认公式:
```text
最晚到仓日期 = 预计开船本地日期 - 提前收货天数
最晚到仓时间 = 收货仓截收时刻
```
- 计算使用仓库业务时区,V1 默认 `Asia/Shanghai`;接口统一传 ISO 8601 时间。
- 计划截止计算只基于实例保存的计划 `etdAt`;后续实际开船时间属于订舱/船节点事实,不反向改写已经产生的收货归班。
- “提前 N 天”首版固定按自然日计算;工作日和节假日规则放到后续工作日历阶段。
- 若收货仓暂未配置截收时刻,首版按 18:00 默认并记录规则来源,不能由前端使用浏览器本地时刻隐式补齐。
- 首版默认使用 `3` 个自然日和 `18:00`;这个默认值不是从截图推断的,而是初始运营参数,后续按试运行数据校准。
- 单班页面允许选择提前 2/3/4 个自然日或自定义本班截止时间;当前展示版保存计算后的最终 `warehouseCutoffAt`,规则快照与覆盖原因审计字段待后端契约补齐后再启用,页面不宣称已完成服务端留痕。
- 最终截止时间必须严格早于开船时间;开船时间、截止时间和容量的校验由服务端最终执行。
- 时间边界统一到毫秒:实际/预计到仓时间等于截止时间视为准时;当前时间大于截止时间才停止新归班,不能由不同页面采用不同的 `<`、`<=` 判断。建议值由服务端计算并返回,前端只展示。
- 是否跳过周末/节假日、节假日向前顺延几天,需要在产品确认后接入工作日历,不能由前端自行猜测。
### 6.2 每周模板生成
- 按模板有效期和周几生成具体日期,跨月日期仍属于实例,查询某月时按开船时间落入月份返回。
- V1 采用“预览后手工确认生成”,不做后台滚动生成;页面默认未来 8 周,并提供未来 4/8/12 周快捷范围,生成范围由操作人确认后提交。
- 生成请求必须幂等;同一模板、日期、航次规则只能生成一条有效实例。
- 已编辑、已关联货量、已关闭、已开船或已完成的实例不被重新生成覆盖。
- 发现相同日期/航次冲突时返回冲突清单,让用户选择跳过或进入实例处理,不能静默覆盖。
- 模板停用只阻止未来生成,不删除历史实例。
- V1 如遇某周停航或临时改船,允许在生成预览中跳过日期或生成后取消实例;节假日批量例外规则放到后续阶段。
- 去重和冲突检查必须跨模板执行,至少按“收货仓 + 渠道代码 + ETD 本地日期时间 + 航次”判断,不能只用 `template_id + date` 作为唯一键。
- 同一收货仓、渠道、起运港和目的港下,若多班船的 ETD 相同,V1 按不可变的 `sailing_no` 升序确定唯一归班;复杂的运营优先级后置。
### 6.3 航次号和船名
V1 支持固定值和“前缀 + ETD 本地日期”两种航次生成规则;每个生成实例允许人工覆盖航次。顺序号和按船名递增规则后置,避免首版因船公司编号规则不同而误生成正式航次。
业务唯一性首版约束“收货仓 + 渠道代码 + 起运港 + 目的港 + ETD 本地日期时间 + 航次”;同一航次跨不同起运港可以存在,但不能在同一组合内重复。`sailing_no` 在实例创建时生成后保持不变,用于同一 ETD 的确定性排序;展示给用户的业务编号应为船期编号、船名和航次,不能回退显示数据库 ID。
### 6.4 可赶船判断和人工改绑
默认推荐某一入仓记录时,只允许候选实例同时满足:
- 渠道、起运港和目的港匹配。
- 状态为 `OPEN`,且当前时间未超过最终收货截止时间。
- 后续启用容量控制时再校验未超过容量;V1 只展示已配置容量和超量提示,不因容量字段缺失阻断归班。无上限实例可明确标记为“不限舱位”。
- 按“最早可开船”或产品确认的优先规则排序。
超过截止时间时不能自动分配到该船期;容量不足在 V1 只显示明确预警,不阻断自动归班,也不能把预警状态伪装成已锁定舱位。任何已关闭、已取消或已开船的船期都不能作为新的自动归班候选。人工改绑必须记录操作者、原船期、新船期、时间和原因。
建议把“预计归班”和“实际收货统计”分成两条事实:
- 预计归班可使用入仓单的 `expectedArrivalAt`,只有预计到仓时间早于或等于最终截止时间,才标记为“预计可赶船”。没有预计到仓时间的记录进入“待确认”,不能凭渠道名称或创建时间猜测。
- 实际统计使用已确认的实际收货时间和审核后的实测体积。已有计划归班的货实际到仓晚于截止时,保留原计划并标记“晚到风险”,不能自动塞到下一班。首次形成实收事实且从未归班的货,若仓库、正式渠道和航线明确,可首次自动选择仍开放且未过截止的最早匹配船期,同时校验实际到仓时间不晚于该船截止;无候选或资料不全则写入“未安排”。因取消、晚到而进入未安排的货不属于“从未归班”,须走第 6.10 节。
- 同一收货批次只能有一个有效计划归班。自动匹配以收货仓、渠道、港口和最早可赶上的有效实例为条件;同一时间存在多班时,必须有固定排序规则。
- 归班在入仓计划审核、预计到仓时间/渠道/仓库变化或实际收货确认时重新计算;船期取消时只新增一条 `UNMATCHED` 变更记录并标记待人工处理,不自动塞入下一班。船期仍为 `OPEN` 且尚未达到最终收货截止时间、没有锁班/订舱事实时允许动态重算。船期达到截止时间后冻结当前归班,即使实例仍显示为 `OPEN` 也不再自动换到下一班;截止前尚未匹配的对象才继续寻找后续可赶船。运营确认订舱、船期进入 `CLOSED` 或船期取消后的改配必须产生新的人工或明确改配记录。
建议的状态落库规则是:预计匹配但尚未收货为 `FORECAST_MATCHED`;实际收货时间不晚于截止且有审核实测体积为 `RECEIVED`;实际收货晚于截止仍沿用原计划为 `LATE`;没有可解释的计划归班为 `UNMATCHED`;重新计算后被新结果替代的旧记录为 `SUPERSEDED`。这些状态首版必须持久化,详情中追溯匹配规则、时间、来源和被替代关系。
首版自动归班采用“写入事实 + 版本化重算”:
- 预计匹配、实际收货、晚到和未安排都写入归班事实,不能只在查询时临时计算。
- 船期仍为 `OPEN` 且未锁班时,重新计算不覆盖旧记录,而是将旧记录标记 `SUPERSEDED` 并写入新版本。
- 船期进入 `CLOSED` 或已确认订舱后,原归班结果保留;取消、甩柜或改配只新增变更记录,不静默改写历史。
- V1 仅开放第 6.10 节的异常重新安排,来源记录为 `MANUAL`;正常货量的批量改绑、拆分分配和强制赶船后置。
- 冻结的是船期归属,不是收货事实的数值。冻结后最终审核、复测、冲正仍更新事实版本及其统计值;原归属保留,不因数值变化自动换船。
### 6.5 货量统计
V1 建议至少将“已收体积”和“预计体积”分开;“预占体积”只有在确认需要锁定容量后再启用:
- **已收体积**:入仓事实已达到产品确认的可计入状态,并有有效体积。
- **预计体积**:按预计归班的未收货量单独展示,不直接计入“已收体积”,也不自动扣减容量,除非产品确认要按预计量锁舱。
- **预占体积**:已明确锁定该船期但尚未形成收货事实的货量;V1 若不做预占则固定为 0。
- **剩余体积**:配置容量上限时按 `容量上限 - 已收体积 - 预占体积` 计算;V1 未启用预占时预占值为 0,预计体积不扣减剩余容量;结果不得小于 0,无上限显示“—”。
体积字段必须统一单位为 m³,并保存统计快照时间。V1 只统计明确达到“已收货”状态、且有正数 `actualVolumeCbm` 的收货批次;实测体积是后续作业的权威输入,申报体积仅作对比,计费体积不能冒充“已收体积”。当前 `inboundedAt` 会在多个入库流程节点写入,不能单独视为最终收货事实;V1 必须新增或正式暴露稳定的收货批次事实,不能把 `inboundedAt` 作为替代。取消、草稿、作废或不符合统计口径的记录不计入已收体积。拆批、复测和冲正均按收货批次版本守恒。
### 6.5.1 V1 数据来源契约(必须一并实现)
当前入仓模型已经有 `warehouseId`、`channelCode`、`expectedArrivalAt`、`actualVolumeCbm` 和测量审核状态,但没有稳定的收货批次 ID、实际收货时间和可靠的未收预计 CBM;现有 `originalVolumeCbm` 是客户申报值,不能作为预计可赶体积。首版必须补齐下面两类事实,不能在页面层临时拼接:
| 事实 | V1 必须提供的字段 | 统计和归班规则 |
| --- | --- | --- |
| 收货批次事实 `ReceiptBatchFact` | `receiptBatchId`、`inboundId`、`receivingWarehouseId`、`channelCode`、起运港/目的港快照、`receivedAt`、`actualVolumeCbm`、最终测量批次 ID、审核状态、冲正/作废状态 | 仅 `RECEIVED/APPROVED` 且实测 CBM 大于 0 的批次计入已收;复测替换同一批次最新有效值,冲正/作废不重复计入 |
| 未收货量预报 `InboundVolumeForecast` | `forecastId`、`inboundId` 或可收货批次键、收货仓/渠道/起运港/目的港、`expectedArrivalAt`、`forecastVolumeCbm`、来源、有效状态、版本和更新时间 | 只有有效状态、预计体积大于 0 且有 ETA 的预报可计入预计可赶量;不得使用 `originalVolumeCbm`、申报箱数或计费体积代替 |
补充规则:
- 当前系统没有上述稳定事实的部分,V1 必须由 Firstleg 提供正式的收货批次和货量预报服务事实;页面不直接读取入仓单分页拼装。没有可靠预报时允许只显示已收和风险,但应显示“未提供有效预报”及缺资料票数,不能把未知量写成“预计无货”。数据生产入口按第 6.5.2 节落地。
- 预报和实收按明确的预报批次关联,不以同一 `inboundId` 就认定整票预报已收完。实收只替换其明确对应的预计批次;未到货部分保留独立有效预报,禁止用实测 CBM 直接减整票预计 CBM。具体拆批与复测规则见第 6.5.2 节。
- 航线匹配优先使用入仓/预报事实中的起运港和目的港;事实缺失时,仅当启用渠道存在唯一的默认起运港/目的港映射,才使用该映射并保存 `route_source=CHANNEL_DEFAULT`。渠道没有唯一默认航线时进入 `UNMATCHED`/风险,不按目的仓名称或自由文本猜测。
- 一票分批到仓时按批次统计;复测、冲正和作废必须以版本或状态替换旧值,并保留审计历史。
- 收货仓、渠道或航线缺失时,批次/预报进入 `UNMATCHED` 或风险清单,不得按渠道名称、订单创建时间或目的地名称猜测。
日历汇总应与右侧详情使用同一服务端统计口径,避免页面格子和详情卡片出现不同数字。建议由服务端按收货仓、渠道、船期、收货日聚合后一次返回;前端不得跨分页拉取入仓单再自行求和。超过容量时返回明确的 `overCapacity=true`,前端只负责展示。月历按开船日期展示班次,详情中应能展开实际收货日期和未安排/晚到记录,避免把“收货发生日”和“船期开船日”混为一个日期。
日期维度固定为两种:月历落格使用 `etdAt` 的本地日期;已收体积明细保留收货批次的 `receivedAt` 日期。V1 月历默认按“开船月”汇总,并将归属该船期的已收实测体积展示在对应开船日;收货月只用于详情展开和后续报表,不参与首版月历主合计。没有稳定 `receivedAt` 的批次只能进入风险/未安排,不能回退使用 `inboundedAt`。月历主合计排除已取消实例;取消实例仍可查历史快照,当前货量进入独立异常待办,重新安排后只计入目标实例,不能在两班重复相加。
### 6.5.2 事实产生、分批收货与预报替换
- **实际收货来源**:仓库完成一批到货确认时生成稳定收货批次业务键,记录真实 `receivedAt`、仓库和对应箱/件范围;最终测量审核通过后,该批次的实测体积才能计入已收。到货确认和最终审核是两个独立条件,审核操作时间不能冒充实际到仓时间。到货待审核单列票数;体积未知显示“待测量”,不补成 0 m³。
- **未收预报来源**:V1 推荐由运营在入仓详情的“货量预报”区域维护预计批次、预计到仓时间、预计 CBM 和航线,Firstleg 保存来源、录入人及版本;更新/作废预报沿用入仓写权限,服务端核验仓库范围。日历消费这些正式事实。客户申报值可作录入参考,但不得未经运营确认直接成为有效预报。
- **分批规则**:一条有效预报表示一批可对应收货的货物;部分到仓前或确认到仓时,将其拆成“本次到货预报部分”和“剩余未到货部分”,子批次保留父预报键、各自预计 CBM 和预计到仓时间。拆分必须显式确认且预计量合计等于拆分前预计量;修订预计总量则另存一个有原因的新版本。
- **替换规则**:本次到货批次一经确认,就停止把该部分计为未收预报;尚未测量审核时归入“已到待审核”,审核通过后用该批的实际体积替换。剩余批次继续按自己的预计体积统计。复测只更新对应实收批次,不能改变其他未到货批次的预计量。
- **无法对应**:实收与预报无法确认对应关系时,相关预计量暂列“待核对”,不与可能重复的实收量一起计入可赶总量;展示缺口原因,运营补齐关系后再恢复。不得凭实测体积相近自动抵消。
- **首版批次关联入口**:审核事件若能携带 `SAILING_FORECAST:<forecastKey>`,服务端只消费该预报批次;多条有效预报且事件没有明确批次键时,实收保持 `UNMATCHED`/待核对,不自动按相近体积抵销。运营可通过预报维护接口显式绑定实收批次、修订预报或作废预报,所有动作均追加版本并保留原记录。
- **事件与版本**:到货、审核、预报修订/拆分、冲正和作废均带稳定业务键及版本;同一事件重复到达不重复记量。来源版本更新与归班更新必须具备重试及对账机制,返回事实版本、归班版本和计算时间,不能只声明建了四张表就认为数据链路完成。
示例:整票预计 100 m³,先拆成本批预计 40、后批预计 60。本批实际到货审核后为 45,则“已收 45 + 未收预计 60 = 可赶 105”;复测为 43 后变成 103。不能用 `100 - 45` 把后批预计改成 55。
### 6.6 订柜数量建议
订柜数量是只读的运营建议,不是正式订舱或货柜创建。首版按以下口径展示:
```text
本船可赶实收 CBM = 当前有效归班为 RECEIVED 的审核实测 CBM
可赶总 CBM = 本船可赶实收 CBM + 有效未收预计可赶 CBM
已收口径建议柜数 = 向上取整(本船可赶实收 CBM / 规划装载容量)
建议订柜数 = 向上取整(可赶总 CBM / 规划装载容量)
已订规划容量 = 求和(每个有效订舱需求的柜数 × 该柜型规划装载容量)
待补 CBM = max(0, 可赶总 CBM - 已订规划容量)
待补柜数(某柜型方案) = 向上取整(待补 CBM / 该方案柜型规划装载容量)
```
- “有效预计 CBM”只取有明确预计到仓时间、渠道/仓库/航线匹配且不晚于最晚到仓时间的有效入仓计划;没有 ETA 的货列为风险,不直接导致多订柜。
- `LATE` 仍保留在该船历史和已收总量中,但不计入本船可赶实收或订柜建议。已收总量与可赶量是不同指标,不要求相等。
- 超过截止但尚无到货确认的原预报,保留原归属并标为“到仓待核实”,退出有效预计可赶量;实际到货事实到达后依据真实到仓时间判为准时或晚到。不得仅凭“当前时间已过截止”把所有未审核批次判成晚到。
- `规划装载容量`、柜型、重量和装载约束复用现有统一柜型目录和智能排柜规则,不在船期日历中另写一套容量或利用率。未选择柜型时,按 40GP/40HQ/45HQ 分别展示方案,不能给出无条件的单一柜数。
- 已订柜型混合时,展示各柜型数量及合计柜数,先按各自规划容量折算已订容量再计算待补量;不能拿“已订 1 个小柜”直接抵掉“建议 1 个大柜”。柜型容量缺失时待补数显示“待核实”,不默认为 0。
- 班次容量上限只触发超量预警,不用它截断可赶总量。例如可赶 150 m³、班次上限 100 m³,建议仍按 150 m³ 计算,并标明超出 50 m³;不能少显示所需柜数。
- 各柜型方案表示“如果待补量全部采用该柜型”的体积初算,未执行重量、混装及实际摆放优化;页面不用“必须订柜数”表示已经确认的订舱指令。
- 页面必须标注“按体积初算,最终以智能排柜/人工确认结果为准”;重量、箱数、货物混装限制或数据缺失会使结果进入风险提示。
- 已订柜数只读取 V1 计划船期-订舱关联下的有效 `RELEASED` 订舱需求;日历只保存建议计算的输入、规则版本、计算时间和结果快照,不直接生成正式柜次。
### 6.7 容量提示
- 0%~80%:正常。
- 80%~100%:接近满舱提示。
- 大于 100%:超容量提示;V1 不自动阻断只读汇总,也不提供绕过容量的强制关联。异常重新安排仍须满足目标船期状态、截止时间和履约锁定校验。
- 无容量上限:展示“—”或“不限”,不参与百分比计算。
### 6.8 船期实例状态转换
建议只允许以下状态转换:
```text
DRAFT → OPEN → CLOSED → DEPARTED → COMPLETED
└──────────────→ CANCELLED
OPEN → CANCELLED
CLOSED → CANCELLED(尚未开船;必须确认影响)
```
`CLOSED` 表示停止收货,`DEPARTED`/`COMPLETED` 表示计划记录的运营状态,不得代替现有订舱、装柜或实际船节点事实。实例一旦有有效收货或预占记录,不能回到 `DRAFT`,取消后仍保留货量关联和审计历史。
建议 `OPEN → CLOSED` 由服务端根据最终截止时间自动生效,页面只展示结果;`CLOSED` 不因刷新或修改筛选重新打开。`DEPARTED`、`COMPLETED` 在 V1 仅允许明确的运营动作或后续已确认事实推进,不能由“预计开船时间已到”直接推断。
### 6.9 停用、取消和甩柜
- 渠道停用只阻止新模板和新实例生成;已经生成且仍为 `OPEN` 的实例默认继续有效,是否批量关闭必须由运营人员明确操作。
- 取消实例前必须展示影响范围:预计归班量、已收量、预占量和已关联订舱/柜次。已有已收量的实例只能取消或进入改配流程,不能删除。
- 取消后,将原有效归班标记 `SUPERSEDED`,新增 `UNMATCHED` 版本并保存原船期及 `reason=CANCELLED`,展示为“未安排/待人工处理”;不自动把货量塞进下一班。新版本保留事实引用,原版本可追溯,不原地覆盖历史。
- 同一取消操作将有效订舱关联改为 `REVOKED`,保留历史快照;不自动作废、重开或修改正式订舱。关联解除后当前订柜统计重新计算,正式订舱是否改船由已有订舱流程处理。
- 甩柜/改配时保留原实例、原归班和实际收货事实,新增目标实例关联;不能回写或覆盖现有港务船期、订舱资料或报关事实。
### 6.10 V1 受限的异常重新安排
这是本轮推荐的范围补充,解决“异常已展示,但首版没有办法处理”的缺口。只允许处理未安排、晚到或因船期取消而待处理的货,正常批次批量改绑仍后置。
1. 运营在异常待办选择货物,查看原船期、当前事实版本、原因及匹配候选;选择目标并填写原因后确认,保存前先显示两班的货量变化。
2. 目标须仓库、正式渠道、港口相符,状态 `OPEN` 且当前未过截止;预报另核验预计到仓时间,实收另核验真实到仓时间。V1 不提供绕过截止、强行修改实收时间或跨仓挪量的入口。
3. 服务端核验独立异常安排权限、当前归班版本及目标船期版本;原有效版本转 `SUPERSEDED`,新版本写 `MANUAL`,保留原/新船期、操作者、时间和原因。并发重复确认只能成功一次。
4. 货物如已绑定真实柜次、预装或装柜履约,先阻止在日历直接改配,并指向既有履约流程;不得通过改日历归班绕过实物锁定。无符合条件的目标时继续待安排,可先新增临时船期。
5. 归班重新安排不自动转移订舱关联;目标班的已订柜仍只来自重新校验后建立的有效订舱关联。
## 7. 建议数据模型
以下为规划字段,具体命名应在接口和 SQL 评审时统一。
### 7.1 `fl_sailing_channel`
| 字段 | 说明 |
| --- | --- |
| `id` | 渠道业务主键 |
| `channel_code` | 稳定渠道代码;固定渠道可映射现有渠道目录,自定义渠道也必须先建立稳定代码。现有装柜的九类渠道分组只作为兼容映射,不能把枚举写死为日历渠道全集 |
| `channel_name` | 渠道名称,如 MATSON / 美森 |
| `source_type` | `CATALOG`(复用现有物流渠道)或 `SCHEDULE_ONLY`(仅用于排班,尚未映射为可订舱渠道) |
| `mapped_channel_code` | 自定义排班渠道对应的正式物流渠道代码,可为空;为空时不能被订舱/价格接口当作正式渠道 |
| `default_departure_port_code` | 默认起运港,可被模板覆盖 |
| `default_destination_port_code` | 默认目的港,可被模板覆盖 |
| `default_cutoff_days` | 默认提前收货天数 |
| `status` | `ACTIVE` / `INACTIVE` |
| `remark` | 备注 |
| `created_at` / `updated_at` | 审计时间 |
标准渠道优先复用现有仓库范围内的 ACTIVE 物流渠道和稳定 `channelCode`,不能只保存一个自由文本名称。自定义排班渠道必须通过 `SCHEDULE_ONLY` 明确标记,后续若要用于订舱或报价,先完成正式渠道映射。渠道停用不影响历史实例和历史统计;停用后不能新建模板或自动生成未来实例。
### 7.2 `fl_sailing_template`
| 字段 | 说明 |
| --- | --- |
| `template_no` | 模板业务编号 |
| `channel_id` | 所属渠道 |
| `receiving_warehouse_id` | 收货仓;截止规则和可赶船判断的范围键 |
| `template_type` | V1 固定为 `WEEKLY` |
| `weekdays` | 周一至周日的选中集合 |
| `effective_from` / `effective_to` | 规则有效期 |
| `departure_time` | 开船本地时间 |
| `voyage_rule` | 船名/航次生成规则快照 |
| `transit_days` 或 `eta_time` | 预计到港计算依据 |
| `cutoff_days` | 模板级提前收货天数 |
| `cutoff_local_time` | 收货仓默认截收时刻,可被实例覆盖 |
| `default_capacity_cbm` | 可选的班次计划容量;不替代统一柜型目录的柜容 |
| `status` | `DRAFT` / `ACTIVE` / `INACTIVE` |
### 7.3 `fl_sailing`
| 字段 | 说明 |
| --- | --- |
| `sailing_no` | 船期实例业务编号 |
| `template_id` | 来源模板,可为空(独立单次船期) |
| `channel_id` | 渠道快照引用 |
| `channel_code_snapshot` / `channel_name_snapshot` | 生成时的渠道编码和名称快照 |
| `receiving_warehouse_id` | 收货仓 |
| `departure_port_code` / `destination_port_code` | 港口快照 |
| `vessel_name` / `voyage_no` | 船名和航次 |
| `etd_at` / `eta_at` | 预计开船、预计到港 |
| `suggested_cutoff_at` | 计算出的建议截止时间 |
| `cutoff_at` | 最终收货截止时间 |
| `cutoff_override` / `cutoff_override_reason` | 是否人工覆盖及原因 |
| `cutoff_rule_version` | 计算时使用的时区、自然日/工作日和截收时刻规则版本 |
| `capacity_cbm` | 可选的班次计划容量上限;空值表示不做班次容量提示,不替代统一柜型目录的柜容 |
| `container_type_code` | 默认柜型;为空时按可用柜型分别给出建议 |
| `status` | 实例生命周期状态 |
| `generated_key` | 模板生成幂等键 |
| `version` | 乐观锁版本 |
| `created_at` / `updated_at` | 审计时间 |
不建议把已收体积、剩余体积作为可直接修改的主表字段;它们应由关联事实聚合,必要时做带统计时间的服务端快照。
### 7.4 `fl_sailing_assignment`(V1 自动归班;V1 受限异常重新安排)
V1 必须由服务端持久化自动归班结果;常规人工改绑、拆分分配和预占仍放到 Phase 2,第 6.10 节的异常重新安排除外。归班对象以收货批次或货量预报为最小粒度,不以整张订单或整张入仓单代替。`sailing_id` 可以为空,表示已记录但尚未找到可解释班次的 `UNMATCHED` 结果。
| 字段 | 说明 |
| --- | --- |
| `id` | 归班事实 ID |
| `sailing_id` | 船期实例;`UNMATCHED` 时为空 |
| `inbound_id` / `receipt_batch_id` / `forecast_id` | 归班对象;至少有一个稳定业务键 |
| `assignment_source` | V1 为 `AUTO`;预留 `MANUAL`、`ROLLOVER` |
| `assignment_version` | 同一归班对象的版本号,重算递增 |
| `match_status` | `FORECAST_MATCHED` / `RECEIVED` / `LATE` / `UNMATCHED` / `SUPERSEDED` |
| `volume_cbm` | 本次归属的已收实测体积;V1 固定来自审核后的 `actualVolumeCbm` |
| `forecast_volume_cbm` | 本次归属的未收有效预计体积,与已收体积分开 |
| `forecast_source` | 预计体积来源;没有可靠预计体积时为空,不得用申报值冒充已收体积 |
| `route_source` | 航线来自入仓/预报事实或 `CHANNEL_DEFAULT`,用于追溯匹配依据 |
| `volume_source` | 已收状态固定为 `MEASURED_ACTUAL`;仅预计状态为空,计费/申报仅作对比或后续扩展 |
| `rule_version` | 自动归班规则版本 |
| `matched_at` / `superseded_at` | 归班和被替代时间 |
| `locked_at` / `lock_reason` | 达到船期截止、确认订舱或船期关闭后冻结归班的时间和原因 |
| `operator_id` / `reason` | 自动任务/后续人工操作审计 |
同一批次或预报在同一时刻只能有一个 `AUTO` 有效归班版本;重新计算采用旧版本 `SUPERSEDED` + 新建版本,不能静默覆盖旧历史。
“同一业务对象”在这里指收货批次,不是整票订单或整张入仓单;一票分批到仓、复测或冲正时,必须按批次守恒,不能用单据级唯一键把后续批次锁死。
### 7.5 `fl_sailing_booking_advice_snapshot`
这是订柜建议的可追溯快照,不是正式订舱记录。每次刷新或人工确认建议时保存:
| 字段 | 说明 |
| --- | --- |
| `sailing_id` / `container_type_code` | 船期和柜型 |
| `received_cbm` / `forecast_catchable_cbm` / `risk_cbm` | 已收、可赶预计和风险体积 |
| `planning_capacity_cbm` | 从统一柜型目录读取的规划容量快照 |
| `recommended_container_count` | 按体积初算的建议柜数 |
| `booked_container_count` / `shortage_count` | 已订柜数和待补柜数 |
| `rule_version` / `calculated_at` | 计算规则版本和计算时间 |
| `risk_codes` | ETA 缺失、重量缺失、箱数缺失、混装限制等风险 |
### 7.6 `fl_sailing_booking_link`(V1 最小订舱关联)
首版要显示“已订柜数”和“待补柜数”,因此必须在 V1 建立计划船期与订舱的最小关系;复杂的实际柜次、甩柜和海运节点关联仍放到后续阶段。
| 字段 | 说明 |
| --- | --- |
| `sailing_id` | 计划船期实例 |
| `ocean_booking_id` | 海运订舱 |
| `link_status` | `ACTIVE` / `REVOKED`;解除关系保留历史 |
| `booking_status_snapshot` | 关联时的订舱状态快照;当前只有订舱状态为 `RELEASED` 才允许计数 |
| `warehouse_id` / `channel_code` / 起运港 / 目的港 | 关联时的业务匹配快照 |
| `linked_at` / `operator_id` / `reason` | 关联审计 |
V1 采用以下计数契约:
- 只有当前订舱状态为 `RELEASED` 且关联记录为 `ACTIVE` 的订舱才计入已订柜;`DRAFT`、已作废或已解除关联的不计入。`RELEASED` 是订舱状态,不是关联记录状态。
- 已订柜数按该订舱的柜型需求 `quantity` 汇总,不按已生成真实柜次数量、不按装柜完成数量替代。
- 一个 `RELEASED` 订舱首版只能关联一个计划船期;如果未来需要一个订舱拆到多班,再升级到按柜型需求行分摊的关系。
- 关联时必须校验收货仓、渠道代码、起运港、目的港和计划船期一致;不一致进入冲突,不允许仅凭船名/航次文字关联。
- 订舱解除、作废或计划取消后,已订柜数应在服务端重新计算,并保留原关联历史;不得把已订柜数当作人工可编辑字段。
## 8. 接口草案
接口路径和 DTO 以最终服务端归属为准,以下只作为拆分和评审入口:
| 能力 | 建议接口 | 说明 |
| --- | --- | --- |
| 月历查询 | `GET /platform/v1/sailings/calendar` | 按月份、收货仓、港口、渠道、关键词、状态返回实例和服务端汇总;同时返回未安排/晚到摘要 |
| 实例详情 | `GET /platform/v1/sailings/{sailingId}` | 返回实例、统计、关联货量和操作权限 |
| 新增单次实例 | `POST /platform/v1/sailings` | 幂等创建,支持无模板来源 |
| 编辑/取消实例 | `PUT/PATCH /platform/v1/sailings/{sailingId}` | 乐观锁;取消保留历史 |
| 渠道管理 | `GET/POST/PUT /platform/v1/sailing-channels` | 启用、停用和主数据维护 |
| 模板管理 | `GET/POST/PUT /platform/v1/sailing-templates` | 周模板和启停;固定单次船期直接走实例接口 |
| 生成预览 | `POST /platform/v1/sailing-templates/{id}/generation-preview` | Phase 1 必须支持;返回日期、航次、冲突和截止时间预览 |
| 批量生成 | `POST /platform/v1/sailing-templates/{id}/generate` | 使用幂等键,返回成功、跳过和冲突清单 |
| 货量明细 | `GET /platform/v1/sailings/{id}/volume-summary` | V1 返回自动归班结果、服务端汇总和收货批次明细,不要求已经存在人工改绑 |
| 收货批次事实 | Firstleg 服务正式事实查询(路径以 Controller 归属评审为准) | 提供 `receiptBatchId`、`receivedAt`、最终实测 CBM、审核/冲正状态及仓库/渠道/航线快照;船期页面不得从入仓单分页自行拼装 |
| 货量预报事实 | Firstleg 服务正式事实查询(路径以 Controller 归属评审为准) | 提供 `forecastId`、`expectedArrivalAt`、`forecastVolumeCbm`、有效状态、版本和来源;没有可靠预报时只返回风险/未安排 |
| 订柜建议 | `GET /platform/v1/sailings/{id}/booking-advice` | 按柜型返回已收、可赶预计、风险、已订柜、建议柜数和待补柜数;只读 |
| 订舱关联 | `POST /platform/v1/sailings/{id}/booking-links` | V1 关联一个 `RELEASED` 订舱并校验仓库、渠道、港口和船期;只建立最小关系,不创建柜次 |
| 手工关联/改绑 | `POST /platform/v1/sailings/{id}/volume-allocations` | Phase 2+;校验截止、容量、收货仓归属和幂等键 |
| 变更历史 | `GET /platform/v1/sailings/{id}/history` | 实例、模板和手工改绑审计 |
所有写接口建议使用 `Idempotency-Key`、`expectedVersion` 和服务端权限校验;错误响应应能区分重复生成、容量不足、已截止、状态冲突和归属不一致。
## 9. 权限建议
权限按“看”和“改”拆分,至少包括:
- `platform:firstleg:sailing-calendar:list`:查看月历、详情、货量汇总和只读历史。
- `platform:firstleg:sailing-calendar:create`:新增单次船期。
- `platform:firstleg:sailing-calendar:edit`:编辑、复制和取消。
- `platform:firstleg:sailing-volume:exception-reassign`:V1 受限异常重新安排;只能处理取消、晚到和未安排货量,不能绕过截止、容量或真实履约锁定。
- `platform:firstleg:sailing-volume:bind`:常规人工改绑、拆分和预占,Phase 2+ 才启用。
- `platform:firstleg:sailing-booking:link`:建立或解除计划船期与订舱的最小关联。
- `platform:firstleg:sailing-template:manage`:渠道、模板、批量生成和停用。
- `platform:firstleg:sailing-volume:bind`(Phase 2+):关联/改绑货量。
- 审计导出或跨业务检索如确有需要,再单独增加权限;普通详情历史不另造重复的只读权限。
权限只由服务端决定,前端按钮隐藏不能替代后端校验。
## 10. 分期建议
### Phase 1:可用的日历排班
- 渠道主数据、单次船期、周模板。
- 截止时间计算和实例生成。
- 生成预览、冲突清单和确认后批量生成。
- 月历、日期详情、渠道维度的预计/已收体积汇总。
- 服务端自动归班的只读结果:预计可赶船、已收、晚到风险、未安排。
- 按现有柜型目录计算只读订柜建议:可赶总 CBM、建议柜数、已订柜数、待补柜数和风险提示。
- 保存自动归班事实和版本历史;建立计划船期与 `RELEASED` 订舱的最小关联,用于计算已订柜数;为取消、晚到和未安排货提供受限异常重新安排。
- 实例编辑、取消和基础历史。
Phase 1 不提供正常货量的批量改绑、强制赶截止和容量预占;只提供第 6.10 节的受限异常重新安排。若没有明确归班事实,只展示“未安排”,不能把按渠道汇总的体积伪装成某一班船已收。
### Phase 2:排班辅助和容量控制
- 模板复制和批量调整。
- 在 V1 已实现的收货仓、渠道、港口、截止时间和确定性排序基础上,Phase 2 再增加复杂优先级、容量约束和多班冲突处理。
- 容量阈值、超容拦截和有权限的强制处理。
- 手工关联、改绑、预占和货量明细。
### Phase 3:运营增强
- 工作日/节假日日历。
- 月度/渠道汇总导出和报表。
- 截止提醒、异常通知和更完整的审计检索。
- 与真实柜次、港务船期、甩柜/改配和实际海运节点的完整可追溯关联,但不覆盖已有运输事实;V1 的最小订舱关联已经前置。
## 11. 验收标准
1. 新建“美森每周周四”模板,预览并生成指定月份的所有周四实例;再次生成不产生重复实例。
2. 新增一条固定船期,能够编辑开船时间、船名、航次、截止时间和容量;已有归班或已收货事实后只能取消,不能物理删除。
3. 日历格和日期详情显示同一套渠道统计口径;至少能区分预计体积、已收体积、未安排/晚到和剩余容量。
4. 根据收货仓、起运港、渠道、船名/航次和月份筛选后,月历数量、日期详情和统计合计一致;跨月日期不会重复或漏显示。
5. 截止时间按服务端规则计算,人工覆盖有明确标识;超过截止或已取消的船期不能被自动推荐。
6. 接近容量上限和超容量有明确状态;V1 只读提示,不能因为体积初算直接生成正式柜次。
7. 停用渠道的历史实例可查询,停用后不能创建新的模板实例;渠道改名不改变历史实例的名称快照。
8. 写接口具备幂等和乐观锁;重复提交、并发编辑、重复生成均返回可理解的冲突信息。
9. 列表/详情只读、实例编辑/取消、模板管理、货量改绑分别受权限控制;操作历史可追溯到操作者和原因。
10. 页面展示业务编号、船名、航次、港口和渠道,不以内部数据库 ID 作为用户可见回退。
11. 对一班固定船和一班临时船,系统能分别展示已收 CBM、截止前预计 CBM、风险 CBM、已订柜数、建议订柜数和待补柜数;建议结果标注计算时间、柜型和规则版本,并明确最终以智能排柜/人工确认结果为准。
12. 同一收货批次先产生预计归班,之后实际收货并复测;系统保留旧归班版本、只计最新有效实测体积,且不把预计体积与实测体积重复相加。
13. 同一时刻建立两班船,自动归班结果稳定选择 `sailing_no` 较小者;船期截止后刷新页面,原归班仍显示为 `LATE` 或 `RECEIVED`,不会变成无历史的未安排。
14. 一个 `RELEASED` 订舱关联一条计划船期,柜型需求数量计入已订柜;`DRAFT` 订舱、未关联订舱和真实柜次数量不会被错误计入。
15. 自动归班在截止前可以重算;达到截止后原归班被冻结,后续实际收货标记为 `LATE` 时仍保留原船期。没有稳定收货批次或预报事实时只显示风险/未安排,不得用 `inboundedAt`、`originalVolumeCbm` 或入仓单分页合计补出数字。
## 12. 首版已冻结与后续校准
本轮已先采用以下口径,开发按此实现:
1. 船期模板/实例必须绑定收货仓、渠道、起运港和目的港;不使用“全部仓”作为模糊范围。
2. 已收只统计 V1 收货批次事实中明确完成收货、最终测量审核通过且 `actualVolumeCbm > 0` 的批次;申报体积和计费体积不计入已收。
3. V1 新增或正式暴露稳定的 `receiptBatchId + receivedAt` 收货批次事实;不能用现有会被多个流程节点更新的 `inboundedAt` 单独代替。
4. V1 新增或正式暴露 `forecastVolumeCbm + expectedArrivalAt` 货量预报事实;`originalVolumeCbm`、申报箱数和计费体积不得作为预计可赶量。
5. 预计可赶量只纳入有明确 ETA、预计体积、渠道/仓库/航线匹配且不晚于 `warehouseCutoffAt` 的有效预报;没有 ETA 或航线的货列为风险/未安排。
6. 自动归班选择最早可赶的 `OPEN` 船期,并在 V1 持久化 `FORECAST_MATCHED`、`RECEIVED`、`LATE`、`UNMATCHED` 和 `SUPERSEDED` 历史;截止前且未锁班时可以版本化重算,达到截止、`CLOSED` 或已确认订舱后冻结当前归班;船期取消只转为风险/待人工处理,不自动换班;晚到不自动改班。
7. 同一收货仓、渠道、起运港和目的港下,ETD 相同的多班船按 `sailing_no` 升序确定唯一归班;同一归班对象同一时刻只能有一个有效版本。
8. `warehouseCutoffAt` 暂定为开船本地日期前 3 个自然日、收货仓 18:00,时区为 `Asia/Shanghai`;它与官方截单/截关/截港分开。
9. V1 建立计划船期与 `RELEASED` 订舱的最小关联;已订柜数按关联订舱的柜型需求 `quantity` 统计,`DRAFT` 不计入,不按真实柜次数量替代。
10. 订柜建议复用现有柜型目录、容量和已订柜事实,只读展示建议柜数和待补柜数,不直接创建正式柜次;结果标注“按体积初算,最终以智能排柜/人工确认结果为准”。
11. V1 航次规则只支持固定值或“前缀 + ETD 本地日期”,生成实例允许人工覆盖;不做顺序号自动递增。
12. V1 采用人工预览后生成固定周模板实例;停航或临时变更先手工跳过/取消,暂不做节假日自动日历。
以下事项不阻断首版,但需要在试运行或进入后续阶段前校准:
1. 多个收货仓是否能共同供货给同一班船;如果可以,建立明确的收货仓组,再做线路级合计,不能直接跨仓混算。
2. 提前天数是否从自然日升级为工作日,周末/节假日是否接入工作日历。
3. 渠道是否需要客户专属范围;若需要,客户必须进入模板、实例、查询和权限维度。
4. 同一渠道同一天多班船的复杂运营优先级、航次生成规则和模板覆盖策略。
5. 订柜建议是否支持多柜型混合、重量约束和装载率优化;首版按体积初算,最终以智能排柜/人工确认结果为准。
6. 计划实例与真实柜次、港务船期的完整关联时机,以及甩柜、改配后的运输事实展示。
## 13. 工程接入门禁
进入实现阶段后,页面与接口需要同步维护以下入口:
- Admin 页面放在 `apps/admin-web/src/modules/` 的独立业务模块内,包含 `pages/`、`routes.ts`、`api/` 和 `types.ts`;不能把模板和实例逻辑塞入现有 `CarrierScheduleQueryPanel.vue`。
- 正式接入时同步 `src/app/router/index.ts`、`src/core/navigation/adminNavigation.ts` 和平台 IAM 权限初始化;菜单归属应在“国内仓”与“货柜管理”中选定后固定。
- Firstleg 若承载服务端能力,需同步 Controller、OpenAPI、数据库基线、Gateway 路由和权限契约;月历统计由服务端返回,前端不以 `InboundOrderSummary` 分页结果自行拼装。
- 需要优先复用现有仓库范围内的正式 `channelCode`/渠道目录,再为确有需要的排班专用渠道建立明确映射;同时校验入库实测字段、订舱/货柜船期和价格簿的引用边界。新计划不能回写港务船期观测或订舱快照。
- V1 开发前必须先落地 `ReceiptBatchFact`、`InboundVolumeForecast`、`fl_sailing_assignment` 和 `fl_sailing_booking_link` 的服务端事实与索引;Firstleg 的当前基线对应 `phase4-sailing-facts-schema.sql`、`sailingcalendar/fact/entity` 和 `SailingFactsRepository`,部署统一增量也必须包含这四张表。如果这些事实没有来源,接口只能返回风险/未安排,不能伪造已收或已订柜数字。
- 订柜建议必须读取统一柜型目录和 `RELEASED` 订舱需求;不得另建 40GP/40HQ/45HQ 容量常量,也不得用真实柜次数量替代订舱需求数量。
- 页面验收覆盖正常、空数据、加载、失败、无权限、并发冲突、已截止、超容量和已取消状态,并按 Admin 当前 1920×1080、100% 缩放基线执行。
## 14. 建议实施顺序
1. 按第 12 节首版冻结口径建立统一的术语、体积、归班、截止时间和订柜建议规则。
2. 画出模板、实例、货量关联和现有港务船期之间的关系,并评审权限与审计边界。
3. 先落地服务端查询模型和月历返回结构,再实现 Admin 月历页面,避免前端自行拼装统计。
4. 用“美森每周周四 + 一条临时船期 + 一批预计到仓数据 + 一批已收实测数据 + 一批晚到数据”做端到端造数和验收。
5. 完成 Phase 1 后再决定手工改绑、容量预占、工作日历、导出和外部船期接入,避免首期把订舱和船公司接口一起带入。
## 15. 本轮复核结论与验收算例
本轮复核后的结论是:**基础排班可以进入开发;完整的收货统计、订柜建议和异常处理必须按本节算例验收后,才能认为首版可用。** 下面的数字是验收用例,不是初始化数据,也不是对截图示例的默认值。
### 15.1 分批收货不能用整票相减
- 一票有效预报拆为 A 批 40 m³、B 批 60 m³。
- A 批实际到货并审核为 45 m³时,结果为“已收 45 m³ + 未收预计 60 m³”;不能把整票 100 m³ 直接减去 45 m³后把 B 批改成 55 m³。
- A 批复测为 43 m³时,只更新 A 批实收版本;B 批仍为 60 m³。任一版本重复消费都不得重复计量。
- 如果实收不能明确对应 A 或 B,相关预报暂列“待核对”,不把实收和预报同时计入可赶总量。
### 15.2 无预报直接收货与晚到的区别
- 没有预报但首次形成 20 m³审核实收,且仓库、渠道、航线明确、实际到仓时间不晚于某个 `OPEN` 船期截止:自动归入该船,状态为 `RECEIVED`。
- 已经归属于周四船的批次,实际到仓晚于周四截止:保留周四历史,状态为 `LATE`;已收总量增加,但周四的“本船可赶实收”和订柜建议不增加。
- 没有归班、资料缺失或已过截止才首次到仓的批次进入异常待办,不能仅因体积或渠道相似自动塞进下一班。
### 15.3 订柜建议要排除晚到并抵扣真实柜型容量
- 40HQ 规划容量按统一柜型目录读取,不能在日历模块复制常量。
- 一班船有本船可赶实收 40 m³、有效未收预报 28 m³、晚到 35 m³;建议计算量为 68 m³,晚到 35 m³只在已收总量/风险中展示。
- 若已有一个 40GP 和一个 40HQ 的 `RELEASED` 订舱关联,已订规划容量按两个柜型各自容量折算;不能只按“已订 2 柜”抵扣,也不能把 `DRAFT` 订舱计入。
- 班次规划容量小于可赶总量时只显示超容量和超出体积,不能静默截断可赶量或降低建议柜数。
### 15.4 取消后的异常重新安排
- `CLOSED` 且尚未开船的船期可以取消;取消会保留原归班和订舱关联历史,将当前货量转为异常待办,不自动改到下一班。
- 运营有异常重新安排权限时,选择目标 `OPEN` 船期、填写原因,系统同时校验仓库、渠道、港口、截止时间、目标版本和履约锁定状态。
- 成功后旧归班为 `SUPERSEDED`,新归班为 `MANUAL`,目标班只计一次;目标班订柜关联必须另行建立或确认,不随货量改配自动转移。
- 已有真实柜次、预装或装柜履约锁定的货不能从日历直接改配,必须回到对应履约流程处理。
### 15.5 开发完成判定
满足以下条件才通过首版业务验收:事实来源可追溯、预报和实收不重复、晚到不增加原班可赶量、取消后有异常处理入口、订柜按柜型需求折算且仅统计有效 `RELEASED` 关联、月历与详情使用同一服务端快照。仅完成页面、接口返回空数组或通过类型/编译检查,均不能作为通过依据。
### 15.6 成熟货代系统的排船期做法
成熟系统把“排船期”视为一条可执行的运输计划,而不是日历上的一个标记,通常分为四层:
1. **服务/周班模板**:维护渠道、收货仓、起运港、目的港、固定星期、开船时间、时区、默认航程和默认截止规则。固定周班按滚动窗口生成未来 8~12 周的具体航次;模板停用只影响未来生成,不回写历史实例。Oracle OTM 也支持按固定星期时间或相对开船时间的偏移配置航次截点:[Oracle Cutoff Recovery Time](https://docs.oracle.com/en/cloud/saas/transportation/26c/otmol/planning/power_data/rates_and_codes/cutoff_recovery_time_details.htm)。
2. **具体航次实例**:每班保存自己的船名、航次、计划 ETD/ETA、内部收货截止、船司确认的各类截止、状态和容量。模板只提供默认值;船司确认或运营人工覆盖后,只更新实例并保留变更历史。
3. **货物归班事实**:预计到仓、审核后的实收批次、体积版本和当前归属独立保存,能追溯自动归班、晚到、取消和人工改配的前后航次。
4. **订舱执行**:预报量、已收量、已订柜量、待补柜量分开统计。只有已确认/已放舱的订舱关联计入已订柜,草稿和取消订舱不计入。
一个航次不应只有一个“截止时间”。至少区分:我司仓库最晚收货时间、订舱接受截止、截单/SI、截关/CY、VGM,以及危险品/冷柜等特殊货类截止。DCSA 将这些作为不同的业务节点,并指出其会随船期、码头和货类变化:[DCSA Cut-off Times](https://dcsa.org/newsroom/cut-off-times-in-shipping)。VGM 还必须在装船前提前提交,才能满足装载条件:[IMO VGM](https://www.imo.org/en/ourwork/safety/pages/verification-of-the-gross-mass.aspx)。因此,本系统的“开船前 3 个自然日、收货仓 18:00”只能作为我司内部 `warehouseCutoffAt` 默认值,不能当作船司截关时间。
自动归班采用以下顺序:先按收货仓、渠道、起运港、目的港筛选 `OPEN` 且尚未超过 `warehouseCutoffAt` 的候选,再按 ETD 最早排序。实际到仓时间等于截止时间仍算准时;超过截止时间后,未锁班、未订舱的货物自动寻找下一班,下一班不存在时进入 `UNMATCHED/待人工`。已经锁班或已确认订舱的货物即使晚到,也保留原 `sailingId`,标记 `LATE/MISSED_CUTOFF`,由运营确认是否转下一班,不能静默改写原归属。
例如美森固定周四 10:00 开船,义乌仓内部收货截止为周二 18:00:周二 17:59 和 18:00 到仓的货物可赶本周四;周二 18:01 首次到仓的货物只能进入下一周四;已经确认归属本周四的晚到货物则保留本周四并进入晚到异常。
建议状态补充 `NEAR_CUTOFF` 和 `BOOKING_CONFIRMED/LOCKED`。`NEAR_CUTOFF` 只用于预警,仍允许收货;超过内部截止后由服务端禁止新的自动归班。船司延误、甩柜或取消时,保留原实例和归班历史,生成异常待办并要求明确改配,不直接把货物静默塞入下一班。
### 15.7 从当前版本到可运营版本的改造顺序
结论是保留现有“模板 → 航次实例 → 收货归班 → 货量汇总 → 订柜建议”主模型,不重做日历页面,先补齐以下规则和闭环。
**P0:上线前必须完成**
1. 服务端统一执行收货截止门禁。候选航次必须同时满足 `status=OPEN` 和 `arrivalAt <= warehouseCutoffAt`;首次晚到不能继续挂当前航次,已存在归班的晚到只能保留原航次并标记 `LATE`。
2. 建立锁班和字段冻结。已有确认订舱、人工锁班或履约事实后,禁止直接修改收货仓、渠道、港口、ETD、截止时间等关键字段;需要调整时走复制、取消或异常改配。
3. 完善预报与实收的批次关联。同一收货批次只能消费一个有效预报;多个预报、错误的预报键、预报作废和拆分必须进入待核对,不能同时计入预报量和实收量。
4. 取消/甩柜只产生异常待办,不自动把货物静默塞进下一班;人工改配必须记录原航次、目标航次、操作者、时间、原因和目标版本。
5. 收货仓、渠道、起运港和目的港使用主数据选择并由服务端校验,不能长期依赖前端自由填写名称或 ID。
6. 月历和详情只统计同一服务端口径:已收使用审核实测 CBM,晚到不计入原班可赶量,取消航次不计入主汇总,已订柜只统计有效的 `RELEASED/CONFIRMED` 关联。
**P1:首版稳定运行后完成**
1. 增加预报维护、异常待办、人工改配、订舱关联/撤销等 Admin 操作入口。
2. 模板支持完整有效期、停班例外、跨月生成和生成前冲突预览;默认滚动生成未来 8~12 周。
3. 增加 `NEAR_CUTOFF`、待补柜、超容量、未安排等风险提示,并提供按渠道的 CBM 和柜量分组汇总。
4. 所有写操作接入统一幂等和版本校验,避免重复点击、重复生成和并发覆盖。
**P2:运营增强**
再接入船司/码头实际截点、节假日工作日历、容量预占、自动提醒、外部船期同步和更复杂的混合柜型优化。它们不应阻塞首版排船和收货统计上线。
### 15.8 当前展示版范围修正(以本节覆盖前文自动归班口径)
本轮需求已明确:船期日历先作为**展示和决策辅助页面**,不接管现有收货、预报、智能排柜或订舱流程。前文关于 `Sailing Assignment` 自动写入、自动下一班、晚到改配、取消后异常重排和收货审核回调的内容,全部降为后续规划,不纳入当前开发验收。
当前版本允许运营人员维护渠道、周模板和具体船期;货量相关能力全部只读:
- **有效船期与过期**:按 `Asia/Shanghai` 自然日,月历和汇总只纳入今天及以后开船的计划;今天之前的计划自然过期并从日历移除,不删除记录、不伪造已开船状态。服务端返回上海本地时间 `nextRefreshAt`,取下一零点和同仓、同渠道范围内最近截仓时刻之后 1 毫秒的较早值;截仓查询不受展示月份、船名、港口或状态筛选限制。页面据此自动刷新,失败后延迟 60 秒重试,避免跨月顺延后货量停留在旧值。
- **已收 CBM 与未发货顺延**:读取现有收货/测量流程已经产生且尚未确认国内离仓的有效实测体积。已排柜且关联有效 `RELEASED` 订舱的部分,保留在其开船日期不早于今天、状态为 `OPEN/CLOSED` 的船期,截仓后仍保留;支持既有跨仓、跨渠道安排。未安排或原安排取消、日期过期的余量,按原收货仓和渠道选取 `OPEN`、开船日期不早于今天且截止时间不早于当前时刻的最近一班,按开船时间、船期编号、ID 稳定排序。同一份货量只计入一班,部分排柜按体积分开,不受原收货月份限制;没有下一有效船期时保留原货物事实,后续新增有效计划即可承接。这里只改变只读展示,不改已确认订舱、排柜或真实运输安排。
- **预计 CBM**:读取现有入仓预报或智能排柜可读数据;没有可靠来源时显示“暂无数据”,不凭渠道名称或订单分页猜测。
- **截止前参考量**:对顺延后的待发货量按收货时间与 `warehouseCutoffAt` 区分参考量和风险量,不表示货物已经绑定该船。每次月历查询共用一个业务时刻,避免跨过截仓瞬间造成重复。
- **已明确本船量**:只读取现有系统已经明确关联到该船名/航次或现有订舱/柜次事实的数据;日历不新增关联。
- **建议订柜数**:对展示的待发实测与可核验预计体积之和,除以统一柜型容量向上取整;正货量不足一柜也算一柜,零货量为零柜。晚到/风险标签不直接抹除已收货量。待补柜按货量先扣除未离仓柜的可用订舱容量,再按对应柜型向上取整,下限为零;已离仓柜的容量不能抵扣仓内余货。日历卡片、汇总和各柜型方案保持一致。不创建订舱、不锁舱位、不改柜次。
- **合船判断**:页面只提供“本船未满、建议关注相邻船期”的提示,并展示相邻船的可用容量和货量对比;不自动合并、不自动改绑。
展示版的数据实现口径再明确一层:已收实测只统计测量审核为 `APPROVED` 且有 `actual_volume_cbm` 的有效入库单,限定 `WAIT_PALLETIZE/WAIT_SPLIT/LOADING/LOADED`。未拆分单与拆分后的余量单各按自身实测体积统计;PDA 拣货结算后处于 `FULLY_SPLIT + LOADING/LOADED` 的源单已归一化为实际装柜部分,未离仓前同样保留,不能与整票原始体积重复。每票按有效装柜明细拆分为已离仓体积、有效订舱占用体积与未安排余量;已离仓要求客户、入库单和柜次一致,离仓状态为 `WAITING_FOR_LOADING` 且存在确认时间。扣除已离仓和有效订舱占用后,余量最低为零;订舱占用部分单独计入对应船期,撤销离仓后按现有运输安排重新纳入。当前没有可核验的预计体积来源时,预计量按 0 展示并在页面提示“暂无可核验数据”,不得把申报体积、原始体积或分页列表合计冒充预计量。已订柜读取已放舱订舱的柜型需求,存在有效显式关联时只认该关联;没有有效显式关联才用仓库、渠道、港口、船名、航次和开船日期做确定性只读匹配,每条订舱只选一班。已订总容量保存在 `bookedPlanningCapacityCbm`,扣除已离仓柜后的可用容量为 `availableBookedCapacityCbm`;按订舱需求中的柜型容量整柜扣除,同柜多票只扣一次,撤销离仓后恢复。日历页面不创建关联,待补柜只做提示。
当前版本明确不做以下写操作:
1. 收货审核通过时,不调用船期日历服务,不写入 `fl_sailing_assignment`、预报消费或船期实收事实。
2. 创建或修订预报不自动生成船期归班;预报维护接口不作为本轮页面入口。
3. 取消船期后未离仓货量按上述规则顺延展示;不改写货物实际归属,仍由原智能排柜和订舱流程维护。
4. 页面不提供货量异常改配、手工绑定实收、订舱关联/解除等操作;这些操作留在现有业务流程。
因此,本轮验收重点从“自动归班正确”改为:船期维护正确、展示口径透明、未发货量顺延且不重复、建议柜数向上取整、取消或截止只影响只读展示,不会改变原收货和智能排柜数据。后续如果需要让管理人员在页面上确认“合并到哪一班”,再单独增加人工确认动作和正式归属模型。