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