|
|
# 异常场景与系统韧性设计(邮件自动预报系统专用)
|
|
|
|
|
|
> 本章为 `docx/需求规格-邮件自动预报-v0.2.md` 第 14 章正文;与 §5–§8 状态机、CC 接口、数据模型、运营 SOP 一一对应。
|
|
|
> 常量约定:`IMPORT_LOCK_TTL=120s`;`IMPORTING_TIMEOUT=180s`;`PARSING_STALE=10min`;`COMPENSATION_MAX_RETRY=3`;`IMAP_CONNECT_TIMEOUT=30s`;`IMAP_READ_TIMEOUT=60s`;`CC_HTTP_TIMEOUT=60s`;`CC_REPLAY_ON_410=1`;`SHIPMENT_ROW_SOFT_LIMIT=1000`;`SHIPMENT_ROW_HARD_LIMIT=5000`。
|
|
|
;**`IMAP_LOOKBACK_DAYS=3`**。
|
|
|
> **2026-07-23 回写**:拉取=近 N 天已读+未读;**一期无 audit_log**;详见 `docx/现行口径-修订说明-v1.md`。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. 输入层异常处理
|
|
|
|
|
|
### 1.1 邮件缺失主题
|
|
|
- **异常描述**:IMAP 入库的 Message `subject` 为空或仅空白。
|
|
|
- **系统行为**:`mail_message.subject` 存 `"(无主题)"`;类型判定仅用正文+附件名;照常进入 `PARSING`。
|
|
|
- **是否允许降级**:是(继续解析)。
|
|
|
- **是否触发告警**:`warn` 日志 `mail.subject_missing`;不计监控告警。
|
|
|
- **用户看到的结果**:列表主题显示「(无主题)」;详情正常。
|
|
|
|
|
|
### 1.2 邮件缺失正文
|
|
|
- **异常描述**:纯附件邮件或正文 HTML/纯文本均为空。
|
|
|
- **系统行为**:正文摘要空串;类型与柜头字段仅从附件/主题提取;无附件则 `REJECTED_VALIDATION`,`last_error=NO_BODY_NO_ATTACHMENT`。
|
|
|
- **是否允许降级**:有附件则继续;无附件则阻断导入路径。
|
|
|
- **是否触发告警**:无附件 → `error` + 指标 `mail.reject_validation`。
|
|
|
- **用户看到的结果**:状态「校验拒绝」;提示「无正文且无附件,无法解析」;可下载原始快照。
|
|
|
|
|
|
### 1.3 邮件无附件
|
|
|
- **异常描述**:类型可能为 NEW/TRANSFER,但无 xlsx/csv/zip。
|
|
|
- **系统行为**:若主题/正文能抽出柜号则写入 `parse_result.container_header`,`shipments=[]`;`mail_type` 仍按 §4 判定;因有效行=0,即使 NEW 也不进 `PENDING_CONFIRM`,停留 `REJECTED_VALIDATION` 或 `PARSED`(非 NEW)+ `last_error=NO_SHIPMENT_ROWS`。
|
|
|
- **是否允许降级**:是(仅展示,不可导入)。
|
|
|
- **是否触发告警**:`warn` `parse.no_attachment`。
|
|
|
- **用户看到的结果**:详情提示「缺少卡派/装箱清单附件」;确认导入入口不可用。
|
|
|
|
|
|
### 1.4 附件格式非 xlsx/csv,且 zip 内无有效文件
|
|
|
- **异常描述**:仅有 pdf/图片/docx,或 zip 解压后无 xlsx/csv/xls。
|
|
|
- **系统行为**:附件仍写入 `mail_attachment`;解析器标记 `template_id=null`;状态 `PARSE_FAILED`,`last_error=NO_SUPPORTED_SPREADSHEET`。
|
|
|
- **是否允许降级**:附件可下载;不调用 SaveContainer。
|
|
|
- **是否触发告警**:`error` `parse.unsupported_attachment`。
|
|
|
- **用户看到的结果**:「不支持的附件格式」+「重新解析」按钮;运营按 SOP 走客户端预报。
|
|
|
|
|
|
### 1.5 附件关键列缺失(仓库ID / 渠道 / 件数)
|
|
|
- **异常描述**:卡派资料表头缺少映射到 `F_FBACode`/`F_Transporter`/`F_CTNS` 的列。
|
|
|
- **系统行为**:整表无法建有效行 → `PARSE_FAILED`,`lineage.missing_columns=[...]`;若仅部分行缺值见 1.7。
|
|
|
- **是否允许降级**:否(不可进 `PENDING_CONFIRM`)。
|
|
|
- **是否触发告警**:`error` `parse.missing_columns`。
|
|
|
- **用户看到的结果**:展示缺失列名列表;可重新解析。
|
|
|
|
|
|
### 1.6 柜号格式非法(非 ISO 6346)
|
|
|
- **异常描述**:抽出柜号未通过校验位(且未关闭 ISO 校验配置)。
|
|
|
- **系统行为**:`container_header.F_ContainerNo` 保留原值并标 `container_no_valid=false`;状态 `REJECTED_VALIDATION`;禁止确认导入。
|
|
|
- **是否允许降级**:Admin 可在详情手工改柜号后触发「重新校验」;不自动猜正确柜号。
|
|
|
- **是否触发告警**:`warn` `parse.invalid_container_no`。
|
|
|
- **用户看到的结果**:「柜号未通过 ISO 校验:{no}」;编辑柜号后可再校验。
|
|
|
|
|
|
### 1.7 货件行半空(件数=0、仓库ID 空等)
|
|
|
- **异常描述**:单行缺少 `F_FBACode` 或 `F_Transporter` 或 `F_CTNS`≤0/空。
|
|
|
- **系统行为**:该行 `row_status=INVALID`,写入 `shipments[]`;不阻断其他 VALID 行;默认不勾选。
|
|
|
- **是否允许降级**:是(柜级部分行导入,§5.7)。
|
|
|
- **是否触发告警**:INVALID 行占比 >30% → `warn` `parse.high_invalid_ratio`。
|
|
|
- **用户看到的结果**:灰显行 + tooltip 列缺失原因;底栏「已选 n / 有效 m」。
|
|
|
|
|
|
### 1.8 超大附件(>20MB)
|
|
|
- **异常描述**:单附件 `size > 20MB`。
|
|
|
- **系统行为**:不落盘完整文件(或落盘后立即拒绝解析);`mail_attachment` 记 size + `rejected=1`;邮件 `REJECTED_VALIDATION`,`last_error=ATTACHMENT_TOO_LARGE`;仍按 §6.2 标 `\Seen`。
|
|
|
- **是否允许降级**:否。
|
|
|
- **是否触发告警**:`error` `mail.attachment_too_large`。
|
|
|
- **用户看到的结果**:「附件超过 20MB,请客户拆分后重发」;快照仅保留邮件头+正文。
|
|
|
|
|
|
### 1.9 渠道映射词不匹配
|
|
|
- **异常描述**:渠道列值不在 §5.6 黄金表。
|
|
|
- **系统行为**:`F_Transporter`=原文截断 30 字符;行标 `CHANNEL_UNMAPPED`;**仍可为 VALID**(若三必填齐全);确认页该行黄标警告。
|
|
|
- **是否允许降级**:是(不阻断提交);CC 若 400 再走补偿。
|
|
|
- **是否触发告警**:`warn` `map.channel_unmapped`(带原词)。
|
|
|
- **用户看到的结果**:警告「渠道未映射:{raw},将原样提交 CC」;可手工改下拉为标准枚举。
|
|
|
|
|
|
### 1.10 体积/重量含非数字
|
|
|
- **异常描述**:`总体积`/`毛重` 含单位文字或非法字符。
|
|
|
- **系统行为**:剥离常见单位(m³/cbm/kg)后 `parseFloat`;失败则该字段 `null`,行仍 VALID(体积重量非必填);`lineage` 记 `FIELD_PARSE_FALLBACK`。
|
|
|
- **是否允许降级**:是。
|
|
|
- **是否触发告警**:仅 `debug`/`info`。
|
|
|
- **用户看到的结果**:对应单元格空;可手工补填。
|
|
|
|
|
|
### 1.11 地址/日期格式错误
|
|
|
- **异常描述**:送仓日期无法解析,或地址超长。
|
|
|
- **系统行为**:日期统一按 `Asia/Shanghai` 解析;失败则 `F_Expected_DeliveryDateB/E=null`;地址截断至 500 字符并 `warn`。
|
|
|
- **是否允许降级**:是。
|
|
|
- **是否触发告警**:日期失败 `warn` `parse.bad_date`。
|
|
|
- **用户看到的结果**:日期空或截断提示;确认页可改。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 2. 用户行为与并发控制
|
|
|
|
|
|
### 2.1 确认页重复点击提交
|
|
|
- **异常描述**:用户连点「确认导入」。
|
|
|
- **系统行为**:前端按钮立即 `disabled` + loading;请求带 `Idempotency-Key=mail_id:container_no:shipments_hash`;后端对 `mail_message` 执行 `UPDATE ... SET status='IMPORTING' WHERE id=? AND status='PENDING_CONFIRM'`,影响行=0 则返回 `409 ALREADY_IMPORTING`。
|
|
|
- **是否允许降级**:第二次请求直接返回当前状态,不二次调 CC。
|
|
|
- **是否触发告警**:`info` `import.duplicate_click`。
|
|
|
- **用户看到的结果**:首次进入「导入中」;再次点击 Toast「正在导入,请勿重复提交」。
|
|
|
|
|
|
### 2.2 快速连续提交(并发)
|
|
|
- **异常描述**:两次请求几乎同时到达。
|
|
|
- **系统行为**:DB 行锁/条件更新串行化;仅赢家进入 CC 调用;输家 409。
|
|
|
- **是否允许降级**:否(严格单飞)。
|
|
|
- **是否触发告警**:`warn` 若 1 分钟内同 mail 409>3 → `import.contention`。
|
|
|
- **用户看到的结果**:同 2.1。
|
|
|
|
|
|
### 2.3 页面刷新导致状态丢失或重复请求
|
|
|
- **异常描述**:提交后刷新,前端本地 state 清空。
|
|
|
- **系统行为**:一切以 DB `status` 为准;刷新后拉取详情,若 `IMPORTING/SUCCESS/FAILED` 展示对应 UI;不自动重放未完成请求(除非用户点重试)。
|
|
|
- **是否允许降级**:是(只读恢复)。
|
|
|
- **是否触发告警**:否。
|
|
|
- **用户看到的结果**:看到真实状态角标;IMPORTING 显示进度/等待。
|
|
|
|
|
|
### 2.4 浏览器回退/前进
|
|
|
- **异常描述**:确认页回退到详情再前进。
|
|
|
- **系统行为**:确认页 `beforeunload` 不拦截只读浏览;若 status 已非 `PENDING_CONFIRM`,确认页挂载时重定向详情并禁用提交。
|
|
|
- **是否允许降级**:是。
|
|
|
- **是否触发告警**:否。
|
|
|
- **用户看到的结果**:「当前状态不可导入:{status}」。
|
|
|
|
|
|
### 2.5 多标签页同时操作同一邮件
|
|
|
- **异常描述**:两标签同时改字段或提交。
|
|
|
- **系统行为**:提交用乐观锁 `mail_message.updated_at` 或 `version` 字段;冲突返回 `409 VERSION_CONFLICT`;改类型接口同锁。
|
|
|
- **是否允许降级**:后写失败,需刷新。
|
|
|
- **是否触发告警**:`warn` `mail.version_conflict`。
|
|
|
- **用户看到的结果**:「数据已被其他页面更新,请刷新后重试」。
|
|
|
|
|
|
### 2.6 Admin 改类型误操作
|
|
|
- **异常描述**:误将 LABEL 改为 NEW 并准备导入。
|
|
|
- **系统行为**:改类型二次确认弹窗展示旧→新 + 证据分;写入 `audit_log`;支持「改回」若尚未 `IMPORTING`;已 SUCCESS 禁止改类型。
|
|
|
- **是否允许降级**:人工改回。
|
|
|
- **是否触发告警**:每次改类型 `info` 审计;1 小时同用户改类型>10 → `warn`。
|
|
|
- **用户看到的结果**:弹窗确认;一期无审计页。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 3. 网络与通信容错
|
|
|
|
|
|
### 3.1 IMAP 连接断开/超时
|
|
|
- **异常描述**:connect>30s 或 read>60s,或 TLS 中断。
|
|
|
- **系统行为**:本次轮询 abort;释放 `GET_LOCK('imap_poll')`;下个 `POLL_INTERVAL` 重试;已 `FETCHED` 的邮件不受影响。
|
|
|
- **是否允许降级**:retry(无限轮询,指数退避上限 5min 仅当连续失败≥5)。
|
|
|
- **是否触发告警**:连续失败≥3 → `error` 监控 `imap.poll_fail`;列表顶栏红条「邮件同步异常」。
|
|
|
- **用户看到的结果**:已有邮件仍可操作;新邮件暂不出现。
|
|
|
|
|
|
### 3.2 前端调 CC 超时/断开
|
|
|
- **异常描述**:Web→自有 API 或自有 API→CC 超过 `CC_HTTP_TIMEOUT`。
|
|
|
- **系统行为**:若请求已发出且本地未收到响应:柜状态先标 `FAILED` 或入 `import_compensation`(`reason=TIMEOUT_UNKNOWN`);**禁止立即自动重放 SaveContainer**(防双写);需 `GetContainerList` 确认柜是否已存在后再决定重试或标 SUCCESS。
|
|
|
- **是否允许降级**:补偿队列 + 人工确认重试。
|
|
|
- **是否触发告警**:`error` `cc.timeout`。
|
|
|
- **用户看到的结果**:「导入结果不确定,已加入待核查队列」;日志页显示「超时待核对」。
|
|
|
|
|
|
### 3.3 请求已发送但响应丢失(部分成功不确定)
|
|
|
- **异常描述**:CC 可能已建柜,客户端读超时。
|
|
|
- **系统行为**:同 3.2;核对流程:用柜号查 `GetContainerList`;命中近 10 分钟新建 → 回写 `external_id` 并 `SUCCESS`;未命中 → 允许补偿重试。
|
|
|
- **是否允许降级**:核查后自动收敛。
|
|
|
- **是否触发告警**:`error` `cc.ambiguous_success`。
|
|
|
- **用户看到的结果**:状态「待核查」直到收敛。
|
|
|
|
|
|
### 3.4 弱网重试导致重复请求
|
|
|
- **异常描述**:浏览器/代理自动重试 POST。
|
|
|
- **系统行为**:依赖 `Idempotency-Key` + 条件更新 `PENDING_CONFIRM→IMPORTING`;CC 侧再经 §5.8 冲突检测。
|
|
|
- **是否允许降级**:幂等吞掉重复。
|
|
|
- **是否触发告警**:`info`。
|
|
|
- **用户看到的结果**:单次成功结果。
|
|
|
|
|
|
### 3.5 CDN/静态资源加载失败
|
|
|
- **异常描述**:JS/CSS chunk 404。
|
|
|
- **系统行为**:Next error boundary 展示「资源加载失败,请强刷」;不影响已运行的 worker/IMAP。
|
|
|
- **是否允许降级**:用户手动刷新。
|
|
|
- **是否触发告警**:前端上报 `warn` `ui.chunk_fail`(若有)。
|
|
|
- **用户看到的结果**:整页错误壳,非静默白屏。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 4. 后端 API(CarrierCentral)容错策略
|
|
|
|
|
|
### 4.1 customerLogin 返回 410 或 5xx
|
|
|
- **异常描述**:登录失败或 token 接口异常。
|
|
|
- **系统行为**:清 `cc_token_cache` + 进程内存 token;5xx 退避重试登录最多 2 次;仍失败则导入整体 `FAILED`,`last_error=CC_LOGIN_FAILED`。
|
|
|
- **是否允许降级**:否(无法调用业务 API)。
|
|
|
- **是否触发告警**:`error` `cc.login_fail`。
|
|
|
- **用户看到的结果**:§5.10「登录失效/CC 异常」文案;运营检查 `CC_USERNAME`。
|
|
|
|
|
|
### 4.2 SaveContainer 业务调用中途 410
|
|
|
- **异常描述**:token 过期发生在 SaveContainer。
|
|
|
- **系统行为**:重登 1 次并**重放同一 Idempotency 载荷 1 次**(`CC_REPLAY_ON_410=1`);仍 410 → FAILED。
|
|
|
- **是否允许降级**:单次重放。
|
|
|
- **是否触发告警**:重放成功 `info`;失败 `error`。
|
|
|
- **用户看到的结果**:成功则无感;失败见 §5.10。
|
|
|
|
|
|
### 4.3 SaveContainer 返回 400(含柜号已存在)
|
|
|
- **异常描述**:CC 业务校验失败。
|
|
|
- **系统行为**:该柜 `container_import.status=FAILED`;`last_error=info`;**不自动重试**(业务错误);邮件多柜则其他柜继续 → 可能 `PARTIAL_SUCCESS`。
|
|
|
- **是否允许降级**:人工改数据后重新确认(若邮件回到可编辑:从 FAILED 允许「重新打开确认」仅当无任何 SUCCESS 柜;已有 SUCCESS 柜则仅补偿失败柜)。
|
|
|
- **是否触发告警**:`warn` `cc.save_400`。
|
|
|
- **用户看到的结果**:「业务校验失败:{info}」。
|
|
|
|
|
|
### 4.4 GetContainerList 超时或数据不全
|
|
|
- **异常描述**:冲突检测调用失败。
|
|
|
- **系统行为**:**默认阻断导入**(`CONFLICT_CHECK_FAILED`),不盲调 SaveContainer;运营可 Admin 强制跳过冲突检测(需 `ENABLE_FORCE_IMPORT=true`)。
|
|
|
- **是否允许降级**:仅 Admin 强制。
|
|
|
- **是否触发告警**:`error` `cc.list_timeout`。
|
|
|
- **用户看到的结果**:「无法校验柜号是否已存在,请稍后重试」。
|
|
|
|
|
|
### 4.5 响应结构变化(字段新增/删除)
|
|
|
- **异常描述**:`data` 非预期类型或缺柜 id。
|
|
|
- **系统行为**:`code=200` 但无法解析 `external_id` → 走 3.2 不确定流程(列表核对);解析层用宽松 schema(忽略未知字段)。
|
|
|
- **是否允许降级**:核对收敛。
|
|
|
- **是否触发告警**:`error` `cc.schema_drift`。
|
|
|
- **用户看到的结果**:「返回异常,已待核查」。
|
|
|
|
|
|
### 4.6 相同 loginMark「重复提交」语义
|
|
|
- **异常描述**:CC 侧会话与多次 SaveContainer。
|
|
|
- **系统行为**:本系统不轮换 loginMark(进程级固定);重复保护完全靠本库幂等 + 冲突检测,不依赖 CC 对 loginMark 去重。
|
|
|
- **是否允许降级**:本库幂等。
|
|
|
- **是否触发告警**:否。
|
|
|
- **用户看到的结果**:无重复柜(冲突则阻断)。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 5. 邮件解析与附件处理容错(重点)
|
|
|
|
|
|
### 5.1 IMAP 拉取成功但无法解析原始内容
|
|
|
- **异常描述**:损坏 MIME / 编码异常。
|
|
|
- **系统行为**:原始字节仍写入 `snapshot_path`;`PARSE_FAILED`,`last_error=MIME_PARSE_ERROR`;已 `\Seen`。
|
|
|
- **是否允许降级**:人工下载快照;「重新解析」可重试。
|
|
|
- **是否触发告警**:`error` `parse.mime_error`。
|
|
|
- **用户看到的结果**:解析失败页 + 下载原始邮件。
|
|
|
|
|
|
### 5.2 zip 解压失败或嵌套多层
|
|
|
- **异常描述**:损坏 zip,或有效 xlsx 在二层目录。
|
|
|
- **系统行为**:仅解压**一层**;失败 → `PARSE_FAILED` `ZIP_EXTRACT_FAILED`;一层内递归目录找 xlsx/csv;超过一层嵌套 zip **不进入**,记 `warn` `zip.nested_ignored`。
|
|
|
- **是否允许降级**:否(需客户重发扁平附件)。
|
|
|
- **是否触发告警**:失败 `error`;嵌套忽略 `warn`。
|
|
|
- **用户看到的结果**:对应错误文案。
|
|
|
|
|
|
### 5.3 多 sheet 找不到「卡派」
|
|
|
- **异常描述**:无名称含「卡派」的 sheet。
|
|
|
- **系统行为**:回退第一 sheet;若第一 sheet 表头映射失败再 `PARSE_FAILED`。
|
|
|
- **是否允许降级**:fallback 第一 sheet。
|
|
|
- **是否触发告警**:`info` `parse.sheet_fallback`。
|
|
|
- **用户看到的结果**:详情显示「使用 Sheet:{name}」。
|
|
|
|
|
|
### 5.4 表头别名匹配失败
|
|
|
- **异常描述**:列名与映射表无一命中必填语义。
|
|
|
- **系统行为**:同 1.5 → `PARSE_FAILED` + `missing_columns`。
|
|
|
- **是否允许降级**:否。
|
|
|
- **是否触发告警**:`error`。
|
|
|
- **用户看到的结果**:缺失列列表。
|
|
|
|
|
|
### 5.5 货件行数过大(>1000 / >5000)
|
|
|
- **异常描述**:解析行数超过软/硬限制。
|
|
|
- **系统行为**:`>SHIPMENT_ROW_HARD_LIMIT(5000)` → `REJECTED_VALIDATION` 不落全量 JSON;`>SOFT(1000)` 且 ≤HARD → 落库,确认页强制虚拟滚动,默认**不**全选,需用户筛选后勾选,底栏警告。
|
|
|
- **是否允许降级**:软限制降级;硬限制拒绝。
|
|
|
- **是否触发告警**:软 `warn`;硬 `error` `parse.row_limit`。
|
|
|
- **用户看到的结果**:硬限「行数过多请拆分」;软限「行数较多,请筛选后勾选导入」。
|
|
|
|
|
|
### 5.6 柜号提取错误
|
|
|
- **异常描述**:正文柜号与附件柜号不一致,或正则误匹配。
|
|
|
- **系统行为**:附件柜号优先(§ 结构化优先);冲突写入 `lineage` `CONTAINER_NO_CONFLICT`;确认页大红警告,**默认阻断提交**直至人工选定柜号(下拉:附件值/正文值/手输)。
|
|
|
- **是否允许降级**:人工选定。
|
|
|
- **是否触发告警**:`warn` `parse.container_conflict`。
|
|
|
- **用户看到的结果**:必须选择柜号后才能确认。
|
|
|
|
|
|
### 5.7 类型判定信号冲突
|
|
|
- **异常描述**:多类型得分接近或同分。
|
|
|
- **系统行为**:严格按 §4.2 同分优先级执行;`type_evidence` 完整落库;UI 展示各类型得分。
|
|
|
- **是否允许降级**:Admin 改类型。
|
|
|
- **是否触发告警**:得分差≤5 → `info` `type.close_call`。
|
|
|
- **用户看到的结果**:证据面板;非 NEW 导入按钮禁用原因含「当前类型=…」。
|
|
|
|
|
|
### 5.8 重新解析
|
|
|
- **异常描述**:用户/系统对 `PARSE_FAILED`/`REJECTED_VALIDATION`/`PARSED` 点重新解析。
|
|
|
- **系统行为**:仅当 status∉{IMPORTING,SUCCESS,PARTIAL_SUCCESS};置 `PARSING`;覆盖 `parse_result`;写审计。
|
|
|
- **是否允许降级**:是。
|
|
|
- **是否触发告警**:`info`。
|
|
|
- **用户看到的结果**:解析中 → 新结果。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 6. 状态机与生命周期保护
|
|
|
|
|
|
### 6.1 落库成功但标 `\Seen` 失败
|
|
|
- **异常描述**:DB 已有 `FETCHED`,IMAP STORE 失败。
|
|
|
- **系统行为**:邮件保持可解析;下次 lookback 再次命中 UID → 幂等键命中则**跳过新建**,仅重试 `\Seen`;不重复解析除非强制。
|
|
|
- **是否允许降级**:retry Seen。
|
|
|
- **是否触发告警**:连续 Seen 失败 `warn` `imap.seen_fail`。
|
|
|
- **用户看到的结果**:列表仍一条;无重复卡片。
|
|
|
|
|
|
### 6.2 进程崩溃卡在 `PARSING`
|
|
|
- **异常描述**:worker 死于解析中。
|
|
|
- **系统行为**:定时任务(每 1min):`status=PARSING AND updated_at < now-PARSING_STALE` → 目标重置为 `FETCHED` 再入队;现行可先置 `PARSE_FAILED` 后「重新解析」(待收敛)。一期不写 audit_log。
|
|
|
- **是否允许降级**:自动恢复。
|
|
|
- **是否触发告警**:`error` `state.stale_parsing`。
|
|
|
- **用户看到的结果**:短暂后恢复为已解析或失败态。
|
|
|
|
|
|
### 6.3 `IMPORTING` 超时未更新
|
|
|
- **异常描述**:超过 `IMPORTING_TIMEOUT=180s`。
|
|
|
- **系统行为**:回收任务将状态改为 `FAILED` 或 `TIMEOUT_UNKNOWN` 入补偿;触发 3.2 核对;禁止无限 IMPORTING。
|
|
|
- **是否允许降级**:补偿+核对。
|
|
|
- **是否触发告警**:`error` `state.importing_timeout`。
|
|
|
- **用户看到的结果**:「导入超时,请至日志核查/重试」。
|
|
|
|
|
|
### 6.4 解析与 Admin 改类型并发
|
|
|
- **异常描述**:PARSING 中同时改类型。
|
|
|
- **系统行为**:改类型 API 拒绝非终态解析中:`409 MAIL_BUSY`;解析结束后以解析器写的 `mail_type` 为准,Admin 需再改。
|
|
|
- **是否允许降级**:否。
|
|
|
- **是否触发告警**:`info`。
|
|
|
- **用户看到的结果**:「邮件正在解析,请稍后再改类型」。
|
|
|
|
|
|
### 6.5 补偿队列丢记录或超最大重试
|
|
|
- **异常描述**:`retry_count >= COMPENSATION_MAX_RETRY(3)` 或行被误删。
|
|
|
- **系统行为**:超限标 `COMPENSATION_EXHAUSTED`,停止自动重试;监控告警;运营手工重试重置 `retry_count`(Admin)。
|
|
|
- **是否允许降级**:人工介入。
|
|
|
- **是否触发告警**:`error` `compensation.exhausted`。
|
|
|
- **用户看到的结果**:日志「重试已耗尽,请联系值班」;SOP §12。
|
|
|
|
|
|
### 6.6 非法状态迁移
|
|
|
- **异常描述**:客户端伪造 status 回写。
|
|
|
- **系统行为**:所有迁移仅服务端按 §6.3 表执行;无通用 status PATCH。
|
|
|
- **是否允许降级**:否。
|
|
|
- **是否触发告警**:`warn` `state.illegal_transition`。
|
|
|
- **用户看到的结果**:400。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 7. 缓存一致性与幂等设计
|
|
|
|
|
|
### 7.1 DB token 过期但内存仍有效
|
|
|
- **异常描述**:`cc_token_cache.expire_at` 已过,进程内存未清。
|
|
|
- **系统行为**:每次用 token 前比较 DB `expire_at`;以 **DB 为权威**;过期强制重登;内存跟随失效。
|
|
|
- **是否允许降级**:重登。
|
|
|
- **是否触发告警**:`info` `cc.token_refresh`。
|
|
|
- **用户看到的结果**:无感或一次短暂延迟。
|
|
|
|
|
|
### 7.2 多实例 token 刷新未同步
|
|
|
- **异常描述**:实例 A 重登写入 DB,实例 B 仍持旧 token。
|
|
|
- **系统行为**:B 收到 410 → 读 DB 最新 token;若仍 410 → 抢锁重登(`GET_LOCK('cc_login')`)写 DB。
|
|
|
- **是否允许降级**:410 重放路径。
|
|
|
- **是否触发告警**:`warn` 频繁 410 `cc.token_churn`。
|
|
|
- **用户看到的结果**:同 §5.10。
|
|
|
|
|
|
### 7.3 raw_hash 碰撞
|
|
|
- **异常描述**:不同邮件 hash 相同(极低概率)。
|
|
|
- **系统行为**:幂等顺序:Message-ID → folder+uid → raw_hash;hash 命中时若 Message-ID 不同则**不合并**,改用 folder+uid 新建并 `error` `idempotency.hash_collision`。
|
|
|
- **是否允许降级**:分叉新建。
|
|
|
- **是否触发告警**:`error`(必告警)。
|
|
|
- **用户看到的结果**:两条独立邮件记录。
|
|
|
|
|
|
### 7.4 同一 Message-ID 被多次处理
|
|
|
- **异常描述**:重复投递/重复拉取。
|
|
|
- **系统行为**:`UNIQUE(message_id)` 插入失败 → 吞掉;若需更新 `\Seen` 则只做 IMAP。
|
|
|
- **是否允许降级**:幂等跳过。
|
|
|
- **是否触发告警**:`info` `idempotency.msgid_dup`。
|
|
|
- **用户看到的结果**:仍一条。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 8. 并发一致性与锁策略
|
|
|
|
|
|
### 8.1 多 worker 锁竞争失败
|
|
|
- **异常描述**:`GET_LOCK('imap_poll', 0)` 返回 0。
|
|
|
- **系统行为**:本轮直接退出,等待下轮;不报错为故障。
|
|
|
- **是否允许降级**:是(正常互斥)。
|
|
|
- **是否触发告警**:连续 30 轮拿不到锁且无其他实例心跳 → `error` `imap.lock_stuck`(锁持有方崩溃未释放极罕见,MySQL 连接断即释放)。
|
|
|
- **用户看到的结果**:无。
|
|
|
|
|
|
### 8.2 同一柜号多封邮件同时确认
|
|
|
- **异常描述**:两封 PENDING 邮件含同柜,两运营同时提交。
|
|
|
- **系统行为**:提交事务内:`SELECT ... FOR UPDATE` 锁 `container_import` 占位行 `UNIQUE(container_no) WHERE status IN ('IMPORTING','SUCCESS')` 或等价唯一活跃索引;第二人冲突检测失败阻断;第一人 CC 成功后第二人必拦。
|
|
|
- **是否允许降级**:后提交失败。
|
|
|
- **是否触发告警**:`warn` `conflict.cross_mail_container`。
|
|
|
- **用户看到的结果**:「柜号已被邮件#{id}占用/已存在」。
|
|
|
|
|
|
### 8.3 多 Admin 同时改类型并提交
|
|
|
- **异常描述**:改类型+导入竞态。
|
|
|
- **系统行为**:`version` 乐观锁;导入前再次校验 `mail_type=NEW_CONTAINER`;类型已变则 409。
|
|
|
- **是否允许降级**:刷新重来。
|
|
|
- **是否触发告警**:`warn`。
|
|
|
- **用户看到的结果**:版本冲突提示。
|
|
|
|
|
|
### 8.4 补偿队列同一柜重复重试
|
|
|
- **异常描述**:调度器重复投递补偿任务。
|
|
|
- **系统行为**:补偿执行前锁 `container_import`;已 SUCCESS 跳过;`IMPORTING` 跳过;用 `retry_token` 幂等。
|
|
|
- **是否允许降级**:跳过。
|
|
|
- **是否触发告警**:`info`。
|
|
|
- **用户看到的结果**:单次结果。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 9. 数据计算安全
|
|
|
|
|
|
### 9.1 CHANNEL_UNMAPPED 未阻断
|
|
|
- **异常描述**:未知渠道仍提交(见 1.9)。
|
|
|
- **系统行为**:确认页强制展示警告计数;提交 API 若存在 UNMAPPED 且未传 `ack_unmapped_channels=true` → 400 要求确认。
|
|
|
- **是否允许降级**:用户显式确认后放行。
|
|
|
- **是否触发告警**:`warn`。
|
|
|
- **用户看到的结果**:二次确认「存在未映射渠道,仍要提交?」。
|
|
|
|
|
|
### 9.2 浮点精度(体积/重量)
|
|
|
- **异常描述**:Excel 浮点噪声。
|
|
|
- **系统行为**:CBM/Weight 存 DECIMAL(18,4);展示与提交前 `round(value,4)`;不在服务端做单位换算乘法链。
|
|
|
- **是否允许降级**:四舍五入。
|
|
|
- **是否触发告警**:否。
|
|
|
- **用户看到的结果**:最多 4 位小数。
|
|
|
|
|
|
### 9.3 日期时区错误
|
|
|
- **异常描述**:解析为 UTC 导致差一天。
|
|
|
- **系统行为**:所有业务日期按 `Asia/Shanghai` 日历日;写入 CC 格式 `YYYY-MM-DD`;`received_at` 存 UTC 瞬时并 UI 转上海。
|
|
|
- **是否允许降级**:固定时区。
|
|
|
- **是否触发告警**:跨日边界单测覆盖;运行时否。
|
|
|
- **用户看到的结果**:与邮件 ETA 日历日一致。
|
|
|
|
|
|
### 9.4 F_ShippingLineId 匹配失败
|
|
|
- **异常描述**:船名无法匹配。
|
|
|
- **系统行为**:留空;确认页可选船司;**不阻断**提交(字段非 SaveContainer 必填)。
|
|
|
- **是否允许降级**:是。
|
|
|
- **是否触发告警**:`info`。
|
|
|
- **用户看到的结果**:「未匹配船司,可手动选择」。
|
|
|
|
|
|
### 9.5 勾选总数与提交数不一致
|
|
|
- **异常描述**:前端展示 n,实际 payload m。
|
|
|
- **系统行为**:服务端以请求体 `shipment_ids[]` 为准再查 `parse_result`;过滤非 VALID/非本 mail;响应回传 `accepted_count`;与前端 n 不符则 UI 以服务端为准刷新。
|
|
|
- **是否允许降级**:服务端权威。
|
|
|
- **是否触发告警**:不一致 `warn` `import.count_mismatch`。
|
|
|
- **用户看到的结果**:Toast「实际提交 {accepted_count} 行」。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 10. 权限与安全控制
|
|
|
|
|
|
### 10.1 未登录访问 API
|
|
|
- **异常描述**:无 session 调 `/api/mails`、`/api/import`。
|
|
|
- **系统行为**:统一 401;不返回业务数据。
|
|
|
- **是否允许降级**:否。
|
|
|
- **是否触发告警**:暴力 401 计数 `security.unauthorized`。
|
|
|
- **用户看到的结果**:跳转登录。
|
|
|
|
|
|
### 10.2 越权查看(一期单租户)
|
|
|
- **异常描述**:伪造 mail_id 扫库。
|
|
|
- **系统行为**:一期单租户:登录即可访问全部邮件;禁止未登录;二期按 `sender_customer_map`/客户隔离时校验 `customer_code`。
|
|
|
- **是否允许降级**:一期 N/A。
|
|
|
- **是否触发告警**:否(一期)。
|
|
|
- **用户看到的结果**:正常列表(仅登录用户)。
|
|
|
|
|
|
### 10.3 Admin 改类型未授权
|
|
|
- **异常描述**:`ENABLE_TYPE_OVERRIDE=false` 但前端暴露按钮;或普通用户调 API。
|
|
|
- **系统行为**:**服务端**校验 `role=admin AND ENABLE_TYPE_OVERRIDE`;失败 403;前端按同条件隐藏(不可作唯一防线)。
|
|
|
- **是否允许降级**:否。
|
|
|
- **是否触发告警**:`warn` `security.type_override_denied`。
|
|
|
- **用户看到的结果**:无按钮或 403。
|
|
|
|
|
|
### 10.4 伪造 loginMark / CC token
|
|
|
- **异常描述**:客户端上传假 token 想直打 CC。
|
|
|
- **系统行为**:浏览器**永不**持有 CC token;仅服务端 Adapter 使用;用户会话与 CC 凭证隔离。
|
|
|
- **是否允许降级**:否。
|
|
|
- **是否触发告警**:出现客户端传 cc_token 字段 → `error` `security.cc_token_probe`。
|
|
|
- **用户看到的结果**:忽略非法字段。
|
|
|
|
|
|
### 10.5 批量刷导入接口
|
|
|
- **异常描述**:脚本对多邮件狂打确认。
|
|
|
- **系统行为**:用户级限流:确认导入 **10 次/分钟**;同 mail 条件更新防重;超限 429。
|
|
|
- **是否允许降级**:限流。
|
|
|
- **是否触发告警**:`warn` `security.rate_limit_import`。
|
|
|
- **用户看到的结果**:「操作过于频繁」。
|
|
|
|
|
|
### 10.6 审计(一期不做)
|
|
|
- **现行口径**:不建 `audit_log`、无 Admin 审计页;关键操作依赖应用日志与 `/logs`。
|
|
|
- **系统行为**:原写审计点可空实现,不阻断主流程。
|
|
|
- **是否允许降级**:是(已降级为不做)。
|
|
|
- **用户看到的结果**:无审计查询入口。
|
|
|
|
|
|
|
|
|
## 11. 监控指标与告警阈值(落地用)
|
|
|
|
|
|
| 指标 | 级别 | 阈值 |
|
|
|
|---|---|---|
|
|
|
| `imap.poll_fail` 连续 | error | ≥3 轮 |
|
|
|
| `cc.login_fail` | error | ≥1/5min |
|
|
|
| `cc.ambiguous_success` | error | ≥1 |
|
|
|
| `state.stale_parsing` | error | ≥1 |
|
|
|
| `state.importing_timeout` | error | ≥1 |
|
|
|
| `compensation.exhausted` | error | ≥1 |
|
|
|
| `idempotency.hash_collision` | error | ≥1 |
|
|
|
| `parse.high_invalid_ratio` | warn | 单邮件 INVALID>30% |
|
|
|
| `import.contention` | warn | 同 mail 409>3/min |
|
|
|
| `security.rate_limit_import` | warn | 触发即记 |
|
|
|
|
|
|
一期告警出口:应用日志 + 管理页顶栏;企微后置。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 12. QA 异常用例索引(按本章)
|
|
|
|
|
|
| 编号 | 对应用例要点 |
|
|
|
|---|---|
|
|
|
| A-1.x | 无主题/无正文/无附件/坏格式/缺列/坏柜号/半空行/20MB+/未映射渠道/坏数字日期 |
|
|
|
| A-2.x | 双击提交、双标签 version、刷新恢复、回退、误改类型 |
|
|
|
| A-3.x | IMAP 断网、CC 超时、模糊成功核对 |
|
|
|
| A-4.x | login 5xx、Save 400、List 超时阻断、410 重放 |
|
|
|
| A-5.x | 坏 MIME、坏 zip、sheet 回退、1001 行软限、柜号冲突、重新解析 |
|
|
|
| A-6.x | Seen 失败去重、卡 PARSING 回收、IMPORTING 超时 |
|
|
|
| A-7.x | token DB 权威、Message-ID 重复 |
|
|
|
| A-8.x | 双 worker 锁、双邮件同柜 |
|
|
|
| A-9.x | unmapped 需 ack、时区、accepted_count |
|
|
|
| A-10.x | 401/403 改类型、429 限流、无 CC token 出前端 |
|
|
|
|
|
|
### 12.1 自动化覆盖(2026-08-12)
|
|
|
|
|
|
跑法:`pnpm test`(单测/集成)+ `pnpm test:e2e tests/e2e/resilience-auth.spec.ts`(需本机可起 web)。
|
|
|
|
|
|
| 编号 | 覆盖 | 自动化入口 | 真人/联调仍需 |
|
|
|
|---|---|---|---|
|
|
|
| A-1.1 空主题 | ✅ | `resilience-scenarios` + `resilience-metrics` | UI 列表展示「(无主题)」 |
|
|
|
| A-1.2/1.3 无正文无附件 / NEW 无清单 | ✅ | `resolveNoSpreadsheetOutcome` | 详情文案 |
|
|
|
| A-1.4 非表格附件 | ✅ | 同上 | 重新解析按钮 |
|
|
|
| A-1.5 缺列 | ✅ | packing-list | — |
|
|
|
| A-1.6 坏柜号 ISO | ✅ | iso6346 + scenarios | Admin 改柜号再校验 |
|
|
|
| A-1.7 半空行 INVALID | ✅ | packing-list scenarios | 确认页灰显 |
|
|
|
| A-1.8 >20MB | ⚠️ 规则在 pipeline | 未造 20MB fixture | 真附件拒收 + `\Seen` |
|
|
|
| A-1.9 未映射渠道 | ✅ | channel-map + packing-list | 确认页黄标 + ack |
|
|
|
| A-1.10 体积重量单位 | ✅ | packing-list | — |
|
|
|
| A-1.11 日期/地址截断 | ✅ | dates + packing-list | — |
|
|
|
| A-2.1/2.2 连点/并发 | ✅ 门禁+409 码 | forecast-gate + ConfirmImportError | 真双击/双请求打 DB CAS |
|
|
|
| A-2.3 刷新恢复 | ⚠️ | 无独立 E2E | 导入中刷新看 DB 状态 |
|
|
|
| A-2.4 回退确认页 | ⚠️ | 无独立 E2E | 浏览器前进后退 |
|
|
|
| A-2.5 version 冲突 | ✅ 错误码 | ConfirmImportError | 双标签真打 |
|
|
|
| A-2.6 误改类型 | ⚠️ API 有 MAIL_BUSY | type/route | Admin 二次确认弹窗 |
|
|
|
| A-3.1 IMAP 超时退避 | ✅ 阈值/退避公式 | runtime-status | 真断网轮询 |
|
|
|
| A-3.2/3.3 模糊成功窗口 | ✅ 10min 窗口 | compensation.isRecentlyCreated | live GetContainerList |
|
|
|
| A-3.4 弱网重试 | ⚠️ 依赖幂等键 | import CAS | 代理重放 POST |
|
|
|
| A-3.5 chunk 404 | ❌ | 无 | 强刷 error boundary |
|
|
|
| A-4.1 login token 解析 | ✅ extractLoginToken | auth.ts | live 5xx/410 |
|
|
|
| A-4.2 Save 410 重放 1 次 | ⚠️ http.ts 有 retried410 | 无单测打真实 410 | live token 过期 |
|
|
|
| A-4.3 Save 400 | ⚠️ mock 冲突 | cc-save + force-import | live 业务 400 文案 |
|
|
|
| A-4.4 List 超时阻断 | ⚠️ 代码默认阻断 | force-import | live timeout |
|
|
|
| A-4.5 schema drift | ✅ token/id 宽松解析 | extractLoginToken | 缺 F_Id → TIMEOUT_UNKNOWN |
|
|
|
| A-5.1 坏 MIME | ⚠️ pipeline MIME_PARSE_ERROR | 无坏 eml fixture | 重新解析+下载快照 |
|
|
|
| A-5.2 zip 一层/嵌套/穿越 | ✅ | zip-extract | — |
|
|
|
| A-5.3 sheet 回退 | ✅ | packing-list sheetFallback | — |
|
|
|
| A-5.4 表头失败 | ✅ 同 1.5 | — | — |
|
|
|
| A-5.5 软/硬行数 | ✅ hardLimit/softLimit | packing-list | 确认页虚拟滚动 UI |
|
|
|
| A-5.6 柜号冲突 | ⚠️ 规则有 | pipeline lineage | 确认页必须选定柜号 |
|
|
|
| A-5.7 类型 close call | ⚠️ classify 有样例 | classify.test | 证据面板 |
|
|
|
| A-5.8 重新解析门禁 | ✅ 状态机 | state-machine | 点按钮 |
|
|
|
| A-6.1 Seen 失败去重 | ❌ 需 IMAP | — | lookback 重试 Seen |
|
|
|
| A-6.2 卡 PARSING 回收 | ✅ 允许 PARSING→FETCHED | state-machine | worker reaper 真跑 |
|
|
|
| A-6.3 IMPORTING 超时 | ✅ 允许 IMPORTING→FAILED | state-machine | reaper + 补偿行 |
|
|
|
| A-6.4 解析中改类型 | ⚠️ API MAIL_BUSY | type/route | 并发点改类型 |
|
|
|
| A-6.5 补偿耗尽 | ⚠️ 代码有 EXHAUSTED | compensation | Admin 手工重置 |
|
|
|
| A-6.6 非法迁移 | ✅ | state-machine | — |
|
|
|
| A-7.1/7.2 token DB 权威 | ⚠️ 实现有 | 无多实例测 | 双 web 410 |
|
|
|
| A-7.3 hash 碰撞分叉 | ✅ | hash + resilience-metrics | — |
|
|
|
| A-7.4 Message-ID 重复/截断 | ✅ truncate 191 | snapshot | UNIQUE 插入失败 |
|
|
|
| A-8.1 双 worker 锁 | ❌ 需双进程+MySQL | db-lock | 双 worker lock_busy |
|
|
|
| A-8.2 双邮件同柜 | ⚠️ conflict mock | cc-save | 两运营同时确认 |
|
|
|
| A-8.3 改类型+导入竞态 | ⚠️ version | ConfirmImportError | — |
|
|
|
| A-8.4 补偿重复投递 | ⚠️ retry_token | compensation | worker 双 tick |
|
|
|
| A-9.1 unmapped ack | ✅ 错误码 + warning | ConfirmImportError | 确认页二次确认 |
|
|
|
| A-9.2 round4 | ✅ | dates | — |
|
|
|
| A-9.3 时区 | ✅ | hash-dates | — |
|
|
|
| A-9.4 船司匹配失败不阻断 | ⚠️ extract-header | extract-header.test | 确认页手选 |
|
|
|
| A-9.5 accepted_count/hash | ✅ shipments_hash | hash | Toast |
|
|
|
| A-10.1 未登录 401 | ✅ E2E | resilience-auth.spec | — |
|
|
|
| A-10.2 单租户 | N/A 一期 | — | — |
|
|
|
| A-10.3 改类型 403 | ⚠️ requireAdmin + ENABLE_TYPE_OVERRIDE | type/route | ops 调 API |
|
|
|
| A-10.4 前端无 CC token | ⚠️ 架构约束 | 无扫 bundle | Network 面板 |
|
|
|
| A-10.5 导入/登录限流 | ✅ 单测 + E2E 登录 429 | rate-limit + e2e | 多副本无效 |
|
|
|
| A-10.6 审计一期不做 | N/A | — | — |
|
|
|
|
|
|
**图例**:✅ 自动化已断言 · ⚠️ 代码有路径、缺完整 fixture/联调 · ❌ 尚未自动化(需 IMAP/双进程/真人 UI)
|