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

34 KiB

异常场景与系统韧性设计(邮件自动预报系统专用)

本章为 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)