初始化足球投注平台 MVP Monorepo

包含 NestJS 后端、三端前端、Prisma 数据模型、结算引擎测试与 PRD 文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 14:35:48 +08:00
commit 14e49374ac
118 changed files with 15944 additions and 0 deletions

View 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")
}