初始化足球投注平台 MVP Monorepo
包含 NestJS 后端、三端前端、Prisma 数据模型、结算引擎测试与 PRD 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
5
apps/api/prisma/migrations/README.md
Normal file
5
apps/api/prisma/migrations/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Initial migration for TheBet365 MVP
|
||||
-- Run: pnpm --filter @thebet365/api db:migrate
|
||||
|
||||
-- Generated from prisma/schema.prisma
|
||||
-- Use `pnpm db:migrate` when PostgreSQL is available
|
||||
585
apps/api/prisma/schema.prisma
Normal file
585
apps/api/prisma/schema.prisma
Normal file
@@ -0,0 +1,585 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// ============ Users & Auth ============
|
||||
|
||||
model User {
|
||||
id BigInt @id @default(autoincrement())
|
||||
username String @unique @db.VarChar(64)
|
||||
userType String @map("user_type") @db.VarChar(20)
|
||||
status String @default("ACTIVE") @db.VarChar(20)
|
||||
parentId BigInt? @map("parent_id")
|
||||
agentLevel Int? @map("agent_level")
|
||||
locale String @default("en-US") @db.VarChar(10)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
auth UserAuth?
|
||||
wallet Wallet?
|
||||
agentProfile AgentProfile?
|
||||
adminRole AdminUserRole?
|
||||
bets Bet[]
|
||||
preferences UserPreference?
|
||||
|
||||
parent User? @relation("UserHierarchy", fields: [parentId], references: [id])
|
||||
children User[] @relation("UserHierarchy")
|
||||
|
||||
@@index([userType])
|
||||
@@index([parentId])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model UserAuth {
|
||||
id BigInt @id @default(autoincrement())
|
||||
userId BigInt @unique @map("user_id")
|
||||
passwordHash String @map("password_hash") @db.VarChar(255)
|
||||
loginFailCount Int @default(0) @map("login_fail_count")
|
||||
lockedUntil DateTime? @map("locked_until")
|
||||
lastLoginAt DateTime? @map("last_login_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("user_auth")
|
||||
}
|
||||
|
||||
model UserPreference {
|
||||
id BigInt @id @default(autoincrement())
|
||||
userId BigInt @unique @map("user_id")
|
||||
locale String @default("en-US") @db.VarChar(10)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("user_preferences")
|
||||
}
|
||||
|
||||
model Role {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(64)
|
||||
name String @db.VarChar(128)
|
||||
description String? @db.VarChar(255)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
permissions RolePermission[]
|
||||
adminUsers AdminUserRole[]
|
||||
|
||||
@@map("roles")
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(128)
|
||||
name String @db.VarChar(128)
|
||||
module String @db.VarChar(64)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
roles RolePermission[]
|
||||
|
||||
@@map("permissions")
|
||||
}
|
||||
|
||||
model RolePermission {
|
||||
roleId BigInt @map("role_id")
|
||||
permissionId BigInt @map("permission_id")
|
||||
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
permission Permission @relation(fields: [permissionId], references: [id])
|
||||
|
||||
@@id([roleId, permissionId])
|
||||
@@map("role_permissions")
|
||||
}
|
||||
|
||||
model AdminUserRole {
|
||||
userId BigInt @unique @map("user_id")
|
||||
roleId BigInt @map("role_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
|
||||
@@map("admin_user_roles")
|
||||
}
|
||||
|
||||
// ============ Agent ============
|
||||
|
||||
model AgentProfile {
|
||||
id BigInt @id @default(autoincrement())
|
||||
userId BigInt @unique @map("user_id")
|
||||
level Int
|
||||
parentAgentId BigInt? @map("parent_agent_id")
|
||||
creditLimit Decimal @default(0) @map("credit_limit") @db.Decimal(18, 4)
|
||||
usedCredit Decimal @default(0) @map("used_credit") @db.Decimal(18, 4)
|
||||
directPlayerLiability Decimal @default(0) @map("direct_player_liability") @db.Decimal(18, 4)
|
||||
childAgentExposure Decimal @default(0) @map("child_agent_exposure") @db.Decimal(18, 4)
|
||||
status String @default("ACTIVE") @db.VarChar(20)
|
||||
maxSingleDeposit Decimal? @map("max_single_deposit") @db.Decimal(18, 4)
|
||||
maxDailyDeposit Decimal? @map("max_daily_deposit") @db.Decimal(18, 4)
|
||||
cashbackRate Decimal @default(0) @map("cashback_rate") @db.Decimal(8, 4)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([parentAgentId])
|
||||
@@map("agent_profiles")
|
||||
}
|
||||
|
||||
model AgentClosure {
|
||||
ancestorId BigInt @map("ancestor_id")
|
||||
descendantId BigInt @map("descendant_id")
|
||||
depth Int
|
||||
|
||||
@@id([ancestorId, descendantId])
|
||||
@@index([descendantId])
|
||||
@@map("agent_closure")
|
||||
}
|
||||
|
||||
model AgentCreditTransaction {
|
||||
id BigInt @id @default(autoincrement())
|
||||
agentId BigInt @map("agent_id")
|
||||
transactionType String @map("transaction_type") @db.VarChar(32)
|
||||
amount Decimal @db.Decimal(18, 4)
|
||||
creditBefore Decimal @map("credit_before") @db.Decimal(18, 4)
|
||||
creditAfter Decimal @map("credit_after") @db.Decimal(18, 4)
|
||||
referenceType String? @map("reference_type") @db.VarChar(32)
|
||||
referenceId String? @map("reference_id") @db.VarChar(64)
|
||||
operatorId BigInt? @map("operator_id")
|
||||
requestId String? @map("request_id") @db.VarChar(128)
|
||||
remark String? @db.VarChar(500)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@unique([operatorId, requestId])
|
||||
@@index([agentId])
|
||||
@@map("agent_credit_transactions")
|
||||
}
|
||||
|
||||
// ============ Wallet ============
|
||||
|
||||
model Wallet {
|
||||
id BigInt @id @default(autoincrement())
|
||||
userId BigInt @unique @map("user_id")
|
||||
availableBalance Decimal @default(0) @map("available_balance") @db.Decimal(18, 4)
|
||||
frozenBalance Decimal @default(0) @map("frozen_balance") @db.Decimal(18, 4)
|
||||
currency String @default("USD") @db.VarChar(16)
|
||||
status String @default("ACTIVE") @db.VarChar(20)
|
||||
version Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
transactions WalletTransaction[]
|
||||
|
||||
@@map("wallets")
|
||||
}
|
||||
|
||||
model WalletTransaction {
|
||||
id BigInt @id @default(autoincrement())
|
||||
transactionId String @unique @map("transaction_id") @db.VarChar(64)
|
||||
userId BigInt @map("user_id")
|
||||
walletId BigInt @map("wallet_id")
|
||||
transactionType String @map("transaction_type") @db.VarChar(32)
|
||||
amount Decimal @db.Decimal(18, 4)
|
||||
balanceBefore Decimal @map("balance_before") @db.Decimal(18, 4)
|
||||
balanceAfter Decimal @map("balance_after") @db.Decimal(18, 4)
|
||||
frozenBefore Decimal @map("frozen_before") @db.Decimal(18, 4)
|
||||
frozenAfter Decimal @map("frozen_after") @db.Decimal(18, 4)
|
||||
referenceType String? @map("reference_type") @db.VarChar(32)
|
||||
referenceId String? @map("reference_id") @db.VarChar(64)
|
||||
operatorId BigInt? @map("operator_id")
|
||||
remark String? @db.VarChar(500)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
wallet Wallet @relation(fields: [walletId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
@@index([walletId])
|
||||
@@index([createdAt])
|
||||
@@map("wallet_transactions")
|
||||
}
|
||||
|
||||
// ============ Sports Data ============
|
||||
|
||||
model League {
|
||||
id BigInt @id @default(autoincrement())
|
||||
sportType String @default("FOOTBALL") @map("sport_type") @db.VarChar(20)
|
||||
code String @unique @db.VarChar(64)
|
||||
displayOrder Int @default(0) @map("display_order")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
matches Match[]
|
||||
|
||||
@@map("leagues")
|
||||
}
|
||||
|
||||
model Team {
|
||||
id BigInt @id @default(autoincrement())
|
||||
sportType String @default("FOOTBALL") @map("sport_type") @db.VarChar(20)
|
||||
code String @unique @db.VarChar(64)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
homeMatches Match[] @relation("HomeTeam")
|
||||
awayMatches Match[] @relation("AwayTeam")
|
||||
|
||||
@@map("teams")
|
||||
}
|
||||
|
||||
model EntityTranslation {
|
||||
id BigInt @id @default(autoincrement())
|
||||
entityType String @map("entity_type") @db.VarChar(32)
|
||||
entityId BigInt @map("entity_id")
|
||||
locale String @db.VarChar(10)
|
||||
fieldName String @map("field_name") @db.VarChar(32)
|
||||
value String @db.VarChar(500)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([entityType, entityId, locale, fieldName])
|
||||
@@index([entityType, entityId])
|
||||
@@map("entity_translations")
|
||||
}
|
||||
|
||||
model Match {
|
||||
id BigInt @id @default(autoincrement())
|
||||
sportType String @default("FOOTBALL") @map("sport_type") @db.VarChar(20)
|
||||
leagueId BigInt @map("league_id")
|
||||
homeTeamId BigInt @map("home_team_id")
|
||||
awayTeamId BigInt @map("away_team_id")
|
||||
startTime DateTime @map("start_time")
|
||||
status String @default("DRAFT") @db.VarChar(32)
|
||||
isHot Boolean @default(false) @map("is_hot")
|
||||
displayOrder Int @default(0) @map("display_order")
|
||||
publishTime DateTime? @map("publish_time")
|
||||
closeTime DateTime? @map("close_time")
|
||||
isOutright Boolean @default(false) @map("is_outright")
|
||||
createdBy BigInt? @map("created_by")
|
||||
updatedBy BigInt? @map("updated_by")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
league League @relation(fields: [leagueId], references: [id])
|
||||
homeTeam Team @relation("HomeTeam", fields: [homeTeamId], references: [id])
|
||||
awayTeam Team @relation("AwayTeam", fields: [awayTeamId], references: [id])
|
||||
score MatchScore?
|
||||
markets Market[]
|
||||
settlements SettlementBatch[]
|
||||
|
||||
@@index([status])
|
||||
@@index([startTime])
|
||||
@@index([leagueId])
|
||||
@@map("matches")
|
||||
}
|
||||
|
||||
model MatchScore {
|
||||
id BigInt @id @default(autoincrement())
|
||||
matchId BigInt @unique @map("match_id")
|
||||
htHomeScore Int? @map("ht_home_score")
|
||||
htAwayScore Int? @map("ht_away_score")
|
||||
ftHomeScore Int? @map("ft_home_score")
|
||||
ftAwayScore Int? @map("ft_away_score")
|
||||
winnerTeamId BigInt? @map("winner_team_id")
|
||||
recordedBy BigInt? @map("recorded_by")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
match Match @relation(fields: [matchId], references: [id])
|
||||
|
||||
@@map("match_scores")
|
||||
}
|
||||
|
||||
model Market {
|
||||
id BigInt @id @default(autoincrement())
|
||||
matchId BigInt @map("match_id")
|
||||
marketType String @map("market_type") @db.VarChar(64)
|
||||
period String @db.VarChar(16)
|
||||
lineValue Decimal? @map("line_value") @db.Decimal(8, 2)
|
||||
status String @default("OPEN") @db.VarChar(20)
|
||||
allowSingle Boolean @default(true) @map("allow_single")
|
||||
allowParlay Boolean @default(true) @map("allow_parlay")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
match Match @relation(fields: [matchId], references: [id])
|
||||
selections MarketSelection[]
|
||||
|
||||
@@index([matchId])
|
||||
@@index([marketType])
|
||||
@@map("markets")
|
||||
}
|
||||
|
||||
model MarketSelection {
|
||||
id BigInt @id @default(autoincrement())
|
||||
marketId BigInt @map("market_id")
|
||||
selectionCode String @map("selection_code") @db.VarChar(64)
|
||||
selectionName String @map("selection_name") @db.VarChar(255)
|
||||
odds Decimal @db.Decimal(18, 6)
|
||||
oddsVersion BigInt @default(1) @map("odds_version")
|
||||
status String @default("OPEN") @db.VarChar(20)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
market Market @relation(fields: [marketId], references: [id])
|
||||
oddsLogs OddsChangeLog[]
|
||||
|
||||
@@index([marketId])
|
||||
@@map("market_selections")
|
||||
}
|
||||
|
||||
model OddsChangeLog {
|
||||
id BigInt @id @default(autoincrement())
|
||||
selectionId BigInt @map("selection_id")
|
||||
oldOdds Decimal @map("old_odds") @db.Decimal(18, 6)
|
||||
newOdds Decimal @map("new_odds") @db.Decimal(18, 6)
|
||||
oddsVersion BigInt @map("odds_version")
|
||||
changedBy BigInt? @map("changed_by")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
selection MarketSelection @relation(fields: [selectionId], references: [id])
|
||||
|
||||
@@index([selectionId])
|
||||
@@map("odds_change_logs")
|
||||
}
|
||||
|
||||
// ============ Bets ============
|
||||
|
||||
model Bet {
|
||||
id BigInt @id @default(autoincrement())
|
||||
betNo String @unique @map("bet_no") @db.VarChar(64)
|
||||
userId BigInt @map("user_id")
|
||||
agentId BigInt? @map("agent_id")
|
||||
betType String @map("bet_type") @db.VarChar(20)
|
||||
stake Decimal @db.Decimal(18, 4)
|
||||
totalOdds Decimal? @map("total_odds") @db.Decimal(18, 6)
|
||||
potentialReturn Decimal? @map("potential_return") @db.Decimal(18, 4)
|
||||
actualReturn Decimal @default(0) @map("actual_return") @db.Decimal(18, 4)
|
||||
status String @default("PENDING") @db.VarChar(32)
|
||||
settlementStatus String? @map("settlement_status") @db.VarChar(32)
|
||||
currency String @default("USD") @db.VarChar(16)
|
||||
requestId String @map("request_id") @db.VarChar(128)
|
||||
placedAt DateTime @default(now()) @map("placed_at")
|
||||
settledAt DateTime? @map("settled_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
selections BetSelection[]
|
||||
|
||||
@@unique([userId, requestId])
|
||||
@@index([userId])
|
||||
@@index([agentId])
|
||||
@@index([status])
|
||||
@@index([placedAt])
|
||||
@@map("bets")
|
||||
}
|
||||
|
||||
model BetSelection {
|
||||
id BigInt @id @default(autoincrement())
|
||||
betId BigInt @map("bet_id")
|
||||
matchId BigInt? @map("match_id")
|
||||
marketId BigInt @map("market_id")
|
||||
selectionId BigInt @map("selection_id")
|
||||
marketType String @map("market_type") @db.VarChar(64)
|
||||
period String? @db.VarChar(16)
|
||||
selectionNameSnapshot String @map("selection_name_snapshot") @db.VarChar(255)
|
||||
handicapLine Decimal? @map("handicap_line") @db.Decimal(8, 2)
|
||||
totalLine Decimal? @map("total_line") @db.Decimal(8, 2)
|
||||
odds Decimal @db.Decimal(18, 6)
|
||||
oddsVersion BigInt @map("odds_version")
|
||||
resultStatus String? @map("result_status") @db.VarChar(32)
|
||||
effectiveOdds Decimal? @map("effective_odds") @db.Decimal(18, 6)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
bet Bet @relation(fields: [betId], references: [id])
|
||||
|
||||
@@index([betId])
|
||||
@@index([matchId])
|
||||
@@map("bet_selections")
|
||||
}
|
||||
|
||||
// ============ Settlement ============
|
||||
|
||||
model SettlementBatch {
|
||||
id BigInt @id @default(autoincrement())
|
||||
matchId BigInt @map("match_id")
|
||||
batchNo String @unique @map("batch_no") @db.VarChar(64)
|
||||
htHomeScore Int? @map("ht_home_score")
|
||||
htAwayScore Int? @map("ht_away_score")
|
||||
ftHomeScore Int? @map("ft_home_score")
|
||||
ftAwayScore Int? @map("ft_away_score")
|
||||
status String @default("PREVIEW") @db.VarChar(20)
|
||||
totalBets Int @default(0) @map("total_bets")
|
||||
totalPayout Decimal @default(0) @map("total_payout") @db.Decimal(18, 4)
|
||||
totalRefund Decimal @default(0) @map("total_refund") @db.Decimal(18, 4)
|
||||
operatorId BigInt? @map("operator_id")
|
||||
confirmedAt DateTime? @map("confirmed_at")
|
||||
isResettle Boolean @default(false) @map("is_resettle")
|
||||
reason String? @db.VarChar(500)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
match Match @relation(fields: [matchId], references: [id])
|
||||
items SettlementItem[]
|
||||
|
||||
@@index([matchId])
|
||||
@@map("settlement_batches")
|
||||
}
|
||||
|
||||
model SettlementItem {
|
||||
id BigInt @id @default(autoincrement())
|
||||
batchId BigInt @map("batch_id")
|
||||
betId BigInt @map("bet_id")
|
||||
userId BigInt @map("user_id")
|
||||
result String @db.VarChar(32)
|
||||
payout Decimal @db.Decimal(18, 4)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
batch SettlementBatch @relation(fields: [batchId], references: [id])
|
||||
|
||||
@@index([batchId])
|
||||
@@index([betId])
|
||||
@@map("settlement_items")
|
||||
}
|
||||
|
||||
// ============ Cashback ============
|
||||
|
||||
model CashbackRule {
|
||||
id BigInt @id @default(autoincrement())
|
||||
name String @db.VarChar(128)
|
||||
targetType String @map("target_type") @db.VarChar(32)
|
||||
targetId BigInt? @map("target_id")
|
||||
rate Decimal @db.Decimal(8, 4)
|
||||
marketType String? @map("market_type") @db.VarChar(64)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("cashback_rules")
|
||||
}
|
||||
|
||||
model CashbackBatch {
|
||||
id BigInt @id @default(autoincrement())
|
||||
batchNo String @unique @map("batch_no") @db.VarChar(64)
|
||||
periodStart DateTime @map("period_start")
|
||||
periodEnd DateTime @map("period_end")
|
||||
status String @default("PREVIEW") @db.VarChar(20)
|
||||
totalAmount Decimal @default(0) @map("total_amount") @db.Decimal(18, 4)
|
||||
playerCount Int @default(0) @map("player_count")
|
||||
operatorId BigInt? @map("operator_id")
|
||||
confirmedAt DateTime? @map("confirmed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
items CashbackItem[]
|
||||
|
||||
@@map("cashback_batches")
|
||||
}
|
||||
|
||||
model CashbackItem {
|
||||
id BigInt @id @default(autoincrement())
|
||||
batchId BigInt @map("batch_id")
|
||||
userId BigInt @map("user_id")
|
||||
effectiveStake Decimal @map("effective_stake") @db.Decimal(18, 4)
|
||||
rate Decimal @db.Decimal(8, 4)
|
||||
amount Decimal @db.Decimal(18, 4)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
batch CashbackBatch @relation(fields: [batchId], references: [id])
|
||||
|
||||
@@index([batchId])
|
||||
@@index([userId])
|
||||
@@map("cashback_items")
|
||||
}
|
||||
|
||||
// ============ Content & i18n ============
|
||||
|
||||
model Content {
|
||||
id BigInt @id @default(autoincrement())
|
||||
contentType String @map("content_type") @db.VarChar(32)
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
status String @default("DRAFT") @db.VarChar(20)
|
||||
linkType String? @map("link_type") @db.VarChar(32)
|
||||
linkTarget String? @map("link_target") @db.VarChar(500)
|
||||
startTime DateTime? @map("start_time")
|
||||
endTime DateTime? @map("end_time")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
translations ContentTranslation[]
|
||||
|
||||
@@index([contentType, status])
|
||||
@@map("contents")
|
||||
}
|
||||
|
||||
model ContentTranslation {
|
||||
id BigInt @id @default(autoincrement())
|
||||
contentId BigInt @map("content_id")
|
||||
locale String @db.VarChar(10)
|
||||
title String? @db.VarChar(255)
|
||||
body String? @db.Text
|
||||
imageUrl String? @map("image_url") @db.VarChar(500)
|
||||
|
||||
content Content @relation(fields: [contentId], references: [id])
|
||||
|
||||
@@unique([contentId, locale])
|
||||
@@map("content_translations")
|
||||
}
|
||||
|
||||
model I18nMessage {
|
||||
id BigInt @id @default(autoincrement())
|
||||
msgKey String @map("msg_key") @db.VarChar(128)
|
||||
locale String @db.VarChar(10)
|
||||
value String @db.Text
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([msgKey, locale])
|
||||
@@map("i18n_messages")
|
||||
}
|
||||
|
||||
// ============ System Config & Audit ============
|
||||
|
||||
model SystemConfig {
|
||||
id BigInt @id @default(autoincrement())
|
||||
configKey String @unique @map("config_key") @db.VarChar(128)
|
||||
configValue String @map("config_value") @db.Text
|
||||
description String? @db.VarChar(255)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("system_configs")
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id BigInt @id @default(autoincrement())
|
||||
operatorId BigInt? @map("operator_id")
|
||||
operatorType String @map("operator_type") @db.VarChar(20)
|
||||
action String @db.VarChar(128)
|
||||
module String @db.VarChar(64)
|
||||
targetType String? @map("target_type") @db.VarChar(32)
|
||||
targetId String? @map("target_id") @db.VarChar(64)
|
||||
beforeData String? @map("before_data") @db.Text
|
||||
afterData String? @map("after_data") @db.Text
|
||||
ipAddress String? @map("ip_address") @db.VarChar(45)
|
||||
userAgent String? @map("user_agent") @db.VarChar(500)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([operatorId])
|
||||
@@index([module])
|
||||
@@index([createdAt])
|
||||
@@map("audit_logs")
|
||||
}
|
||||
182
apps/api/prisma/seed.ts
Normal file
182
apps/api/prisma/seed.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding database...');
|
||||
|
||||
const superAdminRole = await prisma.role.upsert({
|
||||
where: { code: 'SUPER_ADMIN' },
|
||||
create: { code: 'SUPER_ADMIN', name: 'Super Admin', description: 'Full access' },
|
||||
update: {},
|
||||
});
|
||||
|
||||
const permCodes = [
|
||||
'users.create', 'users.view', 'agents.create', 'agents.view',
|
||||
'wallet.deposit', 'wallet.withdraw', 'matches.manage', 'settlement.confirm',
|
||||
'cashback.confirm', 'content.manage', 'reports.view',
|
||||
];
|
||||
|
||||
for (const code of permCodes) {
|
||||
const perm = await prisma.permission.upsert({
|
||||
where: { code },
|
||||
create: { code, name: code, module: code.split('.')[0] },
|
||||
update: {},
|
||||
});
|
||||
await prisma.rolePermission.upsert({
|
||||
where: { roleId_permissionId: { roleId: superAdminRole.id, permissionId: perm.id } },
|
||||
create: { roleId: superAdminRole.id, permissionId: perm.id },
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash('Admin@123', 10);
|
||||
const agentHash = await bcrypt.hash('Agent@123', 10);
|
||||
const playerHash = await bcrypt.hash('Player@123', 10);
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { username: 'admin' },
|
||||
create: {
|
||||
username: 'admin',
|
||||
userType: 'ADMIN',
|
||||
auth: { create: { passwordHash: hash } },
|
||||
adminRole: { create: { roleId: superAdminRole.id } },
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const agent1 = await prisma.user.upsert({
|
||||
where: { username: 'agent1' },
|
||||
create: {
|
||||
username: 'agent1',
|
||||
userType: 'AGENT',
|
||||
agentLevel: 1,
|
||||
auth: { create: { passwordHash: agentHash } },
|
||||
agentProfile: { create: { level: 1, creditLimit: 100000 } },
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
await prisma.agentClosure.upsert({
|
||||
where: { ancestorId_descendantId: { ancestorId: agent1.id, descendantId: agent1.id } },
|
||||
create: { ancestorId: agent1.id, descendantId: agent1.id, depth: 0 },
|
||||
update: {},
|
||||
});
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { username: 'agent2' },
|
||||
create: {
|
||||
username: 'agent2',
|
||||
userType: 'AGENT',
|
||||
agentLevel: 2,
|
||||
parentId: agent1.id,
|
||||
auth: { create: { passwordHash: agentHash } },
|
||||
agentProfile: { create: { level: 2, parentAgentId: agent1.id, creditLimit: 30000 } },
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { username: 'player1' },
|
||||
create: {
|
||||
username: 'player1',
|
||||
userType: 'PLAYER',
|
||||
parentId: agent1.id,
|
||||
auth: { create: { passwordHash: playerHash } },
|
||||
wallet: { create: { availableBalance: 1000 } },
|
||||
preferences: { create: { locale: 'zh-CN' } },
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{ key: 'nav.home', zh: '首页', ms: 'Laman Utama', en: 'Home' },
|
||||
{ key: 'nav.football', zh: '足球', ms: 'Bola Sepak', en: 'Football' },
|
||||
{ key: 'bet.place_bet', zh: '确认下注', ms: 'Letak Pertaruhan', en: 'Place Bet' },
|
||||
{ key: 'error.insufficient_balance', zh: '余额不足', ms: 'Baki tidak mencukupi', en: 'Insufficient balance' },
|
||||
];
|
||||
|
||||
for (const m of messages) {
|
||||
for (const [locale, value] of [['zh-CN', m.zh], ['ms-MY', m.ms], ['en-US', m.en]] as const) {
|
||||
await prisma.i18nMessage.upsert({
|
||||
where: { msgKey_locale: { msgKey: m.key, locale } },
|
||||
create: { msgKey: m.key, locale, value },
|
||||
update: { value },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const league = await prisma.league.upsert({
|
||||
where: { code: 'EPL' },
|
||||
create: { code: 'EPL' },
|
||||
update: {},
|
||||
});
|
||||
|
||||
await prisma.entityTranslation.upsert({
|
||||
where: { entityType_entityId_locale_fieldName: { entityType: 'LEAGUE', entityId: league.id, locale: 'zh-CN', fieldName: 'name' } },
|
||||
create: { entityType: 'LEAGUE', entityId: league.id, locale: 'zh-CN', fieldName: 'name', value: '英超' },
|
||||
update: {},
|
||||
});
|
||||
|
||||
for (const [code, name] of [['MUN', '曼联'], ['CHE', '切尔西']] as const) {
|
||||
const team = await prisma.team.upsert({ where: { code }, create: { code }, update: {} });
|
||||
await prisma.entityTranslation.upsert({
|
||||
where: { entityType_entityId_locale_fieldName: { entityType: 'TEAM', entityId: team.id, locale: 'zh-CN', fieldName: 'name' } },
|
||||
create: { entityType: 'TEAM', entityId: team.id, locale: 'zh-CN', fieldName: 'name', value: name },
|
||||
update: { value: name },
|
||||
});
|
||||
}
|
||||
|
||||
const mun = await prisma.team.findUnique({ where: { code: 'MUN' } });
|
||||
const che = await prisma.team.findUnique({ where: { code: 'CHE' } });
|
||||
|
||||
if (mun && che) {
|
||||
const existing = await prisma.match.findFirst({ where: { homeTeamId: mun.id, awayTeamId: che.id } });
|
||||
if (!existing) {
|
||||
const match = await prisma.match.create({
|
||||
data: {
|
||||
leagueId: league.id,
|
||||
homeTeamId: mun.id,
|
||||
awayTeamId: che.id,
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
status: 'PUBLISHED',
|
||||
isHot: true,
|
||||
publishTime: new Date(),
|
||||
},
|
||||
});
|
||||
await prisma.market.create({
|
||||
data: {
|
||||
matchId: match.id,
|
||||
marketType: 'FT_1X2',
|
||||
period: 'FT',
|
||||
selections: {
|
||||
create: [
|
||||
{ selectionCode: 'HOME', selectionName: 'Home', odds: 2.5 },
|
||||
{ selectionCode: 'DRAW', selectionName: 'Draw', odds: 3.2 },
|
||||
{ selectionCode: 'AWAY', selectionName: 'Away', odds: 2.8 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.content.create({
|
||||
data: {
|
||||
contentType: 'BANNER',
|
||||
status: 'ACTIVE',
|
||||
sortOrder: 1,
|
||||
translations: {
|
||||
create: [
|
||||
{ locale: 'zh-CN', title: '欢迎投注', body: '足球赛事火热进行中' },
|
||||
{ locale: 'en-US', title: 'Welcome', body: 'Football matches available' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}).catch(() => {});
|
||||
|
||||
console.log('Seed completed! admin/Admin@123 agent1/Agent@123 player1/Player@123');
|
||||
}
|
||||
|
||||
main().catch(console.error).finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user