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 验收。