generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================ // 认种订单表 (状态表) // ============================================ model PlantingOrder { id BigInt @id @default(autoincrement()) @map("order_id") orderNo String @unique @map("order_no") @db.VarChar(50) userId BigInt @map("user_id") // 认种信息 treeCount Int @map("tree_count") totalAmount Decimal @map("total_amount") @db.Decimal(20, 8) // 省市选择 (不可修改) selectedProvince String? @map("selected_province") @db.VarChar(10) selectedCity String? @map("selected_city") @db.VarChar(10) provinceCitySelectedAt DateTime? @map("province_city_selected_at") provinceCityConfirmedAt DateTime? @map("province_city_confirmed_at") // 订单状态 status String @default("CREATED") @map("status") @db.VarChar(30) // 底池信息 poolInjectionBatchId BigInt? @map("pool_injection_batch_id") poolInjectionScheduledTime DateTime? @map("pool_injection_scheduled_time") poolInjectionActualTime DateTime? @map("pool_injection_actual_time") poolInjectionTxHash String? @map("pool_injection_tx_hash") @db.VarChar(100) // 挖矿 miningEnabledAt DateTime? @map("mining_enabled_at") // 时间戳 createdAt DateTime @default(now()) @map("created_at") paidAt DateTime? @map("paid_at") fundAllocatedAt DateTime? @map("fund_allocated_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 fundAllocations FundAllocation[] batch PoolInjectionBatch? @relation(fields: [poolInjectionBatchId], references: [id]) @@index([userId]) @@index([orderNo]) @@index([status]) @@index([poolInjectionBatchId]) @@index([selectedProvince, selectedCity]) @@index([createdAt]) @@index([paidAt]) @@map("planting_orders") } // ============================================ // 资金分配明细表 (行为表, append-only) // ============================================ model FundAllocation { id BigInt @id @default(autoincrement()) @map("allocation_id") orderId BigInt @map("order_id") // 分配信息 targetType String @map("target_type") @db.VarChar(50) amount Decimal @map("amount") @db.Decimal(20, 8) targetAccountId String? @map("target_account_id") @db.VarChar(100) // 元数据 metadata Json? @map("metadata") createdAt DateTime @default(now()) @map("created_at") // 关联 order PlantingOrder @relation(fields: [orderId], references: [id]) @@index([orderId]) @@index([targetType, targetAccountId]) @@index([createdAt]) @@map("fund_allocations") } // ============================================ // 用户持仓表 (状态表) // ============================================ model PlantingPosition { id BigInt @id @default(autoincrement()) @map("position_id") userId BigInt @unique @map("user_id") // 持仓统计 totalTreeCount Int @default(0) @map("total_tree_count") effectiveTreeCount Int @default(0) @map("effective_tree_count") pendingTreeCount Int @default(0) @map("pending_tree_count") // 挖矿状态 firstMiningStartAt DateTime? @map("first_mining_start_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 distributions PositionDistribution[] @@index([userId]) @@index([totalTreeCount]) @@map("planting_positions") } // ============================================ // 持仓省市分布表 // ============================================ model PositionDistribution { id BigInt @id @default(autoincrement()) @map("distribution_id") userId BigInt @map("user_id") // 省市信息 provinceCode String? @map("province_code") @db.VarChar(10) cityCode String? @map("city_code") @db.VarChar(10) // 数量 treeCount Int @default(0) @map("tree_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 position PlantingPosition @relation(fields: [userId], references: [userId]) @@unique([userId, provinceCode, cityCode]) @@index([userId]) @@index([provinceCode]) @@index([cityCode]) @@map("position_province_city_distribution") } // ============================================ // 底池注入批次表 (状态表) // ============================================ model PoolInjectionBatch { id BigInt @id @default(autoincrement()) @map("batch_id") batchNo String @unique @map("batch_no") @db.VarChar(50) // 批次时间窗口 (5天) startDate DateTime @map("start_date") @db.Date endDate DateTime @map("end_date") @db.Date // 统计信息 orderCount Int @default(0) @map("order_count") totalAmount Decimal @default(0) @map("total_amount") @db.Decimal(20, 8) // 注入状态 status String @default("PENDING") @map("status") @db.VarChar(20) scheduledInjectionTime DateTime? @map("scheduled_injection_time") actualInjectionTime DateTime? @map("actual_injection_time") injectionTxHash String? @map("injection_tx_hash") @db.VarChar(100) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // 关联 orders PlantingOrder[] @@index([batchNo]) @@index([startDate, endDate]) @@index([status]) @@index([scheduledInjectionTime]) @@map("pool_injection_batches") } // ============================================ // 认种事件表 (行为表, append-only) // ============================================ model PlantingEvent { id BigInt @id @default(autoincrement()) @map("event_id") eventType String @map("event_type") @db.VarChar(50) // 聚合根信息 aggregateId String @map("aggregate_id") @db.VarChar(100) aggregateType String @map("aggregate_type") @db.VarChar(50) // 事件数据 eventData Json @map("event_data") // 元数据 userId BigInt? @map("user_id") occurredAt DateTime @default(now()) @map("occurred_at") version Int @default(1) @map("version") @@index([aggregateType, aggregateId]) @@index([eventType]) @@index([userId]) @@index([occurredAt]) @@map("planting_events") } // ============================================ // Outbox 事件发件箱表 (Outbox Pattern) // 保证事件发布的可靠性: // 1. 业务数据和 Outbox 记录在同一个事务中写入 // 2. 后台任务轮询 Outbox 表并发布到 Kafka // 3. 发布成功后标记为已处理 // ============================================ model OutboxEvent { id BigInt @id @default(autoincrement()) @map("outbox_id") // 事件信息 eventType String @map("event_type") @db.VarChar(100) topic String @map("topic") @db.VarChar(100) key String @map("key") @db.VarChar(200) payload Json @map("payload") // 聚合根信息 (用于幂等性检查) aggregateId String @map("aggregate_id") @db.VarChar(100) aggregateType String @map("aggregate_type") @db.VarChar(50) // 发布状态 status String @default("PENDING") @map("status") @db.VarChar(20) // PENDING, PUBLISHED, FAILED retryCount Int @default(0) @map("retry_count") maxRetries Int @default(5) @map("max_retries") lastError String? @map("last_error") @db.Text // 时间戳 createdAt DateTime @default(now()) @map("created_at") publishedAt DateTime? @map("published_at") nextRetryAt DateTime? @map("next_retry_at") @@index([status, createdAt]) @@index([status, nextRetryAt]) @@index([aggregateType, aggregateId]) @@index([topic]) @@map("outbox_events") } // ============================================ // 支付补偿表 (用于处理支付失败需要补偿的订单) // 当订单支付过程中发生以下情况时创建补偿记录: // 1. 资金已冻结/扣款但数据库事务失败 // 2. 数据库事务成功但确认扣款失败 // 3. 确认扣款成功但资金分配失败 // ============================================ model PaymentCompensation { id BigInt @id @default(autoincrement()) @map("compensation_id") orderNo String @map("order_no") @db.VarChar(50) userId BigInt @map("user_id") // 补偿类型 compensationType String @map("compensation_type") @db.VarChar(50) // UNFREEZE, REFUND, RETRY_CONFIRM, RETRY_ALLOCATE // 金额信息 amount Decimal @map("amount") @db.Decimal(20, 8) // 状态 status String @default("PENDING") @map("status") @db.VarChar(20) // PENDING, PROCESSING, COMPLETED, FAILED // 失败信息 failureReason String? @map("failure_reason") @db.Text failureStage String? @map("failure_stage") @db.VarChar(50) // FREEZE, DB_TRANSACTION, CONFIRM, ALLOCATE // 重试信息 retryCount Int @default(0) @map("retry_count") maxRetries Int @default(5) @map("max_retries") nextRetryAt DateTime? @map("next_retry_at") lastError String? @map("last_error") @db.Text // 时间戳 createdAt DateTime @default(now()) @map("created_at") processedAt DateTime? @map("processed_at") completedAt DateTime? @map("completed_at") @@unique([orderNo, compensationType]) @@index([status, nextRetryAt]) @@index([userId]) @@index([orderNo]) @@index([createdAt]) @@map("payment_compensations") }