generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model MailboxAccount { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt /// 归属租户(AppUser.role=customer);空=管理端公共邮箱 ownerUserId BigInt? @map("owner_user_id") @db.UnsignedBigInt name String @db.VarChar(128) host String @default("imap.qq.com") @db.VarChar(255) port Int @default(993) @db.UnsignedInt username String @db.VarChar(320) /// AES-GCM ciphertext (base64); empty when OAuth-only passwordEnc String @default("") @map("password_enc") @db.VarChar(1024) /// PASSWORD = 授权码;OAUTH = XOAUTH2 authType String @default("PASSWORD") @map("auth_type") @db.VarChar(16) oauthProvider String? @map("oauth_provider") @db.VarChar(32) oauthRefreshEnc String? @map("oauth_refresh_enc") @db.VarChar(2048) oauthAccessEnc String? @map("oauth_access_enc") @db.VarChar(2048) oauthExpiresAt DateTime? @map("oauth_expires_at") @db.DateTime(3) idleEnabled Boolean @default(true) @map("idle_enabled") folder String @default("INBOX") @db.VarChar(64) enabled Boolean @default(true) lastTestAt DateTime? @map("last_test_at") @db.DateTime(3) lastTestOk Boolean? @map("last_test_ok") lastTestError String? @map("last_test_error") @db.VarChar(500) createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) owner AppUser? @relation(fields: [ownerUserId], references: [id], onDelete: SetNull) mails MailMessage[] @@unique([username], map: "uk_mailbox_username") @@index([enabled], map: "idx_mailbox_enabled") @@index([ownerUserId], map: "idx_mailbox_owner") @@map("mailbox_account") } model MailMessage { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt // utf8mb4 unique key limit: keep message_id ≤ 191 chars for UNIQUE messageId String? @unique @map("message_id") @db.VarChar(191) mailboxAccountId BigInt? @map("mailbox_account_id") @db.UnsignedBigInt imapUid BigInt? @map("imap_uid") @db.UnsignedBigInt folder String @default("INBOX") @db.VarChar(64) subject String @default("(无主题)") @db.VarChar(512) fromAddr String @default("") @map("from_addr") @db.VarChar(320) receivedAt DateTime? @map("received_at") @db.DateTime(3) bodyText String? @map("body_text") @db.MediumText /// OCR 抽取文本(权威解析辅助) ocrText String? @map("ocr_text") @db.MediumText /// 线程根 Message-ID(规范化) threadId String? @map("thread_id") @db.VarChar(191) inReplyTo String? @map("in_reply_to") @db.VarChar(191) referencesHeader String? @map("references_header") @db.Text isThreadRoot Boolean @default(true) @map("is_thread_root") mailType String @default("UNKNOWN") @map("mail_type") @db.VarChar(32) status String @default("FETCHED") @db.VarChar(32) typeEvidence Json? @map("type_evidence") snapshotPath String? @map("snapshot_path") @db.VarChar(512) rawHash String? @unique @map("raw_hash") @db.Char(64) lastError String? @map("last_error") @db.VarChar(1000) /// 贯穿拉取/解析/暂存/提交日志 traceId String? @map("trace_id") @db.VarChar(36) version Int @default(1) @db.UnsignedInt createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) mailboxAccount MailboxAccount? @relation(fields: [mailboxAccountId], references: [id], onDelete: SetNull) attachments MailAttachment[] parseResult ParseResult? imports ContainerImport[] @@unique([mailboxAccountId, folder, imapUid], map: "uk_mailbox_folder_uid") @@index([status, receivedAt], map: "idx_status_received") @@index([mailType], map: "idx_mail_type") @@index([mailboxAccountId], map: "idx_mailbox") @@index([threadId], map: "idx_thread") @@index([traceId], map: "idx_trace") @@map("mail_message") } model MailAttachment { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt mailId BigInt @map("mail_id") @db.UnsignedBigInt filename String @db.VarChar(512) contentType String? @map("content_type") @db.VarChar(128) sha256 String @db.Char(64) path String @db.VarChar(512) size Int @db.UnsignedInt rejected Boolean @default(false) templateId String? @map("template_id") @db.VarChar(64) createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) mail MailMessage @relation(fields: [mailId], references: [id]) @@index([mailId], map: "idx_mail") @@map("mail_attachment") } model ParseResult { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt mailId BigInt @unique @map("mail_id") @db.UnsignedBigInt containerHeader Json @map("container_header") shipments Json lineage Json? createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) mail MailMessage @relation(fields: [mailId], references: [id]) @@map("parse_result") } model ContainerImport { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt mailId BigInt @map("mail_id") @db.UnsignedBigInt containerNo String @map("container_no") @db.VarChar(50) externalId String? @map("external_id") @db.VarChar(64) requestBody String? @map("request_body") @db.MediumText responseBody String? @map("response_body") @db.MediumText status String @db.VarChar(32) lastError String? @map("last_error") @db.VarChar(1000) shipmentsHash String @map("shipments_hash") @db.Char(64) /// forecast | work_order | do_upload | transfer importKind String? @map("import_kind") @db.VarChar(32) /// 写入时选中的 CC 客户档案名称 ccCustomerName String? @map("cc_customer_name") @db.VarChar(128) ccProfileId BigInt? @map("cc_profile_id") @db.UnsignedBigInt createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) mail MailMessage @relation(fields: [mailId], references: [id]) compensations ImportCompensation[] @@unique([mailId, containerNo, shipmentsHash], map: "uk_mail_container_shiphash") @@index([mailId], map: "idx_mail") @@index([containerNo], map: "idx_container") @@index([importKind], map: "idx_import_kind") @@map("container_import") } model ContainerActiveLock { containerNo String @id @map("container_no") @db.VarChar(50) mailId BigInt @map("mail_id") @db.UnsignedBigInt importId BigInt? @map("import_id") @db.UnsignedBigInt lockedAt DateTime @map("locked_at") @db.DateTime(3) @@map("container_active_lock") } model ImportCompensation { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt importId BigInt @map("import_id") @db.UnsignedBigInt retryCount Int @default(0) @map("retry_count") @db.UnsignedInt maxRetry Int @default(3) @map("max_retry") @db.UnsignedInt nextRetryAt DateTime? @map("next_retry_at") @db.DateTime(3) reason String @db.VarChar(64) status String @default("OPEN") @db.VarChar(32) retryToken String @unique @map("retry_token") @db.Char(36) createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) import ContainerImport @relation(fields: [importId], references: [id]) @@index([status, nextRetryAt], map: "idx_open") @@map("import_compensation") } model CcTokenCache { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt loginMark String @unique @map("login_mark") @db.Char(36) token String @db.VarChar(128) expireAt DateTime @map("expire_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) @@map("cc_token_cache") } /// CC 连接配置:userId 为空为管理端默认(原 id=1);租户各有一行(遗留,优先用 CcCustomerProfile) model CcSettings { id Int @id @default(autoincrement()) userId BigInt? @unique @map("user_id") @db.UnsignedBigInt mock Boolean @default(false) apiBase String @map("api_base") @db.VarChar(512) saasHeader String @map("saas_header") @db.VarChar(64) loginMark String @map("login_mark") @db.Char(36) username String @default("") @db.VarChar(128) /// AES-GCM;空表示沿用 .env 密码或未配置 passwordEnc String? @map("password_enc") @db.VarChar(1024) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) user AppUser? @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("cc_settings") } /// 可切换的 CC 客户档案:ownerUserId 空=管理端共享;非空=归属某租户 model CcCustomerProfile { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt ownerUserId BigInt? @map("owner_user_id") @db.UnsignedBigInt name String @db.VarChar(128) mock Boolean @default(false) apiBase String @map("api_base") @db.VarChar(512) saasHeader String @map("saas_header") @db.VarChar(64) loginMark String @map("login_mark") @db.Char(36) username String @default("") @db.VarChar(128) passwordEnc String? @map("password_enc") @db.VarChar(1024) enabled Boolean @default(true) sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) owner AppUser? @relation(fields: [ownerUserId], references: [id], onDelete: Cascade) @@index([ownerUserId, enabled], map: "idx_cc_profile_owner") @@index([sortOrder], map: "idx_cc_profile_sort") @@map("cc_customer_profile") } /// CC 写能力门禁:live | mock | off model CcWriteCapability { id String @id @db.VarChar(32) mode String @default("mock") @db.VarChar(16) endpoint String? @db.VarChar(512) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) @@map("cc_write_capability") } /// Singleton OAuth 应用凭证(id=1);优先于 .env,secret AES 加密 model OauthSettings { id Int @id @default(1) publicBaseUrl String @default("http://localhost:3000") @map("public_base_url") @db.VarChar(512) googleClientId String @default("") @map("google_client_id") @db.VarChar(256) googleClientSecretEnc String? @map("google_client_secret_enc") @db.VarChar(1024) msClientId String @default("") @map("ms_client_id") @db.VarChar(256) msClientSecretEnc String? @map("ms_client_secret_enc") @db.VarChar(1024) msTenant String @default("common") @map("ms_tenant") @db.VarChar(64) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) @@map("oauth_settings") } model SenderCustomerMap { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt matchType String @map("match_type") @db.VarChar(32) matchValue String @map("match_value") @db.VarChar(320) customerCode String @map("customer_code") @db.VarChar(64) enabled Boolean @default(true) @@unique([matchType, matchValue], map: "uk_match") @@map("sender_customer_map") } /// 登录账号。role=admin|ops 为员工;role=customer 为租户登录账号(一租户一账号) model AppUser { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt username String @unique @db.VarChar(64) passwordHash String @map("password_hash") @db.VarChar(128) /// AES-GCM,管理端可回显明文密码 passwordEnc String @default("") @map("password_enc") @db.VarChar(1024) role String @default("ops") @db.VarChar(16) displayName String @default("") @map("display_name") @db.VarChar(128) enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) mailboxes MailboxAccount[] ccSettings CcSettings? ccCustomerProfiles CcCustomerProfile[] @@index([role], map: "idx_app_user_role") @@map("app_user") } /// Singleton IMAP 自动拉取配置(id=1);poll_interval_ms 优先于 .env POLL_INTERVAL_MS model ImapSettings { id Int @id @default(1) /// 默认 30min;合法范围 3min~7d pollIntervalMs Int @default(1800000) @map("poll_interval_ms") updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) @@map("imap_settings") } /// CC 客户留言类型字典(设置页维护;工单表单下拉来源) model CcMessageTypeDict { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt /// 写入 CC 的 F_MessageType 文本 name String @unique @db.VarChar(64) enabled Boolean @default(true) sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.DateTime(3) @@index([enabled, sortOrder], map: "idx_cc_msg_type_enabled_sort") @@map("cc_message_type_dict") } /// 拉取过滤规则:关键词 / 发件人白名单 / 发件人黑名单 / 关键词黑名单 model ImapFilterRule { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt kind String @db.VarChar(32) value String @db.VarChar(256) enabled Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") @db.DateTime(3) @@unique([kind, value], map: "uk_imap_filter_kind_value") @@index([kind, enabled], map: "idx_imap_filter_kind") @@map("imap_filter_rule") } /// 拉取记录(每封一条;最多保留 1000,超则删最旧) model ImapPullLog { id BigInt @id @default(autoincrement()) @db.UnsignedBigInt pulledAt DateTime @default(now()) @map("pulled_at") @db.DateTime(3) mailboxAccountId BigInt? @map("mailbox_account_id") @db.UnsignedBigInt mailboxName String @default("") @map("mailbox_name") @db.VarChar(128) mailboxUsername String @default("") @map("mailbox_username") @db.VarChar(320) fromAddr String @default("") @map("from_addr") @db.VarChar(320) subject String @default("") @db.VarChar(512) mailType String? @map("mail_type") @db.VarChar(32) mailId BigInt? @map("mail_id") @db.UnsignedBigInt result String @db.VarChar(16) reason String? @db.VarChar(64) @@index([pulledAt], map: "idx_imap_pull_at") @@map("imap_pull_log") }