You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mail-yubao/docx/需求规格-邮件自动预报-v0.2.md

717 lines
31 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 邮件自动预报系统 — 需求规格 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` |