|
|
# 邮件自动预报系统 — 需求规格 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/邮件`、`ccnew` TEST、接口 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 请求信封
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"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 冲突检测
|
|
|
|
|
|
导入前(确认提交时):
|
|
|
|
|
|
1. `GetContainerList`,`queryJson.F_ContainerNo = 柜号`,近 30 天(`StartTime/EndTime`)
|
|
|
2. 若存在非归档/有效记录 → 本柜 `CONFLICT`,不调用 SaveContainer,状态保持可编辑
|
|
|
3. 本库 `container_import` 同柜 `SUCCESS` 且同 Message-ID → 幂等跳过
|
|
|
4. 本库他邮件已 SUCCESS 同柜 → 阻断并提示原邮件
|
|
|
|
|
|
### 5.9 SaveContainer 黄金请求样例
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"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 页面
|
|
|
|
|
|
1. **邮件列表** — 主题、发件人、时间、类型色标、状态角标;默认按时间倒序;筛类型/状态
|
|
|
2. **邮件详情** — 正文摘要、附件下载、`type_evidence`、货件表预览(虚拟列表)
|
|
|
3. **确认导入** — TransMode/OperationType 必选;柜头可编辑;货件表:
|
|
|
- 默认勾选所有 `VALID` 行;`INVALID` 灰显不可选
|
|
|
- 顶栏:全选有效 / 反选;按 FBACode/ShipmentID 搜索过滤
|
|
|
- 327 行:**虚拟滚动**;底部显示「已选 n / 有效 m」
|
|
|
- 二次确认对话框:「将向 CC 导入柜 {no},货件 {n} 行」
|
|
|
- 提交后按钮 loading,状态 IMPORTING 防重复
|
|
|
4. **导入日志/补偿** — 按柜成功失败、错误文案、重试按钮
|
|
|
|
|
|
空态:无邮件「等待 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 环境变量(统一命名)
|
|
|
|
|
|
```bash
|
|
|
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`](./异常场景与系统韧性设计.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`;DB `UPDATE 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
|
|
|
|
|
|
- `customerLogin` 5xx/失败:清 `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
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"id": "M1",
|
|
|
"subject_contains": "TIIU8073522-90022",
|
|
|
"mail_type": "UNKNOWN",
|
|
|
"shipments_expected": 0,
|
|
|
"importable": false
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### M2
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"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
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"id": "M3",
|
|
|
"subject_contains": "WHSU5574991",
|
|
|
"mail_type": "INSTRUCTION_HOLD_SPLIT",
|
|
|
"notes": "卡转海/贴标为证据分;主类型按 §4 同分规则",
|
|
|
"importable": false
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### M4
|
|
|
|
|
|
```json
|
|
|
{
|
|
|
"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` |
|