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/异常场景与系统韧性设计.md

573 lines
34 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.

# 异常场景与系统韧性设计(邮件自动预报系统专用)
> 本章为 `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)