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/技术设计方案-邮件自动预报-v1.0.md

26 KiB

技术设计方案 v1.0(邮件自动预报系统)

状态:可直接开发(2026-07-23 现行口径回写;2026-08-03 指令拆分产品规则锁定)
依据:docx/需求规格-邮件自动预报-v0.2.md、docx/异常场景与系统韧性设计.md、docx/产品规则-邮件指令识别与拆分.md
栈锁定:Next.js App Router + MySQL 8 + docker-compose(web / worker / mysql)
回写要点:src/worker;IMAP lookback N 天;无 audit_log;路由 /confirm /logs /settings;本地 MySQL 7023;seed 仅账号;详见 docx/现行口径-修订说明-v1.md
指令口径:主题/正文/附件分工、仅最新段可确认、软词→客户指令 — 见产品规则页;实现见 instruction-lexicon.ts / split-instructions.ts(无 LLM)


1. 细化技术选型

1.1 ORM

维度 Prisma Drizzle TypeORM
AI 生成 Schema 高(schema 声明式) 高(TS 表定义) 中
类型安全 生成客户端强类型 推断强 装饰器弱于前两者
迁移 prisma migrate 成熟 drizzle-kit 可用 易踩坑
JSON 字段 原生支持 原生支持 一般

唯一推荐:Prisma 5.x
理由:Cursor 生成/改 schema 成功率最高;迁移命令固定;与 Next.js 官方示例一致;Json 字段直接对应 type_evidence / shipments。

1.2 UI 组件库

维度 shadcn/ui Ant Design MUI
客服后台 中(需自拼表格) 高(Table/Form 开箱) 高
虚拟滚动 327+ @tanstack/react-virtual 自接 Table virtual 文档成熟 需 DataGrid Pro 或自接
定制 最高 中 中
包体/样式冲突 低 中 中

唯一推荐:Ant Design 5.x + @tanstack/react-virtual(仅确认页货件表)
理由:列表/筛选/Form/Modal 一次齐;确认页用 TanStack Virtual 包 Antd Table 行,避免 Pro 授权;后台信息密度匹配运营场景。

1.3 Worker

维度 setInterval 脚本 BullMQ PM2 only
对齐 60s 轮询 直接 过重 仅进程管理
GET_LOCK 互斥 业务内实现 队列锁重复 不解决业务锁
compose 复杂度 低(第二容器) 需 Redis 需宿主机 PM2

唯一推荐:独立 Node 入口 worker/index.ts + setInterval(POLL_INTERVAL_MS) + MySQL GET_LOCK('imap_poll', 0)
理由:PRD 明确单连接轮询,无任务扇出;不加 Redis;docker 内 node dist/worker.js 常驻即可。Compose 用 restart: unless-stopped,不用 PM2。

1.4 其余锁定

项 选型
语言 TypeScript 5.x strict
运行时 Node.js 20 LTS
Next 15.x App Router
鉴权(一期) 自建 session:iron-session + 环境变量账号;Cookie HttpOnly
HTTP(CC) undici(Node 原生 fetch 封装层 CcHttpClient)
IMAP imapflow
邮件解析 mailparser
xlsx exceljs
zip adm-zip(只解一层)
密码 MD5 crypto.createHash('md5')
校验 zod(API 入参 + 解析中间态)
日志 pino
包管理 pnpm
测试 vitest(单元)+ playwright(E2E,M4+)

2. 系统架构设计

2.1 总体架构(文字)

imap.qq.com:993
       │ SINCE N天(已读+未读) / STORE \Seen
       ▼
┌────────────────── worker 容器 ──────────────────┐
│  ImapPoller → SnapshotStore → ParsePipeline     │
│       │              │              │           │
│       └──────────────┴──────────────┘           │
│                      ▼                          │
│              Prisma → MySQL                     │
│   (FETCHED→PARSING→PARSED|PENDING_CONFIRM|…)    │
└─────────────────────────────────────────────────┘
                       ▲
                       │ 读写同一库
┌────────────────── web 容器 ─────────────────────┐
│  Next.js App Router                             │
│  UI: 列表/详情/确认导入/日志                     │
│  Route Handlers: /api/*                         │
│  ImportService → ConflictCheck → CcAdapter      │
│       │                              │          │
│       │                              ▼          │
│       │             test.saas.carriercentral.vip│
│       │            customerLogin / SaveContainer│
│       │            GetContainerList / ShipLine  │
│       └────────── MySQL (status/import/audit) ──┘
└─────────────────────────────────────────────────┘

硬边界:

  • 人工确认路径(NEW_CONTAINER):仅 Web API 在用户点击确认后调用 SaveContainer。
  • 自动执行路径(TRANSFER / INSTRUCTION_HOLD_SPLIT / INSTRUCTION_LABEL):允许 worker/executor 调用对应 CC 写接口,受 ENABLE_AUTO_EXEC_* + cc_write_capability(live|mock|off)+ 审计约束;mock ≠ TEST 验收。
  • 禁止对超时/未知态盲重放写接口。

2.2 模块划分

模块 路径 输入 输出
IMAP 拉取 src/worker + src/services/imap 设置页 mailbox + IMAP_LOOKBACK_DAYS mail_message FETCHED + 快照/附件
解析引擎 src/services/parse mail_id mail_type + parse_result + 新 status
类型判定 src/services/parse/classify.ts subject/body/filenames MailType + type_evidence
卡派解析 src/services/parse/packing-list.ts xlsx/csv buffer header + shipments[]
状态机 src/services/state-machine.ts from/to/guard 合法迁移或抛 IllegalTransition
CC Adapter src/services/cc 业务 DTO CC 响应;token 缓存
确认导入 src/services/import mail_id + 勾选行 + 柜头 container_import + 终态
补偿重试 src/services/compensation compensation id 重试 SaveContainer
前端 src/app/(ops)/* — 运营 UI

2.3 数据流

1. ImapPoller.GET_LOCK('imap_poll')
2. SEARCH SINCE N天 → 过滤已入库 UID → FETCH → 写 data/mails/{id}/raw.eml + attachments
3. INSERT mail_message status=FETCHED(幂等:message_id / folder+uid / raw_hash)
4. STORE \Seen(失败仅记日志,下轮补 Seen)
5. status→PARSING → classify + packing-list
6. NEW + valid rows≥1 → PENDING_CONFIRM;否则 PARSED / PARSE_FAILED / REJECTED_VALIDATION
7. 用户 POST /api/mails/:id/import
8. CAS: PENDING_CONFIRM→IMPORTING(version 校验)
9. GetContainerList 冲突检测 → 失败则回滚可编辑态并返回 CONFLICT
10. 按柜 SaveContainer(一柜一请求;货件=勾选行)
11. 全成功 SUCCESS;多柜部分 PARTIAL_SUCCESS;失败 FAILED + import_compensation

2.4 第三方依赖

依赖 用途 超时
imap.qq.com:993 拉取/标已读 connect 30s / read 60s
https://test.saas.carriercentral.vip/api CC HTTP 60s
本地 ./data 快照与附件 —
exceljs / adm-zip 清单解析 单附件 ≤20MB

3. 数据库设计

Prisma schema 落地;下列 SQL 语义等同。字符集 utf8mb4,引擎 InnoDB。

3.1 mail_message

CREATE TABLE mail_message (
  id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  message_id      VARCHAR(998) NULL,
  imap_uid        BIGINT UNSIGNED NULL,
  folder          VARCHAR(64) NOT NULL DEFAULT 'INBOX',
  subject         VARCHAR(998) NOT NULL DEFAULT '(无主题)',
  from_addr       VARCHAR(320) NOT NULL DEFAULT '',
  received_at     DATETIME(3) NULL,
  body_text       MEDIUMTEXT NULL,
  mail_type       VARCHAR(32) NOT NULL DEFAULT 'UNKNOWN',
  status          VARCHAR(32) NOT NULL DEFAULT 'FETCHED',
  type_evidence   JSON NULL,
  snapshot_path   VARCHAR(512) NULL,
  raw_hash        CHAR(64) NULL,
  last_error      VARCHAR(1000) NULL,
  version         INT UNSIGNED NOT NULL DEFAULT 1,
  created_at      DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  updated_at      DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  UNIQUE KEY uk_message_id (message_id),
  UNIQUE KEY uk_folder_uid (folder, imap_uid),
  UNIQUE KEY uk_raw_hash (raw_hash),
  KEY idx_status_received (status, received_at),
  KEY idx_mail_type (mail_type)
);

mail_type 枚举值:NEW_CONTAINER|TRANSFER|INSTRUCTION_HOLD_SPLIT|INSTRUCTION_LABEL|WORK_ORDER|UNKNOWN
工单关键词(贴标/拦截/拍照/转仓/快递单号)覆盖为 WORK_ORDER;解析产出 parse_result.mail_record(提单 BL_FORECAST / 工单表);本期工单不 auto-exec。
status 枚举值:PRD §6.3 全量。

3.2 mail_attachment

CREATE TABLE mail_attachment (
  id           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  mail_id      BIGINT UNSIGNED NOT NULL,
  filename     VARCHAR(512) NOT NULL,
  content_type VARCHAR(128) NULL,
  sha256       CHAR(64) NOT NULL,
  path         VARCHAR(512) NOT NULL,
  size         INT UNSIGNED NOT NULL,
  rejected     TINYINT(1) NOT NULL DEFAULT 0,
  template_id  VARCHAR(64) NULL,
  created_at   DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  KEY idx_mail (mail_id),
  CONSTRAINT fk_att_mail FOREIGN KEY (mail_id) REFERENCES mail_message(id)
);

3.3 parse_result

CREATE TABLE parse_result (
  id                BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  mail_id           BIGINT UNSIGNED NOT NULL,
  container_header  JSON NOT NULL,
  shipments         JSON NOT NULL,
  lineage           JSON NULL,
  created_at        DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  updated_at        DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  UNIQUE KEY uk_mail (mail_id),
  CONSTRAINT fk_parse_mail FOREIGN KEY (mail_id) REFERENCES mail_message(id)
);

shipments[] 元素契约:

{
  row_index: number;
  row_status: 'VALID' | 'INVALID';
  invalid_reasons?: string[];
  warnings?: string[]; // e.g. CHANNEL_UNMAPPED
  F_FBACode?: string;
  F_Transporter?: string;
  F_CTNS?: number;
  F_CBM?: number | null;
  F_Weight?: number | null;
  F_FBAID?: string | null;
  F_ReferenceId?: string | null;
  F_ShipmentID?: string | null;
  F_Address?: string | null;
  F_Expected_DeliveryDateB?: string | null; // YYYY-MM-DD
  F_Expected_DeliveryDateE?: string | null;
  F_Remark?: string | null;
  channel_raw?: string;
}

container_header 契约:含 F_ContainerNo, container_no_valid, F_CabinetType, F_ETA, F_ETD, F_BLCopyCode, F_Classis, F_ShippingLineId, F_MemoRemark, F_Instruction, 推荐 F_TransMode/F_OperationType。

3.4 container_import

CREATE TABLE container_import (
  id            BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  mail_id       BIGINT UNSIGNED NOT NULL,
  container_no  VARCHAR(50) NOT NULL,
  external_id   VARCHAR(64) NULL,
  request_body  MEDIUMTEXT NULL,
  response_body MEDIUMTEXT NULL,
  status        VARCHAR(32) NOT NULL, -- PENDING|IMPORTING|SUCCESS|FAILED|CONFLICT|TIMEOUT_UNKNOWN
  last_error    VARCHAR(1000) NULL,
  shipments_hash CHAR(64) NOT NULL,
  created_at    DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  updated_at    DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  KEY idx_mail (mail_id),
  KEY idx_container (container_no),
  UNIQUE KEY uk_mail_container_shiphash (mail_id, container_no, shipments_hash),
  CONSTRAINT fk_imp_mail FOREIGN KEY (mail_id) REFERENCES mail_message(id)
);

活跃柜占位(应用层):导入前插入/更新 status IN ('IMPORTING','SUCCESS') 同行柜号冲突检查;另建:

CREATE TABLE container_active_lock (
  container_no  VARCHAR(50) NOT NULL PRIMARY KEY,
  mail_id       BIGINT UNSIGNED NOT NULL,
  import_id     BIGINT UNSIGNED NULL,
  locked_at     DATETIME(3) NOT NULL
);

3.5 import_compensation

CREATE TABLE import_compensation (
  id             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  import_id      BIGINT UNSIGNED NOT NULL,
  retry_count    INT UNSIGNED NOT NULL DEFAULT 0,
  max_retry      INT UNSIGNED NOT NULL DEFAULT 3,
  next_retry_at  DATETIME(3) NULL,
  reason         VARCHAR(64) NOT NULL,
  status         VARCHAR(32) NOT NULL DEFAULT 'OPEN', -- OPEN|DONE|EXHAUSTED
  retry_token    CHAR(36) NOT NULL,
  created_at     DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  updated_at     DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  UNIQUE KEY uk_retry_token (retry_token),
  KEY idx_open (status, next_retry_at),
  CONSTRAINT fk_comp_imp FOREIGN KEY (import_id) REFERENCES container_import(id)
);

3.6 cc_token_cache

CREATE TABLE cc_token_cache (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  login_mark  CHAR(36) NOT NULL,
  token       VARCHAR(128) NOT NULL,
  expire_at   DATETIME(3) NOT NULL,
  updated_at  DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  UNIQUE KEY uk_login_mark (login_mark)
);

3.7 audit_log(一期不做 / 表已移除)

现行口径:不落库、无 Admin 查询页。以下 DDL 仅作历史参考。

3.7.1 (历史)audit_log

CREATE TABLE audit_log (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  actor       VARCHAR(64) NOT NULL,
  action      VARCHAR(64) NOT NULL,
  mail_id     BIGINT UNSIGNED NULL,
  payload     JSON NULL,
  created_at  DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  KEY idx_mail_time (mail_id, created_at)
);

3.8 其余

CREATE TABLE sender_customer_map (
  id             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  match_type     VARCHAR(32) NOT NULL, -- EMAIL|DOMAIN|SUBJECT|ATTACHMENT
  match_value    VARCHAR(320) NOT NULL,
  customer_code  VARCHAR(64) NOT NULL,
  enabled        TINYINT(1) NOT NULL DEFAULT 1,
  UNIQUE KEY uk_match (match_type, match_value)
);

CREATE TABLE app_user (
  id            BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  username      VARCHAR(64) NOT NULL UNIQUE,
  password_hash VARCHAR(128) NOT NULL,
  role          VARCHAR(16) NOT NULL DEFAULT 'ops', -- ops|admin
  created_at    DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);

4. API 设计

Base:/api。鉴权:未登录 → 401。成功包络:

{ ok: true, data: T }
{ ok: false, error: { code: string, message: string, details?: unknown } }

4.1 认证

方法 路径 入参 出参 错误
POST /api/auth/login {username,password} {role} + Set-Cookie 401 INVALID_CREDENTIALS
POST /api/auth/logout — {ok:true} —
GET /api/auth/me — {username,role} 401

4.2 邮件列表

方法 路径 入参 出参
GET /api/mails query: page=1&pageSize=20&mail_type=&status=&q= {items: MailListItem[], total}

MailListItem:id,subject,from_addr,received_at,mail_type,status,container_no?

错误:400 VALIDATION

4.3 邮件详情

方法 路径 出参
GET /api/mails/:id MailDetail:邮件字段 + type_evidence + parse_result + attachments[] + imports[]
GET /api/mails/:id/attachments/:attId/download file stream

错误:404 NOT_FOUND

4.4 确认导入

方法 路径 入参 出参
POST /api/mails/:id/import 见下 {mail_status, imports[], accepted_count}
// body
{
  version: number;
  idempotency_key: string; // mail_id:container_no:shipments_hash 或整单 UUID
  F_TransMode: 0|1|3;
  F_OperationType: 0|2|4;
  container_header: { /* 可覆盖字段 */ };
  selected_row_indexes: number[];
  ack_unmapped_channels?: boolean;
  force_skip_conflict?: boolean; // 需 admin + ENABLE_FORCE_IMPORT
}
错误码 含义
409 ALREADY_IMPORTING CAS 失败
409 VERSION_CONFLICT version 不匹配
400 ACK_UNMAPPED_REQUIRED 未确认未映射渠道
409 CONFLICT_CONTAINER GetContainerList / 本地锁冲突
503 CONFLICT_CHECK_FAILED 列表接口失败且未强跳
429 RATE_LIMITED >10/min
403 FORBIDDEN 非 NEW / 无权限强跳
400 INVALID_ROWS 勾选含 INVALID

限流:每用户 10 次/分钟(内存滑动窗或 DB)。

4.5 Admin 改类型

方法 路径 入参 条件
POST /api/mails/:id/type {version, mail_type, reason} role=admin 且 ENABLE_TYPE_OVERRIDE=true

成功:更新 mail_type;若 NEW 且 valid≥1 → PENDING_CONFIRM;写 audit_log action=TYPE_OVERRIDE。
错误:403 / 409 MAIL_BUSY / 409 VERSION_CONFLICT / 409 IMMUTABLE_STATUS

4.6 重新解析

方法 路径 条件
POST /api/mails/:id/reparse status ∉ `IMPORTING

行为:status→PARSING;worker 或 web 内同步调用 ParsePipeline(一期 web 同步执行解析 亦可;生产可投递 DB 标志位由 worker 捞——一期锁定:API 进程内直接跑 ParsePipeline,与 worker 共用 src/services/parse)。
错误:409 ILLEGAL_TRANSITION

4.7 补偿重试

方法 路径 入参
GET /api/imports page,status,mail_id
POST /api/imports/:importId/retry {retry_token?}
POST /api/compensations/:id/retry — admin 可重置 exhausted

行为:锁 container_import;SUCCESS 跳过;核对超时单;再 SaveContainer。
错误:409 / 400 EXHAUSTED

4.8 忽略邮件

方法 路径
POST /api/mails/:id/ignore

4.9 健康检查

| GET /api/health | {mysql:ok, imap_lock?:string} | 无鉴权 |


5. 项目工程结构

email-forecast/
├── docker-compose.yml          # web + worker + mysql
├── Dockerfile                  # 同一镜像,不同 CMD
├── .env.example
├── package.json                # pnpm workspaces 可不用,单 package
├── prisma/
│   ├── schema.prisma
│   └── seed.ts                 # 仅 AppUser(admin/ops)
├── data/                       # volume 挂载(gitignore)
├── docx/                       # 需求/设计(已有,不进镜像必填)
├── src/
│   ├── app/
│   │   ├── (auth)/login/page.tsx
│   │   ├── (ops)/
│   │   │   ├── layout.tsx
│   │   │   ├── mails/page.tsx              # 列表
│   │   │   ├── mails/[id]/page.tsx         # 详情
│   │   │   ├── mails/[id]/import/page.tsx  # 确认导入
│   │   │   └── imports/page.tsx            # 日志/补偿
│   │   ├── api/...                         # Route Handlers
│   │   └── layout.tsx
│   ├── components/
│   │   ├── MailTable.tsx
│   │   ├── ShipmentVirtualTable.tsx        # antd + tanstack virtual
│   │   └── StatusTag.tsx
│   ├── services/
│   │   ├── db.ts                           # PrismaClient 单例
│   │   ├── state-machine.ts
│   │   ├── imap/
│   │   │   ├── client.ts
│   │   │   └── poller.ts
│   │   ├── parse/
│   │   │   ├── pipeline.ts
│   │   │   ├── classify.ts
│   │   │   ├── packing-list.ts
│   │   │   └── channel-map.ts
│   │   ├── cc/
│   │   │   ├── http.ts
│   │   │   ├── auth.ts                     # token DB+memory
│   │   │   ├── save-container.ts
│   │   │   └── conflict.ts
│   │   ├── import/
│   │   │   ├── confirm.ts
│   │   │   └── compensation.ts
│   │   └── audit.ts
│   ├── types/
│   │   ├── mail.ts                         # MailStatus, MailType
│   │   ├── parse.ts
│   │   └── cc.ts
│   ├── utils/
│   │   ├── hash.ts
│   │   ├── iso6346.ts
│   │   └── dates.ts                        # Asia/Shanghai
│   └── worker/
│       └── index.ts                        # setInterval 入口
├── tests/
│   ├── unit/
│   └── e2e/
└── README.md

docker-compose 对应

服务 镜像/构建 命令 职责
mysql mysql:8.0 官方入口 库 email_forecast
web 本 Dockerfile pnpm start(next start) UI + /api + 导入/CC
worker 同镜像 node dist/worker/index.js 或 tsx src/worker/index.ts 仅 IMAP+解析+PARSING 回收

共享:./data volume、同一 DATABASE_URL、同一代码树 src/services/*。

.env 键名严格对齐 PRD §9.1:CC_SAAS_HEADER、POLL_INTERVAL_MS、ENABLE_TYPE_OVERRIDE 等。
Worker 轮询基线优先读 imap_settings.poll_interval_ms(设置页,默认 30min);过滤规则见 imap_filter_rule;每封写 imap_pull_log(≤1000)。


6. MVP 实现路径(PRD §10)

里程碑 任务拆分(可直接建 Jira) DoD
M0 初始化 Next15+Prisma+Antd;docker-compose.yml;.env.example;/api/health;登录页壳 docker compose up 打开登录页;migrate 成功
M1 Prisma 全表;seed.ts 写入 M1–M4 元数据;列表+详情只读;状态/类型 Tag 列表 4 条;类型与金样例一致
M2 classify.ts 规则;packing-list.ts 吃邮件2 xlsx;详情展示 327 行虚拟表;种子挂附件路径 shipments 行数=327;核心列断言通过 vitest
M3 CcAdapter login/token;SaveContainer mock 单测;可选 TEST 烟雾脚本 scripts/cc-smoke.ts mock code=200;TEST 有账号则 1 柜成功
M4 确认导入页;CAS+幂等;冲突检测;补偿表+重试 API;Admin 改类型 勾选 2 行 → payload 2 货件;冲突阻断;双击不双写
M5 GetShippingLineList 下拉;/logs 导入+拉取;README;回收定时;无审计页 可演示全流程

Cursor 编码顺序: 严格 M0→M5;每里程碑只改该阶段文件。


7. 风险与工程约束

项 约束
附件 单文件 ≤20MB;zip 只解一层;路径禁止 ..;落盘 data/mails/{mailId}/
IMAP 锁 每轮 SELECT GET_LOCK('imap_poll',0);finally RELEASE_LOCK;拿不到锁直接 return
Token 读路径:DB expire_at 权威 → 内存缓存;写路径:GET_LOCK('cc_login',5) 后 login 写 DB
虚拟滚动 ShipmentVirtualTable 固定行高 48px;只渲染可视区±10 行;禁止 Antd Table 默认全量 DOM
CC 超时 禁止盲重放;TIMEOUT_UNKNOWN → 补偿 + GetContainerList 核对
状态机 仅 state-machine.ts 允许改 status;API 禁止通用 PATCH status
韧性条文 实现错误处理对照 docx/异常场景与系统韧性设计.md 全文

8. 核心模块 I/O 契约(Cursor 直接实现)

8.1 ParsePipeline.run(mailId: bigint)

  • 读 mail_message + attachments
  • 写:type_evidence, mail_type, parse_result, status, last_error
  • 副作用:无 CC 调用

8.2 ImapPoller.tick()

  • 锁 → fetch → upsert mail → seen → 对每个新 id 调 ParsePipeline
  • 另:回收 PARSING 超时、IMPORTING 超时扫描(只改状态/写补偿,不调 CC)

8.3 ConfirmImport.execute(input)

  • 校验 NEW + version + 勾选
  • CAS status
  • conflict check
  • 逐柜 SaveContainer
  • 更新 mail 终态 + audit

8.4 CcAuth.getToken()

  • 返回 {token, loginMark};410 路径由 CcHttpClient.request 触发 refresh+replay once

9. docker-compose 骨架(落地原文)

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: email_forecast
      MYSQL_USER: app
      MYSQL_PASSWORD: app
      MYSQL_ROOT_PASSWORD: root
    ports: ["7023:3306"]  # 本地避让宿主机 3306
    volumes: ["mysql_data:/var/lib/mysql"]
    command: ["--default-authentication-plugin=mysql_native_password","--character-set-server=utf8mb4"]
  web:
    build: .
    command: ["pnpm","start"]
    ports: ["3100:3100"]
    env_file: [.env]
    volumes: ["./data:/app/data"]
    depends_on: [mysql]
  worker:
    build: .
    command: ["node","dist/worker/index.js"]
    env_file: [.env]
    volumes: ["./data:/app/data"]
    depends_on: [mysql]
volumes:
  mysql_data:

10. 文档索引

文档 路径
PRD docx/需求规格-邮件自动预报-v0.2.md
韧性 docx/异常场景与系统韧性设计.md
本设计 docx/技术设计方案-邮件自动预报-v1.0.md
实施计划 .cursor/rules/implementation-plan.mdc

附录 A — CC Write API 矩阵(一期扩范围)

capability id 默认 mode 拟定 endpoint 触发 mail_type 完成标准
save_container live /Container/SaveContainer NEW_CONTAINER(人工确认) 已有
transfer mock /Container/TransferWarehouse TRANSFER live 真接通 + 烟雾 1 次
hold_split mock /Container/HoldSplitInstruction INSTRUCTION_HOLD_SPLIT 同上
label mock /Container/ApplyLabelInstruction INSTRUCTION_LABEL 同上

表 cc_write_capability 可改 mode:live|mock|off。CC_MOCK=true 时全局按 mock。mock ≠ TEST 验收。