出货节点倒排 · 截关截单报关最晚哪天做
定位与服务边界
这个 Skill 只回答一个问题:这票货每个环节最晚哪天必须做完。
不订舱,不报关,不联系货代,不推荐航线,不含任何船期数据。
它做的是:你已经知道开船日(或客户要求到货日),现在需要
「倒着排一遍,看看到底还来不来得及、哪天是死线」。
两件事必须说在前面,否则这份报告会被误用:
- 倒排出的是「最晚完成日」,不是「计划开始日」。
按这个日子动手没有任何余量——它是一条死线,不是一份从容的计划。 - 本工具不预设任何环节。 订舱、拖车、装柜、报关、截单、截关……
这些环节名和提前天数全部来自你的输入文件,脚本一个都不替你猜。
因为每家公司的作业链条不一样,「标准模板」只会给出错的答案。
同理,不内置任何国家的法定节假日——节假日只能由 --holidays 传进来。
中国的春节、国庆,俄罗斯的胜利日,沙特的斋月,各有各的调休规则,
写死在脚本里迟早会算错,算错的代价是甩柜。
三分钟上手
第一步:先确认脚本本身是好的
python scripts/ship_countdown.py --selftest
实测输出 selftest 61/61 通过,退出码 0。这一步不要跳过——
它同时验证日期解析(含中文日期与不存在的日期)、单位换算、
工作日顺延、拦截规则、报告章节编号、退出码封闭。
第二步:跑内置 demo,看清报告长什么样
python scripts/ship_countdown.py --demo --out ./out
会在 ./out 下一个 _demo_出货 目录里造 2 个示例批次(一个是给开船日的,
一个是只给客户要求到货日 + 运输天数的)并出报告。
demo 的数字随「今天」变:内置示例的开船日在 10 月中下旬,而「环节已经来不及」
「缓冲天数已耗尽」这两类警告是拿 --today 比出来的。想复现文档里的数字,显式给基准日:
python scripts/ship_countdown.py --demo --out ./out --today 2026-10-06
实测 --today 2026-10-06 时是 阻断 0 / 警告 5 / 提示 3;换到 2026-10-10 再跑就是
阻断 0 / 警告 7 / 提示 2(多出 2 条「已来不及」,少 1 条顺序倒挂提示)。
这不是脚本不稳定,是「过没过期」本来就该跟着日子变。
第三步:跑自己的出货批次
一票一个文件,全部放进同一个目录:
Order: SO-2026-1001
ETD: 2026-10-20
ETA: 2026-11-15
Transit Days: 28
Node: 订舱 -7
Node: 拖车派车 -3
Node: 装柜进仓 -2
Node: 报关放行 -1
Node: 截单SI -2
python scripts/ship_countdown.py --path ./出货 --out ./out
只给要求到货日也行——脚本会反推最晚开船日:
票号: SO-2026-1002
要求到货: 2026年11月20日
运输天数: 30天
环节: 订舱 -10
环节: 产成品入库 -15
环节: 报关放行 -2
支持 .txt / .md / .markdown / .csv / .json / .ini / .properties;
中文键名与中文日期同样认,实测 票号 / 开船日 / 要求到货 / 运输天数 / 环节 可正常解析。
字段名不要求统一写法,下面这些都能认:
| 你要表达的意思 | 认得的写法 |
|---|---|
| 票号 | Order Order No SO PO 票号 订单号 合同号 批次 |
| 开船日 | ETD Ship Date On Board 开船日 船期 预计开船 离港日 |
| 到港日 | ETA Arrival 到港日 预计到港 抵港日 |
| 要求到货日 | Deadline Required Date 最迟到货 要求到货 客户要求 交货期 |
| 运输天数 | Transit Days Transit 运输天数 航程天数 在途天数 海运天数 |
| 环节 | Node Step Milestone Task 环节 节点 步骤 事项 |
| 备注 | Note Remark 备注 说明 |
环节行的写法:Node: 环节名 提前天数,天数写在最后,负数表示开船前。
Node: 订舱 -7 就是「最晚开船前 7 天必须订完舱」。
天数带单位也能剥掉(-7天 -7 d 都认)。
第四步:把它变成一道门禁
出货前每天定时跑一次,有阻断就让流水线红:
python scripts/ship_countdown.py --path ./出货 --out ./出货/report --fail-on-block
退出码 2 即代表「有阻断级问题」,交给 CI 判断即可。
报告可以直接写进输入目录——本工具不会把上一轮自己产出的
出货节点倒排报告-*.md / .json 当成下一轮的输入(已写进自检断言),
所以 --out 与 --path 指到同一个目录也不会越跑越多。
日历选项
| 选项 | 取值 | 默认 |
|---|---|---|
--weekend |
sat+sun(双休)/ sun(单休)/ none(不休) |
sat+sun |
--holidays |
逗号分隔的日期,如 2026-10-01,2026-10-02 |
无(不内置任何法定节假日) |
--today |
基准「今天」,用于算缓冲天数 | 系统当天 |
输出契约
每次运行产出两份同前缀文件,写到 --out 指定的目录:
出货节点倒排报告-<YYYYmmdd-HHMMSS>.md——给人看出货节点倒排报告-<YYYYmmdd-HHMMSS>.json——给程序消费
md 报告章节(编号连续,条件章节自动顺延)
| 章节 | 是否必有 | 内容 |
|---|---|---|
| 一、结论先行 | 必有 | 批次数 / 最该动手的一天 / 剩余缓冲 / 环节总数 / 已前移数 |
| 二、逐票倒排 | 必有 | 每票的开船日、到港日、要求到货日、运输天数,与逐环节倒排表 |
| 三、问题清单 | 必有 | 阻断 / 警告 / 提示三级,每条带「怎么改」 |
| 四、跳过清单 | 有跳过文件才有 | 列出被跳过的文件与原因,不静默吞掉 |
| 五、扫描范围与漏检边界 | 必有 | 明确写出本工具不覆盖什么 |
出现条件章节时编号自动顺延,已由自检断言「有/无跳过清单两种情况下编号均连续」覆盖。
JSON 产物字段(给程序消费,逐字段固定)
顶层:generated_at / input / options / counts / aggregate / shipments / skipped
counts:shipmentsskippedblockwarninfoaggregate:soonest(全批最早到期日)buffer_daysn_nodesn_movedshipments[]:relorderetdetd_derivedetadeadlinetransit
nodes[]buffer_daysissues[]nodes[]:nameleadrawraw_dueduemoved
——raw_due为顺延前日期,moved为 true 时才不同issues[]:固定 4 键levelcatsnippetfix;level取值封闭为阻断 / 警告 / 提示
etd_derived 为 true 表示开船日是反推出来的(要求到货日 − 运输天数),
看报告的人必须知道这个数是算出来的、不是船公司给的。
检查项清单
一、阻断级(排不出来或排出来是错的)
| 检查项 | 触发条件 | 为什么阻断 |
|---|---|---|
| 缺开船日 | 既无 ETD,又缺 Deadline 或 Transit Days |
没有基准日,倒排无从谈起 |
| 开船日 / 到港日 / 要求到货日认不出 | 日期格式不在可认范围 | 认不出就不排,绝不猜 |
| 日期不存在 | 如 2027-02-30 |
用真实日历构造拦截,不让它流到下游再崩 |
| 运输天数不是整数 / 为负 | 天数写成了文字或负数 | 同上 |
| 环节缺提前天数 | Node: 后没有数字 |
没有提前天数就算不出日期 |
| 环节名为空 | 只有数字没有名字 | 报告里会出现无名事项 |
| 环节名重复 | 同一票里两个同名环节 | 会互相覆盖,倒排表说不清 |
| 倒排日仍落在非工作日 | 连续非工作日超过 60 天(如 --holidays 误填了一整年) |
宁可判阻断,也不给一个根本没人上班的「最晚完成日」 |
| 到港早于开船 | ETA < ETD | 多半是填反了 |
| 到港晚于要求到货日 | ETA > Deadline | 这票已经来不及,要改航线或谈交期 |
| 按运输天数推到港晚于要求到货日 | ETD + 运输天数 > Deadline | 同上 |
二、警告级(排得出来,但风险你自己拍板)
| 检查项 | 触发条件 | 怎么改 |
|---|---|---|
| 环节落在非工作日已前移 | 倒排日撞周末或你给的节假日 | 已自动前移,看报告里的「原定日 → 最晚完成日」 |
| 环节已经来不及 | 某环节最晚日早于今天 | 改船期,或砍掉这个环节走加急 |
| 缓冲天数已耗尽 | 最早环节 ≤ 今天 | 按当前船期这票排不下 |
三、提示级(省钱线索)
| 检查项 | 触发条件 | 怎么改 |
|---|---|---|
| 缓冲偏多 | 最早环节距今 > 5 天 | 可以晚点动手省仓储,或接更紧的船期 |
| 环节顺序与提前天数不匹配 | 排在前面的环节提前天数反而更小 | 先确认环节真实先后:先做的事提前天数应更负 |
| 提前天数为正 | 天数是正数 | 正数表示开船后;开船前的环节请写负数 |
| 字段未被识别(已忽略) | 文件里出现了别名表不认的字段名 | 不参与倒排也不改你的数据;检查是不是拼错,或本工具确实没有这个字段 |
触发与不触发
会触发
- 「截关最晚哪天」「截单什么时候截止」「报关最晚哪天做」
- 「这票还赶不赶得上这班船」「缓冲还有几天」
- 「船期倒排」「交期倒排」「出货计划怎么排」
- 「客户只给了要求到货日,我最晚什么时候订舱」
- 「我这个环节撞上周末了,要不要提前」
不会触发(非触发边界)
- 「哪家货代便宜」「这柜运费多少」——属报价比价,本工具不算钱
- 「一柜能装多少箱」「装载率多少」——属装柜测算
- 「运费按多少公斤收」「体积重怎么算」——属计费重核算
- 「发票箱单提单齐不齐」——属出口单证体检
- 「关税多少、到岸成本多少」——属到岸成本测算
- 「选哪个国家出海」——属出海准备度诊断
- 「帮我订舱 / 报关 / 拖车」——属执行动作,本工具不产生任何外部动作
扫描范围与漏检边界
会读的
- 指定目录下所有
.txt / .md / .markdown / .csv / .json / .ini / .properties
文本文件(子目录递归,跳过__pycache__ / .git / node_modules等) - 也接受
--path直接指向单个文件 - 编码按
utf-8-sig → utf-8 → gbk → utf-16依次尝试,最后才errors=replace - 日期认
2026-10-202026/10/202026.10.20202610202026年10月20日
不读的(会计入「跳过」并列出,绝不静默吞掉)
- 其他扩展名的文件(如
.xlsx、图片、PDF) - 空文件
- 解析不出任何字段的文件
- 含 NUL 字节的二进制文件(记作「疑似二进制」跳过,不当正文解析)
- 本工具自己产出的报告(
出货节点倒排报告-*.md / .json),无论它在哪个目录
不认但会留痕的
- 文件里出现别名表不认的字段名 → 在报告里列为「字段未被识别(已忽略)」,
不参与倒排,也不静默改你的数据。宁可多报一条提示,也不让你以为它算进去了。
判不了的(本工具明确不做,也不是缺陷)
- 不内置任何船公司船期、港口作业时长、各国法定节假日——节假日只能由
--holidays传入,一个都不预设 - 不预设任何环节:环节名与提前天数全部来自输入文件,本工具不替你猜该做哪几步
- 不订舱、不报关、不联系货代、不推荐航线——只做日期运算
- 倒排出的是最晚完成日,不含作业本身耗时,也不含任何安全余量
- 顺延规则只认「周末 + 你给的节假日」;调休上班日(周末要上班)本工具无法识别,需你自己核对
- 跨月/跨年、闰年按真实日历算;
2027-02-30这类不存在的日期一律判阻断 - 只做单票内部的环节倒排;多票之间的资源冲突(同一辆车/同一个报关行)本工具不覆盖
- 连续非工作日超过 60 天时判阻断(多半是
--holidays填错了),不会硬凑一个日期给你
本报告 0 问题 ≠ 这票一定赶得上船,只代表上面的检查项没命中。
常见错误与应对
| 现象 | 原因 | 怎么改 |
|---|---|---|
报错 需要 --path(或 --demo) |
没给输入 | 补 --path ./出货 或先用 --demo 看样子 |
报错 需要 --out(输出目录) |
没给输出目录 | 补 --out ./out |
报错 没有可倒排的出货批次(共跳过 N 个) |
目录下没有能解析出字段的文件 | 看跳过原因,多半是扩展名不对或键名不在别名表 |
报错 没有任何环节被排出 |
文件解析出来了但一个 Node 都没配上 |
环节行写成 Node: 订舱 -7,别漏了 Node 这个键名 |
报错 --weekend 只能是 sat+sun / sun / none |
拼错 | 三选一;不休就写 none |
报错 节假日日期认不出:下周三 |
节假日只认具体日期 | 写 2026-10-01,2026-10-02 这种确切日期 |
报错 --today 日期认不出 |
同上 | 写 2026-10-06 或 2026年10月6日 |
| 报「开船日认不出」但我明明写了 | 日期写法不在可认范围 | 用 2026-10-20 或 2026年10月20日,别写 10/20(缺年份) |
| 报「环节缺提前天数」 | Node: 后没数字 |
写成 Node: 订舱 -7 |
| 报告里某环节显示「已前移」 | 倒排日撞周末或节假日 | 这是正常的,看「原定日 → 最晚完成日」两列 |
| 倒排出来的日子比我想的早 | 这是最晚完成日,不是计划开始日 | 要留余量就自己把提前天数加大 |
| 我给了 ETA 和 Deadline,两个都有,报「来不及」 | 到港日确实晚于客户要求 | 换航线/空运,或去谈交期——这正是本工具要提前告诉你的 |
| 报告里出现「字段未被识别(已忽略)」 | 字段名不在别名表里 | 照抄上表的写法;这条只是留痕,不会改动你的数据,也不会影响已认出的字段 |
| 报「倒排日仍落在非工作日」 | 连续非工作日超过 60 天 | 查 --holidays 是不是填错(比如误填了一整年);本工具宁可判阻断也不给假日期 |
| 批次越跑越多,还冒出一堆「缺开船日」 | 把报告写进了输入目录,老版本会拿报告当输入 | 升级到 1.0.1:本工具的报告产物永不当输入扫描 |
--path 指向单个文件却报「没有可倒排的出货批次」 |
1.0.0 的路径拼接缺陷 | 升级到 1.0.1:单文件路径已可直接读 |
| 排不出环节时只有一行报错、没有任何产物 | 1.0.0 会整轮丢弃报告 | 升级到 1.0.1:即使一个环节都没排出,报告照样落地,原因在「三、问题清单」 |
异常与兜底
| 场景 | 行为 | 验证方式 |
|---|---|---|
| 某个批次文件损坏或解析失败 | 记入「跳过清单」,其余照排,单条坏数据不打断整批 | 解析失败即跳过,不抛异常 |
| 目录下全是二进制垃圾 | 不崩溃,退出码 2 | 二进制-only 目录实测 |
| 空目录 | 报 没有可倒排的出货批次,退出码 2 |
空目录实测 |
| 编码猜不出(GBK / UTF-16 混着) | 四级兜底,最后一级 errors=replace 也不抛异常 |
自检断言编码兜底顺序 |
不存在的日期(2027-02-30) |
判阻断,不静默通过、不崩在下游 | 自检断言 |
--out 指向一个已存在的文件 |
报 输出目录不可写,退出码 3 |
退出码封闭探针实测 |
--path 不存在 |
报 路径不存在,退出码 2 |
退出码封闭探针实测 |
| 参数缺失 / 非法 | 报明确原因,退出码 2 | 缺 --out、缺 --path、--weekend 非法、--holidays 非法、--today 非法逐项实测 |
--demo 与 --path 同时给 |
退出码 2,明确报互斥 | 退出码封闭探针实测 |
有阻断级问题且加了 --fail-on-block |
退出码 2 | 造「缺开船日」与「环节缺天数」两条路径实测均 2 |
无阻断且加了 --fail-on-block |
退出码 0 | 正常批次实测 |
--path 指向单个文件 |
正常倒排,票号来源记文件名 | 单文件实测 rc=0 且 order 正确读出 |
--out 与 --path 指到同一目录 |
报告产物不被当输入扫描,重跑批次不增加 | 同目录连跑两次,第二次仍 shipments=1 / block=0 |
| 一个环节都排不出(如开船日认不出) | 报告照样落地,退出码 2,原因写在「三、问题清单」 | 2027-02-30 实测:rc=2 且报告含阻断项 |
文件含 NUL 字节(伪装成 .txt 的二进制) |
记入跳过清单,原因写「疑似二进制」,不解析 | 二进制样本实测 |
| 连续非工作日超过 60 天 | 判阻断「倒排日仍落在非工作日」,不硬凑日期 | 400 天连续假期样本实测 |
退出码封闭为 0 / 2 / 3:0 成功、2 数据或参数问题、3 输出不可写。
main() 顶层显式捕获异常并转为退出码 2,不会把 traceback 抛给调用方;
SystemExit 单独放行,不会被 except Exception 吞掉。
所有参数校验前置到分支之前——--demo 走的是同一套倒排逻辑,
不会因为它带内置示例数据就绕过 --weekend / --holidays 校验。
实战案例(真实跑通,数字为脚本实跑输出)
案例一:内置 demo,2 个批次
命令:python scripts/ship_countdown.py --demo --out ./out
结果:出货批次 2 份(跳过 0),问题:阻断 0 / 警告 5 / 提示 3
SO-2026-1001(给了开船日 2026-10-20,周二):
| # | 环节 | 提前天数 | 原定日 | 最晚完成日 | 状态 |
|---|---|---|---|---|---|
| 1 | 订舱 | -7 | 2026-10-13 | 2026-10-13 | 正常 |
| 2 | 拖车派车 | -3 | 2026-10-17 | 2026-10-16 | 已前移(撞周六) |
| 3 | 装柜进仓 | -2 | 2026-10-18 | 2026-10-16 | 已前移(撞周日) |
| 4 | 报关放行 | -1 | 2026-10-19 | 2026-10-19 | 正常 |
| 5 | 截单SI | -2 | 2026-10-18 | 2026-10-16 | 已前移(撞周日) |
手算复核:2026-10-20 是周二;−7 天 = 10-13(周二)✓;
−3 天 = 10-17(周六)→ 前移到 10-16(周五)✓;−2 天 = 10-18(周日)→ 10-16 ✓。
SO-2026-1002(只给要求到货日 2026年11月20日 + 运输天数 30 天,中文键名):
- 反推开船日 = 2026-11-20 − 30 天 = 2026-10-21(周三)
- 产成品入库 −15 天 = 2026-10-06,订舱 −10 天 = 10-11(周日)→ 前移到 10-09(周五)
这张表最有用的一列是「原定日 → 最晚完成日」:看起来还有 4 天,
实际只剩 2 个工作日,这就是甩柜的由来。
案例二:换周末规则,结果必须变
同一票(开船日 2026-10-19,环节「报关 -1」):
--weekend |
最晚完成日 |
|---|---|
sat+sun(默认) |
2026-10-16(周五,因 10-18 是周日) |
none |
2026-10-18 |
none + --holidays 2026-10-18 |
2026-10-17 |
→ 换参数结果必须变化,已写进自检断言——防止参数被解析了却没被消费。
案例三:把报告写回输入目录,连续跑两天
需求:每天定时扫 ./出货 目录并把报告也放在里面,担心第二天把昨天的报告当输入扫进去。
数据:./出货/a.txt 一票(开船日 2026-10-20,环节「订舱 -7」)。
命令:
python scripts/ship_countdown.py --path ./出货 --out ./出货 --today 2026-10-10
实际产出(已跑通):第一天 出货批次 1 份(跳过 0),问题:阻断 0 / 警告 0 / 提示 0;
第二天再跑,仍是 出货批次 1 份、阻断 0。
关键点:1.0.0 在这条路径上会翻车——报告是 .md + .json,两者都在可扫扩展名里,
第二天会产生「3 个批次、7 条阻断」的凭空假告警。修法是双保险:
按文件名规则排除本工具的报告产物,同时把输出目录整体排除在扫描之外(输出目录就是输入目录时改用前者)。
误报的代价比漏报高得多——拿一堆不存在的阻断去质问货代,比漏一条更伤协作。
三个反直觉的设计
- 倒排出的是「最晚完成日」,不是「计划开始日」。 按报告上的日子动手,等于零余量。
真要留安全垫,把提前天数写大(-7改-9),而不是看着日历自己加。 - 「看起来还有 4 天」往往只剩 2 个工作日。 demo 里
订舱 -10原定 10-11(周日)被前移到 10-09,
中间那两天是周末——甩柜多半就是这么来的,所以报告里特地把「原定日」和「最晚完成日」分开两列。 - 宁可判阻断,也不给一个没人上班的日期。 连续非工作日超过 60 天时(通常是
--holidays填错了)
脚本直接阻断。一个「理论上最晚但当天全公司放假」的日期,比一句「算不出来」危险得多。
已验证清单(2026-10-06 建立,2026-10-10 增补;未跑的不写)
python scripts/ship_countdown.py --selftest→ 61/61 通过,退出码 0(1.0.0 为 54/54)- 内置 demo(2 批次,
--today 2026-10-06)→ 阻断 0 / 警告 5 / 提示 3;
最该动手的一天 2026-10-06,环节 8 个,已前移 4 个;换到--today 2026-10-10为 0 / 7 / 2 --path指向单个文件 → 退出码 0,order正确读出(1.0.0 在此路径上失败)--out与--path同目录连跑两次 → 第二次仍shipments=1 / block=0(1.0.0 为 3 / 7)- 连续 22 天假期(10-05~10-26)+ 开船日 10-28 → 订舱落到 10-02,不落在假期内
- 400 天连续非工作日 → 判阻断「倒排日仍落在非工作日」,不硬凑日期
- 未识别字段(如
客户名称)→ 报告列出「字段未被识别(已忽略)」,其余字段照常倒排 - 含 NUL 字节的二进制
.txt→ 记入跳过清单,原因「疑似二进制」,退出码 2 - 开船日
2027-02-30→ 退出码 2 且报告仍落地,问题清单含「开船日认不出」阻断 - 中文键名 + 中文日期(
票号 / 要求到货: 2026年11月20日 / 运输天数: 30天 / 环节)→ 正常解析并反推 ETD - 日期五种写法(
-/.紧凑2026年10月20日)→ 全部认得 - 不存在的日期
2027-02-30、月份越界2026-13-01、自然语言下周三→ 全部判认不出,不猜 - 撞周末/节假日 → 自动前移并报警告(
--weekend none时不移,已断言) - 缺开船日 → 判阻断且该批次不可用
- 到港晚于要求到货日 → 判「来不及」阻断
- 环节缺天数 / 名为空 / 名重复 → 均判阻断
- 顺序倒挂(先做的事提前天数反而更小)→ 判提示,不阻断
- 已过期环节 → 判警告
- 空目录 / 二进制-only 目录 → 不崩溃,退出码 2
--out指向已存在文件 → 退出码 3- 缺
--out/ 缺--path/--weekend非法 /--holidays非法 /--today非法 /--demo+--path同时给 / 路径不存在 → 退出码 2 - 有阻断加
--fail-on-block→ 退出码 2(缺开船日、环节缺天数两条路径均实测);无阻断 → 0 - 换
--weekend结果必须变化 → 已断言,确认参数真被消费 - 报告章节编号在有无「跳过清单」两种情况下均连续 → 已断言
- 产物无
%%转义残留、无 U+FFFD 替换字符 → 已断言