31 KiB
邮件自动预报系统 — 需求规格 v0.2
状态:待评审确认(2026-07-23 现行口径回写;2026-08-03 指令拆分规则见独立产品规则页)
相对 v0.1:补齐状态机/已读策略、导入柜·行语义、金样例、冲突检测、UI 主路径、测试 DoD、运营 SOP
相对审查补强:新增 §14 异常场景与系统韧性设计(全文亦独立于docx/异常场景与系统韧性设计.md)
相对实现回写:邮件来源仅 IMAP(无开发种子进库);不做操作审计页/audit_log;IMAP 为近 N 天已读+未读(默认 3 天)+ 幂等;轮询默认 30min(设置页优先);路由含/settings、/logs(导入+拉取);详见docx/现行口径-修订说明-v1.md
指令识别/拆分锁定口径:docx/产品规则-邮件指令识别与拆分.md(与 §4 类型打分并用;冲突以产品规则页 +instruction-lexicon.ts为准)
依据:v0.1、四方审查结论、推荐开发方案、docx/邮件、ccnewTEST、接口 PDF V1.72
0. 已确认决策
| # | 决策 | 结论 |
|---|---|---|
| 1 | 验收主路径 | 主题规则识别「新增预报」+ 清单映射以「邮件2 卡派资料.xlsx」为第一模板 |
| 2 | TransMode / OperationType | 解析给推荐默认;确认页必选可改 |
| 3 | CarrierCentral | TEST https://test.saas.carriercentral.vip/api + Header Saas: TEST;SaveContainer |
| 4 | 邮件线程 | 自动拆单:按 In-Reply-To/References 拆成独立 Message;解析/执行只看当前条,不把历史主题动作合并进当前 |
| 5 | 导入触发 | NEW_CONTAINER 必须人工确认;TRANSFER / 留仓拆分 / 贴标解析成功后 自动调 CC 写接口(mock/live 门禁) |
| 6 | 可导入类型 | NEW_CONTAINER 人工确认;指令类自动执行(须 CC 能力 live,mock 不可作 TEST 验收) |
| 10 | IMAP 接入 | 多邮箱 + IDLE(失败回退轮询)+ OAuth 与授权码并存;可配间隔(默认 30min) + 黑/白名单与关键词过滤 + 拉取日志(≤1000) |
| 11 | OCR | PDF/图片 OCR 作为权威正文辅助(参与分类/预筛);置信度低于阈值不覆盖人工可改内容 |
| 7 | 一期验收口径(消解矛盾) | 正式口径:绑定真实 IMAP → 拉近 N 天邮件 → 列表/详情/确认导入闭环;Admin 改类型保留为运维能力。上线门禁:至少 1 封「新增预报+卡派清单」真邮件可进 PENDING_CONFIRM(金样例 JSON 仅对照,不种子进业务库)(见 §3.2) |
| 8 | 已读策略 | 先落库再标已读;解析失败仍标已读但可「重新解析」(见 §6.2) |
| 9 | 导入粒度 | 一柜一请求;勾选过滤货件行后组包;多柜才柜级部分成功(见 §5.7) |
| 12 | 操作审计 | 一期不做 Admin 审计查询与 audit_log 落库;以应用日志 / 拉取日志 / 导入日志为准 |
| 13 | 指令拆分 | 见 docx/产品规则-邮件指令识别与拆分.md:主题+正文+附件分源;仅最新指令可确认;软词→客户指令;解析无 LLM |
1. 背景与目标
客户不愿走客户端手工预报,改为向客服邮箱发邮件。系统:拉信 → 解析 → 结构化入库 → 人工确认 → SaveContainer 完成新增柜预报。
一期目标: Next.js + MySQL + docker-compose;邮件列表/详情/确认导入;对接 TEST SaveContainer。
一期量化成功标准:
| 指标 | 目标 |
|---|---|
| M2 卡派资料字段映射 | 核心列(柜号/仓库ID/渠道/件数)映射正确率 100%(金样例对照) |
| 人工确认路径 | 从打开详情到提交确认 ≤ 3 分钟(327 行场景含筛选勾选) |
| TEST 导入 | 烟雾 1 柜成功 code=200 且返回柜 id;勾选子集导入货件数与请求一致 |
| 类型规则 | M1–M4 金样例 mail_type 命中率 4/4 |
与现有客户端关系: 本系统是预报入口之一;写入同一 CarrierCentral 账套。一期不做双向同步;冲突以 CC 已有柜为准阻断(§5.8)。
2. 范围
2.1 In Scope
- 多邮箱 IMAP:授权码与 OAuth(Gmail/Microsoft XOAUTH2)并存;QQ 等无 OAuth 的主机仅授权码
- IMAP IDLE(账号可关);失败/不支持时回退轮询(
POLL_INTERVAL_MS) - 邮件快照、附件落盘、幂等键(§8.2);业务预筛后入库
- 线程历史自动拆单(In-Reply-To / References →
thread_id) - PDF/图片 OCR 权威解析(local/aliyun;置信度门限)
- 类型识别(4 类 + UNKNOWN)+ 金样例
- 「卡派资料」xlsx/csv + 贴标指令单解析 → 柜头 + 货件行
- 前端:列表、详情、确认导入、导入日志/补偿、拉取记录;Admin 改类型;设置页(多邮箱/OAuth/CC/拉取间隔与过滤)
- CC:
customerLogin+GetContainerList+GetShippingLineList+SaveContainer - CC 写:转仓 / 留仓拆分 / 贴标自动执行(无接口时 mock+门禁;live 真接通才算完成)
- docker-compose:
web+worker+mysql - 不做:开发种子邮件入库、操作审计页(见决策 #7/#12)
2.2 Out of Scope
- AI 客服
- SMTP 自动回信客户(失败亦不自动通知客户)
- 与 CC 双向全量同步(冲突仍以 CC 已有柜阻断)
- 变更须书面确认后方可再扩范围
3. 样例邮件与验收口径
3.1 现网样例(原样展示)
路径:docx/邮件/(QQ 截图 PDF + 附件,无 .eml)。
| ID | 目录 | 期望 mail_type |
一期动作 |
|---|---|---|---|
| M1 | 邮件1/ |
UNKNOWN |
展示 |
| M2 | 邮件2/ + 卡派资料.xlsx |
WORK_ORDER(转仓关键词覆盖;历史 TRANSFER) |
标准记录表;不导入 |
| M3 | 邮件3/ |
INSTRUCTION_HOLD_SPLIT(贴标信号为证据,主类型按最高分;同分见 §4) |
展示 |
| M4 | 邮件4/ 当前 Message |
WORK_ORDER(贴标/拍照覆盖;历史 INSTRUCTION_LABEL) |
标准记录;不拆根主题 NEW |
| M_BL_TEMPLATE | 邮件/模板/ |
NEW_CONTAINER + mail_record.kind=BL_FORECAST |
模块化柜头 + 数据模版表 |
3.2 验收口径(推荐方案,已采纳)
| 阶段 | 口径 |
|---|---|
| 开发/一期验收 | ① 设置页绑定 IMAP 并连通测试;② 拉取近 N 天邮件可见列表;③ 命中「新增预报+清单」或 Admin 改类型后走确认导入(CC mock 可演示;TEST 须 live) |
| 上线门禁 | 至少 1 封真邮件「主题含新增预报 + 卡派/装箱清单」→ NEW_CONTAINER/PENDING_CONFIRM;金样例 docx/金样例/*.json 仅作期望对照,禁止再依赖种子邮件验收 |
4. 邮件类型判定
4.1 枚举
NEW_CONTAINER | TRANSFER | INSTRUCTION_HOLD_SPLIT | INSTRUCTION_LABEL | WORK_ORDER | UNKNOWN
4.2 规则(加权 + 词边界)
信号源:主题 + 正文纯文本 + 附件名。
取最高分;同分优先级: INSTRUCTION_LABEL = INSTRUCTION_HOLD_SPLIT > TRANSFER > NEW_CONTAINER > UNKNOWN(指令内部:同时命中贴标与拆分时,贴标分 ≥ 拆分则 LABEL,否则 HOLD_SPLIT)。
| 信号 | 匹配方式 | 分 | 类型 |
|---|---|---|---|
| 新增预报 | 主题优先;词完整匹配 | +50 | NEW_CONTAINER |
| 新增转仓 / 转仓 | 词边界;排除「不转仓」 | +50 | TRANSFER(可被工单覆盖) |
| 换标 / 覆盖贴 / 贴标 / 贴好拍照 | 正文或附件名 | +40 | INSTRUCTION_LABEL(可被工单覆盖) |
| 拆柜清单 / 卡转海 / 拦截 / 改自提 / 留仓 | 同上 | +40 | INSTRUCTION_HOLD_SPLIT(可被工单覆盖) |
| 附件名含换标、贴标指令 | 文件名 | +30 | INSTRUCTION_LABEL |
| 附件名含卡派资料 | 文件名 | +15 | 只加分到当前领先类型,不单独定类型 |
| ISO 柜号 | ^[A-Z]{4}\d{7}$ |
+5 | 辅证到领先类型 |
| ETA / 船名航次 / 柜型 | 主题或正文 | +5 | 辅证到领先类型 |
最低可判定分:40,否则 UNKNOWN。
只依据当前 Message。证据写入 type_evidence(各信号命中列表 + 总分)。
4.3 工单覆盖(现行优先)
正文/主题/附件名命中任一:贴标 / 拦截 / 拍照 / 转仓 / 快递单号(「不转仓」除外)→ 最终 mail_type=WORK_ORDER,覆盖上表得分结果。
本期:工单只做标准记录落库与详情展示,不自动写 CC、不进确认导入。转仓一律先记工单,到仓分流自动转仓后置。
4.4 混乱正文柜号启发式
正文较乱时:优先扫正文前两行,取首个 ^[A-Z]{4}\d{7}(ISO 柜号)写入 mail_record.modules.container_no。
4.5 提单「+」标准模板 → mail_record
样例(docx/邮件/模板):主题/正文含
客户+提单号+柜号+港口+柜型+EDT… ETA…船名航次…+服务描述。
解析为 kind=BL_FORECAST 模块化柜头;货件行对齐 数据模版.xlsx 列写入 mail_record.table。详情按模块 + 表格标准化展示。
4.6 标准记录(本期交付重心)
解析产出 parse_result.mail_record(kind / summary / modules / table / work_order_actions)。
本期不做导入闭环变更;NEW_CONTAINER 仍可进 PENDING_CONFIRM,工单类一律 PARSED。
5. CarrierCentral 对接
5.1 环境
| 环境 | API Base | Header |
|---|---|---|
| 一期默认 TEST | https://test.saas.carriercentral.vip/api |
Saas: TEST |
| Demo 备选 | https://api.saas.carriercentral.vip/api-demo |
按租户 |
| 本机 ccnew | http://host.docker.internal:31173 |
Saas: TEST |
以 ccnew TEST.json 为准,不用 PDF 的 demovip。
5.2 请求信封
{
"token": "<customerLogin>",
"loginMark": "<进程级固定 UUID v4>",
"data": "<多数接口为 JSON 字符串>"
}
Header:Saas: <CC_SAAS_HEADER>。
5.3 登录
POST /learun/adms/user/customerLogin- password = MD5(明文) 32 位小写
- token 缓存于 web/worker 进程内存 + DB 表
cc_token_cache(多实例以 DB 为准);410 → 清缓存重登 → 重放原请求 1 次 - 谁调用 SaveContainer:仅 Web API(用户点击确认);worker 不做导入
5.4 接口选择
| 接口 | 一期 |
|---|---|
/Container/Import |
不用(ccnew Insert 可能被注释) |
/Container/SaveContainer |
采用 |
/Container/GetContainerList |
导入前冲突检测 |
/ConfigModule/GetShippingLineList |
船司匹配(可 M5) |
5.5 字段映射
柜头 entity:
| 字段 | 规则 |
|---|---|
F_TransMode |
确认页必选:0/1/3;默认推荐 0 |
F_OperationType |
确认页必选:0/2/4;默认推荐 0;主题含「直送」推荐 2;「提拆派」仍推荐 0(开放项可改) |
F_ContainerNo |
必填;ISO 6346(可配置关闭) |
F_CabinetType |
可解析可改 |
F_BLCopyCode / F_ETD / F_ETA / F_LoadPort / F_Dock |
有则填 |
F_ShippingLineId |
模糊匹配;失败留空人工选 |
F_Classis |
不带托架→0;带车架→1;默认 0 |
F_MemoRemark / F_Instruction |
正文摘要 |
keyValue |
新建恒 null |
货件 ← 卡派资料:
| 列 | 字段 | 必填 |
|---|---|---|
| 仓库ID | F_FBACode |
是 |
| 渠道 | F_Transporter(卡派→TRUCK;UPS/FEDEX/DHL/USPS/自提/留仓/扣货) |
是 |
| 件数 | F_CTNS |
是 |
| 总体积/毛重 | F_CBM / F_Weight |
否 |
| FBA ID | F_FBAID |
否 |
| Amazon reference ID | F_ReferenceId |
否 |
| 分货标识/箱唛 | F_ShipmentID |
否 |
| 派送地址 | F_Address |
否 |
| 最早/最晚送仓 | F_Expected_DeliveryDateB/E |
否 |
| 备注 | F_Remark |
否 |
有效行:F_FBACode + F_Transporter + F_CTNS 齐全。无效行 row_status=INVALID,默认不勾选。
5.6 渠道映射表(黄金)
| 原文(含) | F_Transporter |
|---|---|
| 卡派 / TRUCK / truck | TRUCK |
| UPS | UPS |
| FEDEX / FedEx | FEDEX |
| DHL | DHL |
| USPS | USPS |
| 自提 | 自提 |
| 留仓 | 留仓 |
| 扣货 / 拦截 | 扣货 |
| 其他 | 原样上限 30 字符;标 CHANNEL_UNMAPPED 警告 |
5.7 导入语义(柜 / 行)— 推荐方案已采纳
一封邮件解析结果通常 = 1 个柜号 + N 条货件行
确认页:勾选货件行(过滤 INVALID)
提交:按柜号分组
→ 每个柜号 1 次 SaveContainer
→ entity = 柜头;sR_Shipments = 该柜勾选行
多柜(少见):柜 A 成功、柜 B 失败 → PARTIAL_SUCCESS;B 入补偿队列
单柜:要么 SUCCESS 要么 FAILED(行已在提交前过滤,不再「半柜成功」)
禁止: 同一柜拆多次 SaveContainer 做「行级部分成功」(CC 侧是整柜货件列表)。
重试:补偿队列按柜重试;已成功柜 external_container_id 非空则跳过。
5.8 冲突检测
导入前(确认提交时):
GetContainerList,queryJson.F_ContainerNo = 柜号,近 30 天(StartTime/EndTime)- 若存在非归档/有效记录 → 本柜
CONFLICT,不调用 SaveContainer,状态保持可编辑 - 本库
container_import同柜SUCCESS且同 Message-ID → 幂等跳过 - 本库他邮件已 SUCCESS 同柜 → 阻断并提示原邮件
5.9 SaveContainer 黄金请求样例
{
"loginMark": "11111111-2222-3333-4444-555555555555",
"token": "<from customerLogin>",
"data": "{\"keyValue\":null,\"entity\":{\"F_TransMode\":0,\"F_OperationType\":0,\"F_ContainerNo\":\"MATU2745683\",\"F_CabinetType\":\"40HQ\",\"F_BLCopyCode\":\"\",\"F_ETD\":null,\"F_ETA\":\"2026-07-13\",\"F_LoadPort\":\"\",\"F_Dock\":\"\",\"F_ShippingLineId\":\"\",\"F_Classis\":0,\"F_MemoRemark\":\"邮件预报导入\",\"F_Instruction\":\"\"},\"sR_Shipments\":[{\"F_FBACode\":\"ABQ2\",\"F_Transporter\":\"TRUCK\",\"F_ShipmentID\":\"BAZUS001799251\",\"F_Remark\":\"转POC2\",\"F_FBAID\":\"FBA19G5XJLV4\",\"F_ReferenceId\":\"3NHUDF7E\",\"F_Address\":\"\",\"F_CTNS\":1,\"F_CBM\":0.09,\"F_Weight\":10.69},{\"F_FBACode\":\"FTW1\",\"F_Transporter\":\"TRUCK\",\"F_ShipmentID\":\"\",\"F_FBAID\":\"FBA19GLHTHQ2\",\"F_ReferenceId\":\"5ELVT8ED\",\"F_CTNS\":4,\"F_CBM\":0.21,\"F_Weight\":85.08}]}"
}
成功:code=200,data = 集装箱 id 字符串。
失败:400/500 记入 container_import.last_error;410 触发重登重放一次。
5.10 错误码 → 文案
| code | 文案 |
|---|---|
| 200 | 导入成功 |
| 400 | 业务校验失败:{info} |
| 410 | 登录失效,已自动重试;仍失败请检查 CC 账号 |
| 500 | CC 异常:{info} |
| 网络/超时 | 连接 CC 超时,已入补偿队列 |
6. 邮件接入与状态机
6.1 IMAP
| 项 | 值 |
|---|---|
| 服务器 | imap.qq.com:993 SSL(多主机可配) |
| 鉴权 | 授权码;Gmail/MS 可 OAuth |
| 轮询 | 默认 30min(设置页 imap_settings 优先;.env POLL_INTERVAL_MS 回退);可 IDLE |
| 拉取 | 近 N 天已读+未读(IMAP_LOOKBACK_DAYS 默认 3);按 UID 去重后再 FETCH;单连接 + GET_LOCK |
| 幂等 | 见 §8.2 |
| SMTP | 不做 |
6.2 已读策略(推荐已采纳)
SEARCH SINCE (今天起往前 N-1 天) ← 含已读+未读
→ 过滤 DB 已有 (mailbox, folder, uid) / message_id / raw_hash
→ FETCH 新 UID → 落库 FETCHED(raw.eml + 附件)
→ 立即 STORE \Seen ← 避免重复消费;以 DB 状态为准
→ 同进程 PARSING
→ 成功 PARSED / PENDING_CONFIRM /(指令类可 AUTO_EXECUTING)
→ 失败 PARSE_FAILED(可点「重新解析」,不依赖邮箱未读)
若落库前进程崩溃:邮件可能仍未入库,下轮 lookback 仍会命中;靠幂等键去重。 (历史口径「仅 UNSEEN」已废止。)
6.3 状态机
| 状态 | 含义 | 可执行操作 |
|---|---|---|
FETCHED |
已落库 | 系统自动解析 |
PARSING |
解析中 | 无 |
PARSED |
已解析;类型非 NEW 或未达确认条件 | 查看;Admin 改类型 |
PENDING_CONFIRM |
类型=NEW 且 ≥1 有效货件 | 编辑字段;确认导入 |
IMPORTING |
调用 CC 中 | 禁止重复提交 |
SUCCESS |
全部柜成功 | 查看日志 |
PARTIAL_SUCCESS |
多柜部分成功 | 对失败柜重试 |
FAILED |
单柜失败或全部失败 | 重试;改数据再确认 |
PARSE_FAILED |
解析失败 | 重新解析 |
REJECTED_VALIDATION |
本地校验拒绝(无柜号等) | 编辑/补附件后重解析 |
IGNORED |
人工忽略 | 仅 Admin 从详情触发 |
迁移规则:
| 从 | 条件 | 到 |
|---|---|---|
| FETCHED | 开始解析 | PARSING |
| PARSING | 成功且 type=NEW 且有效行≥1 | PENDING_CONFIRM |
| PARSING | 成功且其他类型 | PARSED |
| PARSING | 异常/无模板 | PARSE_FAILED |
| PARSING | 无柜号等核心缺失 | REJECTED_VALIDATION |
| PENDING_CONFIRM | 用户确认 | IMPORTING |
| IMPORTING | 全成功 | SUCCESS |
| IMPORTING | 多柜部分成功 | PARTIAL_SUCCESS |
| IMPORTING | 失败 | FAILED |
| PARSE_FAILED | 重新解析 | PARSING |
| PARSED | Admin 改类型为 NEW 且有效行≥1 | PENDING_CONFIRM |
| * | Admin 忽略 | IGNORED |
7. 前端(含交互细则)
7.1 信息架构 / 主路径
登录 → 邮件列表 → 邮件详情(摘要/附件/证据/解析表)
├─ 类型≠NEW:仅查看(按钮禁用+原因)
└─ 类型=NEW 或 Admin 改类型后 → 确认导入页
→ 提交 → 结果 Toast → 导入日志
详情与确认:两步(详情只读+轻编辑柜头;确认页负责 TransMode/OperationType/行勾选/提交)。
7.2 页面
- 邮件列表 — 主题、发件人、时间、类型色标、状态角标;默认按时间倒序;筛类型/状态
- 邮件详情 — 正文摘要、附件下载、
type_evidence、货件表预览(虚拟列表) - 确认导入 — TransMode/OperationType 必选;柜头可编辑;货件表:
- 默认勾选所有
VALID行;INVALID灰显不可选 - 顶栏:全选有效 / 反选;按 FBACode/ShipmentID 搜索过滤
- 327 行:虚拟滚动;底部显示「已选 n / 有效 m」
- 二次确认对话框:「将向 CC 导入柜 {no},货件 {n} 行」
- 提交后按钮 loading,状态 IMPORTING 防重复
- 默认勾选所有
- 导入日志/补偿 — 按柜成功失败、错误文案、重试按钮
空态:无邮件「等待 IMAP 拉取」;错态见 §5.10。
7.3 Admin「改类型」
- 环境变量
ENABLE_TYPE_OVERRIDE=true或角色=admin 时显示 - 正式运营账号默认隐藏
- Admin 改类型:详情 Modal;
ENABLE_TYPE_OVERRIDE(前端构建侧须同步暴露开关,见 UI 方案);一期不写 audit_log
7.4 线框要点(一期低保真即可)
- 列表:左类型色点,右状态 pill
- 确认页:上柜头表单两列;下表格 + 固定底栏「确认导入」
- 不强制品牌规范;清晰密度优先(客服桌面端为主,手机不一期优化)
8. 数据模型
8.1 表(核心字段)
mail_message
| 列 | 类型 | 说明 |
|---|---|---|
| id | BIGINT PK | |
| message_id | VARCHAR(998) UNIQUE NULL | RFC Message-ID |
| imap_uid | BIGINT | 与 folder 组合唯一 |
| folder | VARCHAR(64) | 默认 INBOX |
| subject, from_addr, received_at | ||
| mail_type | VARCHAR(32) | |
| status | VARCHAR(32) | |
| type_evidence | JSON | |
| snapshot_path | VARCHAR(512) | |
| raw_hash | CHAR(64) | 正文+关键指纹 |
| created_at / updated_at |
唯一: UNIQUE(message_id)(NULL 不冲突时用下条);UNIQUE(folder, imap_uid);UNIQUE(raw_hash) 兜底。
mail_attachment — mail_id, filename, sha256, path, size, template_id
parse_result — mail_id UNIQUE, container_header JSON, shipments JSON, lineage JSON
container_import — mail_id, container_no, external_id, request_body, response_body, status, last_error
import_compensation — import_id, retry_count, next_retry_at, max 3
cc_token_cache — login_mark, token, expire_at
audit_log — actor, action, payload JSON, created_at
sender_customer_map — 可空,一期不阻断
8.2 幂等键(无 Message-ID 时)
优先级:Message-ID → folder+imap_uid → sha256(Date+From+Subject+附件名列表)。
8.3 附件处理
- xlsx/csv 直接解析;xls 转读;zip 解压一层取其中 xlsx/csv(邮件3)
- 多 sheet:优先名含「卡派」否则第一 sheet
- 表头别名失败 →
PARSE_FAILED+ 缺失列列表 - 单附件 ≤20MB;超限拒收记 REJECTED_VALIDATION
8.4 存储
./data volume;保留 30 天(可配 DATA_RETENTION_DAYS)。
9. 技术架构
Next.js Web ──确认导入──▶ CC Adapter ──▶ test.saas.../SaveContainer
│ ▲
▼ │ token
MySQL 8 ◀──解析入库── worker (IMAP only)
- Worker:只负责 IMAP + 解析;不调 SaveContainer
- Web:确认导入、补偿重试、Admin
- compose:
web/worker/mysql - 多 worker:用 MySQL
GET_LOCK('imap_poll')互斥,保证单连接语义
9.1 环境变量(统一命名)
DATABASE_URL=mysql://app:***@mysql:3306/email_forecast
POLL_INTERVAL_MS=1800000
IMAP_LOOKBACK_DAYS=3
DATA_RETENTION_DAYS=30
ENABLE_TYPE_OVERRIDE=true
IMAP_HOST=imap.qq.com
IMAP_PORT=993
IMAP_USER=
IMAP_PASS=
CC_API_BASE=https://test.saas.carriercentral.vip/api
CC_SAAS_HEADER=TEST
CC_LOGIN_MARK=
CC_USERNAME=
CC_PASSWORD_PLAIN= # 服务端 MD5;或直接 CC_PASSWORD_MD5=
APP_ADMIN_USER=
APP_ADMIN_PASS=
10. 开发路线与 DoD
| 阶段 | 交付 | DoD(可测) |
|---|---|---|
| M0 | 脚手架 compose | docker compose up 打开登录页 |
| M1 | IMAP 绑定 + 列表详情 | 绑定邮箱后可拉取;列表/详情可用 |
| M2 | 卡派解析 + 规则 | M2 shipments 行数=327;核心列金样例通过 |
| M3 | CC 客户端 | mock/TEST 烟雾 1 柜 code=200 |
| M4 | 确认导入 | 勾选 2 行 → 请求仅 2 货件;冲突柜阻断;失败可重试 |
| M5 | 船司匹配 + /logs + README | 演示全流程 + 运维文档(无审计页) |
原则:独立 Next 栈;只复用 CC HTTP 契约;样例驱动。
11. 测试策略
| 层 | 内容 |
|---|---|
| 单元 | 类型规则(含「不转仓」负例)、渠道映射、ISO 柜号、状态迁移 |
| 契约 | SaveContainer 请求 snapshot 与黄金样例 diff;410 重登重放 |
| 组件/集成 | xlsx 解析 327 行;zip 解压 |
| E2E | IMAP 拉取 →(可选 Admin 改类型)→ 确认导入(CC mock) |
| 手工 | 真实 IMAP(可选);TEST 烟雾 |
金样例文件(实现时落地): docx/金样例/M1.json … M4.json(期望 type + header 摘要 + shipments 前 2 行)。本期文档附录见 §14。
12. 运营 SOP(一期)
| 场景 | 动作 | 责任 |
|---|---|---|
| 邮件未进系统 | 查 worker 日志 / IMAP 授权码;看是否已读但 PARSE_FAILED | 研发值班 |
| 解析失败 | 详情点「重新解析」;仍失败则人工走客户端预报 | 运营 |
| 类型不对但要导入 | Admin 改类型(需授权)后确认 | 运营主管 |
| 导入失败 | 日志看文案;改字段重试或补偿重试 ≤3 | 运营 |
| 与客户端重复预报 | 冲突阻断后,以 CC 已有单为准,忽略邮件或改柜号 | 运营 |
| 客户追问结果 | 一期无回信 → 运营自行邮件/IM 回复 | 运营 |
失败不自动通知客户(Out of Scope)。
13. 非功能
- 日邮件 <100;附件 ≤20MB;IMAP 单连接(锁互斥)
- 导入串行;确认页防重复提交
- 拉取/导入日志保留:随 DATA_RETENTION_DAYS(默认 30)
- 准确率:金样例 4/4;不承诺 99% 直至标注集扩大
14. 异常场景与系统韧性设计
实现与 QA 以全文为准:
docx/异常场景与系统韧性设计.md(含每条:异常描述 / 系统行为 / 降级 / 告警 / 用户结果)。
下文为强制覆盖维度与关键决策摘要,与 §5–§8、§12 SOP 对齐。
14.0 韧性常量
| 常量 | 值 |
|---|---|
IMPORT_LOCK_TTL |
120s |
IMPORTING_TIMEOUT |
180s |
PARSING_STALE |
10min |
COMPENSATION_MAX_RETRY |
3 |
IMAP_CONNECT_TIMEOUT / READ |
30s / 60s |
CC_HTTP_TIMEOUT |
60s |
CC_REPLAY_ON_410 |
1 |
SHIPMENT_ROW_SOFT_LIMIT / HARD |
1000 / 5000 |
14.1 输入层异常
| 场景 | 落点状态/字段 | 系统行为 |
|---|---|---|
| 无主题 | subject="(无主题)" |
继续 PARSING |
| 无正文且无附件 | REJECTED_VALIDATION / NO_BODY_NO_ATTACHMENT |
阻断导入 |
| 无支持表格(非 xlsx/csv,zip 无有效表) | PARSE_FAILED / NO_SUPPORTED_SPREADSHEET |
可「重新解析」 |
| 缺仓库ID/渠道/件数列 | PARSE_FAILED + missing_columns |
阻断 |
| 柜号非 ISO 6346 | REJECTED_VALIDATION;container_no_valid=false |
可手改再校验;不自动猜号 |
| 行半空(CTNS≤0 / 仓空) | row_status=INVALID |
跳过行,不挡其他 VALID |
| 附件 >20MB | REJECTED_VALIDATION / ATTACHMENT_TOO_LARGE |
不落全量附件;仍 \Seen |
| 渠道未映射 | CHANNEL_UNMAPPED |
可 VALID;提交需 ack_unmapped_channels=true |
| 体积/重量非数字 | 字段 null |
行仍可 VALID |
| 日期/地址错误 | 日期 null;地址截断 500 | 确认页可改;时区 Asia/Shanghai |
14.2 用户行为与并发
- 重复/并发确认:前端 disabled +
Idempotency-Key;DBUPDATE status PENDING_CONFIRM→IMPORTING;失败者 409。 - 刷新:以 DB
status为准;不自动重放 SaveContainer。 - 回退/前进:非
PENDING_CONFIRM禁止提交并重定向详情。 - 多标签:
version乐观锁 → 409 VERSION_CONFLICT。 - Admin 改类型:二次确认 +
audit_log;SUCCESS后禁止改;PARSING中改类型 → 409 MAIL_BUSY。
14.3 网络与通信
- IMAP 超时/断开:释放
GET_LOCK('imap_poll'),下轮重试;连续失败 ≥3 → 顶栏红条 +imap.poll_fail。 - CC 超时或响应丢失:禁止盲重放 SaveContainer;
TIMEOUT_UNKNOWN入补偿;用GetContainerList核对后收敛SUCCESS或允许重试。 - 弱网重复 POST:靠幂等键 + 条件更新吞掉。
- 静态资源失败:Error boundary;不影响 worker。
14.4 CarrierCentral API
customerLogin5xx/失败:清cc_token_cache+内存;重试登录 ≤2;仍失败整单FAILED。- SaveContainer 中途 410:重登并重放 1 次(
CC_REPLAY_ON_410)。 - SaveContainer 400:该柜
FAILED,不自动重试;多柜可PARTIAL_SUCCESS。 GetContainerList失败:默认阻断导入;ENABLE_FORCE_IMPORT=true才可强跳(一期无 audit_log)。- 响应 schema 漂移:
code=200但无柜 id → 走超时不确定核对流程。 - 浏览器永不持有 CC token。
14.5 解析与附件
- MIME 损坏:快照保留 →
PARSE_FAILED/MIME_PARSE_ERROR。 - zip:只解 一层;嵌套 zip 忽略并
warn;损坏 →ZIP_EXTRACT_FAILED。 - 无「卡派」sheet → 回退第一 sheet。
- 行数 >5000 →
REJECTED_VALIDATION;>1000 → 落库但默认不全选 + 警告。 - 正文/附件柜号冲突 → 确认页强制人选,默认阻断提交。
- 类型同分:严格执行 §4.2;
type_evidence落库。 - 「重新解析」:禁止在
IMPORTING|SUCCESS|PARTIAL_SUCCESS。
14.6 状态机与生命周期
- 落库成功但
\Seen失败:幂等跳过新建,仅重试 Seen。 PARSING超过PARSING_STALE:自动回收(现行可置 PARSE_FAILED 后可再解析;目标语义回 FETCHED)。IMPORTING超过IMPORTING_TIMEOUT:转失败/待核查 + 补偿,禁止永久 IMPORTING。- 补偿
retry_count≥3→COMPENSATION_EXHAUSTED,停自动重试,人工重置。 - 无通用 status PATCH;非法迁移拒绝。
14.7 缓存与幂等
- Token:DB
cc_token_cache为权威;内存跟随;多实例 410 时抢GET_LOCK('cc_login')重登。 - 幂等序:
Message-ID→folder+imap_uid→raw_hash。 raw_hash碰撞且 Message-ID 不同:分叉新建 +error告警。- 同 Message-ID 重复拉取:唯一约束吞掉。
14.8 并发与锁
- Worker:
GET_LOCK('imap_poll');拿不到锁本轮退出(正常)。 - 跨邮件同柜并发确认:活跃柜号占位唯一 + §5.8;后提交阻断。
- 补偿调度:已
SUCCESS/IMPORTING跳过;retry_token幂等。
14.9 数据计算
- UNMAPPED 渠道:无
ack_unmapped_channels则提交 API 400。 - CBM/Weight:
DECIMAL(18,4),提交前 round 4 位。 - 船司匹配失败:
F_ShippingLineId留空,不阻断。 - 勾选数以服务端
accepted_count为准,不一致warn。
14.10 权限与安全
- 未登录 API → 401。
- 改类型:服务端校验
role=admin && ENABLE_TYPE_OVERRIDE→ 否则 403。 - 导入限流:10 次/用户/分钟 → 429。
- 改类型 / 确认导入 / 强制跳过冲突 / 补偿重试 / 忽略 → 全写
audit_log。
14.11 监控与 QA
全文 §11 指标阈值、§12 用例索引 A-1.x…A-10.x(见独立文件)。
15. 附录:金样例期望(摘要)
M1
{
"id": "M1",
"subject_contains": "TIIU8073522-90022",
"mail_type": "UNKNOWN",
"shipments_expected": 0,
"importable": false
}
M2
{
"id": "M2",
"subject_contains": "MATU2745683",
"mail_type": "TRANSFER",
"container_no": "MATU2745683",
"shipments_expected": 327,
"sample_rows": [
{ "F_FBACode": "ABQ2", "F_Transporter": "TRUCK", "F_CTNS": 1, "F_FBAID": "FBA19G5XJLV4" },
{ "F_FBACode": "FTW1", "F_Transporter": "TRUCK", "F_CTNS": 4, "F_FBAID": "FBA19GLHTHQ2" }
],
"importable_default": false,
"importable_after_admin_type_override_to_NEW": true
}
M3
{
"id": "M3",
"subject_contains": "WHSU5574991",
"mail_type": "INSTRUCTION_HOLD_SPLIT",
"notes": "卡转海/贴标为证据分;主类型按 §4 同分规则",
"importable": false
}
M4
{
"id": "M4",
"subject_contains": "新增预报",
"mail_type": "INSTRUCTION_LABEL",
"notes": "当前 Message 为换标指令;不拆历史新增预报",
"importable": false
}
16. 开放项
- TEST 客户账号密码(谁提供)
- 「提拆派组合柜」默认 OperationType 是否维持 0
- 生产 API Base / Header
- 上线门禁:补「新增预报+清单」真邮件
- 运营登录:简单账号 vs SSO
17. 索引
| 资料 | 路径 |
|---|---|
| 本文 v0.2 | docx/需求规格-邮件自动预报-v0.2.md |
| 异常与韧性(全文) | docx/异常场景与系统韧性设计.md |
| v0.1 | docx/需求规格-邮件自动预报-v0.1.md |
| 接口 PDF | docx/接口/carriercentral客户端通用接口V1.72docx(4).pdf |
| 样例 | docx/邮件/ |
| ccnew TEST | X:\work\ccnew\...\App_Data\Saas\TEST.json |
| 实施计划 | .cursor/rules/implementation-plan.mdc |