feat(player): 完善 H5 投注端与 API 演示数据
- 球赛/串关/优胜冠军、赛事详情、历史投注与个人资料编辑 - 固定顶栏、公告与底栏,仅内容区滚动 - 底部导航与站点 favicon 使用 logo,登录页精简 - API 种子、冠军盘与历史注单增强 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
703
apps/api/prisma/migrations/20260602065556_init/migration.sql
Normal file
703
apps/api/prisma/migrations/20260602065556_init/migration.sql
Normal file
@@ -0,0 +1,703 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"username" VARCHAR(64) NOT NULL,
|
||||
"user_type" VARCHAR(20) NOT NULL,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
"parent_id" BIGINT,
|
||||
"agent_level" INTEGER,
|
||||
"locale" VARCHAR(10) NOT NULL DEFAULT 'en-US',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_auth" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"password_hash" VARCHAR(255) NOT NULL,
|
||||
"login_fail_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"locked_until" TIMESTAMP(3),
|
||||
"last_login_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "user_auth_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "user_preferences" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"locale" VARCHAR(10) NOT NULL DEFAULT 'en-US',
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "user_preferences_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "roles" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"code" VARCHAR(64) NOT NULL,
|
||||
"name" VARCHAR(128) NOT NULL,
|
||||
"description" VARCHAR(255),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "permissions" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"code" VARCHAR(128) NOT NULL,
|
||||
"name" VARCHAR(128) NOT NULL,
|
||||
"module" VARCHAR(64) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "role_permissions" (
|
||||
"role_id" BIGINT NOT NULL,
|
||||
"permission_id" BIGINT NOT NULL,
|
||||
|
||||
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("role_id","permission_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "admin_user_roles" (
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"role_id" BIGINT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_profiles" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"level" INTEGER NOT NULL,
|
||||
"parent_agent_id" BIGINT,
|
||||
"credit_limit" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"used_credit" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"direct_player_liability" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"child_agent_exposure" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
"max_single_deposit" DECIMAL(18,4),
|
||||
"max_daily_deposit" DECIMAL(18,4),
|
||||
"cashback_rate" DECIMAL(8,4) NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "agent_profiles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_closure" (
|
||||
"ancestor_id" BIGINT NOT NULL,
|
||||
"descendant_id" BIGINT NOT NULL,
|
||||
"depth" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "agent_closure_pkey" PRIMARY KEY ("ancestor_id","descendant_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_credit_transactions" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"agent_id" BIGINT NOT NULL,
|
||||
"transaction_type" VARCHAR(32) NOT NULL,
|
||||
"amount" DECIMAL(18,4) NOT NULL,
|
||||
"credit_before" DECIMAL(18,4) NOT NULL,
|
||||
"credit_after" DECIMAL(18,4) NOT NULL,
|
||||
"reference_type" VARCHAR(32),
|
||||
"reference_id" VARCHAR(64),
|
||||
"operator_id" BIGINT,
|
||||
"request_id" VARCHAR(128),
|
||||
"remark" VARCHAR(500),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "agent_credit_transactions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "wallets" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"available_balance" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"frozen_balance" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"currency" VARCHAR(16) NOT NULL DEFAULT 'USD',
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
"version" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "wallets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "wallet_transactions" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"transaction_id" VARCHAR(64) NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"wallet_id" BIGINT NOT NULL,
|
||||
"transaction_type" VARCHAR(32) NOT NULL,
|
||||
"amount" DECIMAL(18,4) NOT NULL,
|
||||
"balance_before" DECIMAL(18,4) NOT NULL,
|
||||
"balance_after" DECIMAL(18,4) NOT NULL,
|
||||
"frozen_before" DECIMAL(18,4) NOT NULL,
|
||||
"frozen_after" DECIMAL(18,4) NOT NULL,
|
||||
"reference_type" VARCHAR(32),
|
||||
"reference_id" VARCHAR(64),
|
||||
"operator_id" BIGINT,
|
||||
"remark" VARCHAR(500),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "wallet_transactions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "leagues" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"sport_type" VARCHAR(20) NOT NULL DEFAULT 'FOOTBALL',
|
||||
"code" VARCHAR(64) NOT NULL,
|
||||
"display_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "leagues_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "teams" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"sport_type" VARCHAR(20) NOT NULL DEFAULT 'FOOTBALL',
|
||||
"code" VARCHAR(64) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "teams_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "entity_translations" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"entity_type" VARCHAR(32) NOT NULL,
|
||||
"entity_id" BIGINT NOT NULL,
|
||||
"locale" VARCHAR(10) NOT NULL,
|
||||
"field_name" VARCHAR(32) NOT NULL,
|
||||
"value" VARCHAR(500) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "entity_translations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "matches" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"sport_type" VARCHAR(20) NOT NULL DEFAULT 'FOOTBALL',
|
||||
"league_id" BIGINT NOT NULL,
|
||||
"home_team_id" BIGINT NOT NULL,
|
||||
"away_team_id" BIGINT NOT NULL,
|
||||
"start_time" TIMESTAMP(3) NOT NULL,
|
||||
"status" VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
|
||||
"is_hot" BOOLEAN NOT NULL DEFAULT false,
|
||||
"display_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"publish_time" TIMESTAMP(3),
|
||||
"close_time" TIMESTAMP(3),
|
||||
"is_outright" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_by" BIGINT,
|
||||
"updated_by" BIGINT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"deleted_at" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "matches_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "match_scores" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"match_id" BIGINT NOT NULL,
|
||||
"ht_home_score" INTEGER,
|
||||
"ht_away_score" INTEGER,
|
||||
"ft_home_score" INTEGER,
|
||||
"ft_away_score" INTEGER,
|
||||
"winner_team_id" BIGINT,
|
||||
"recorded_by" BIGINT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "match_scores_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "markets" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"match_id" BIGINT NOT NULL,
|
||||
"market_type" VARCHAR(64) NOT NULL,
|
||||
"period" VARCHAR(16) NOT NULL,
|
||||
"line_value" DECIMAL(8,2),
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'OPEN',
|
||||
"allow_single" BOOLEAN NOT NULL DEFAULT true,
|
||||
"allow_parlay" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "markets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "market_selections" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"market_id" BIGINT NOT NULL,
|
||||
"selection_code" VARCHAR(64) NOT NULL,
|
||||
"selection_name" VARCHAR(255) NOT NULL,
|
||||
"odds" DECIMAL(18,6) NOT NULL,
|
||||
"odds_version" BIGINT NOT NULL DEFAULT 1,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'OPEN',
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "market_selections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "odds_change_logs" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"selection_id" BIGINT NOT NULL,
|
||||
"old_odds" DECIMAL(18,6) NOT NULL,
|
||||
"new_odds" DECIMAL(18,6) NOT NULL,
|
||||
"odds_version" BIGINT NOT NULL,
|
||||
"changed_by" BIGINT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "odds_change_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "bets" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"bet_no" VARCHAR(64) NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"agent_id" BIGINT,
|
||||
"bet_type" VARCHAR(20) NOT NULL,
|
||||
"stake" DECIMAL(18,4) NOT NULL,
|
||||
"total_odds" DECIMAL(18,6),
|
||||
"potential_return" DECIMAL(18,4),
|
||||
"actual_return" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"status" VARCHAR(32) NOT NULL DEFAULT 'PENDING',
|
||||
"settlement_status" VARCHAR(32),
|
||||
"currency" VARCHAR(16) NOT NULL DEFAULT 'USD',
|
||||
"request_id" VARCHAR(128) NOT NULL,
|
||||
"placed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"settled_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "bets_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "bet_selections" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"bet_id" BIGINT NOT NULL,
|
||||
"match_id" BIGINT,
|
||||
"market_id" BIGINT NOT NULL,
|
||||
"selection_id" BIGINT NOT NULL,
|
||||
"market_type" VARCHAR(64) NOT NULL,
|
||||
"period" VARCHAR(16),
|
||||
"selection_name_snapshot" VARCHAR(255) NOT NULL,
|
||||
"handicap_line" DECIMAL(8,2),
|
||||
"total_line" DECIMAL(8,2),
|
||||
"odds" DECIMAL(18,6) NOT NULL,
|
||||
"odds_version" BIGINT NOT NULL,
|
||||
"result_status" VARCHAR(32),
|
||||
"effective_odds" DECIMAL(18,6),
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "bet_selections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "settlement_batches" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"match_id" BIGINT NOT NULL,
|
||||
"batch_no" VARCHAR(64) NOT NULL,
|
||||
"ht_home_score" INTEGER,
|
||||
"ht_away_score" INTEGER,
|
||||
"ft_home_score" INTEGER,
|
||||
"ft_away_score" INTEGER,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'PREVIEW',
|
||||
"total_bets" INTEGER NOT NULL DEFAULT 0,
|
||||
"total_payout" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"total_refund" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"operator_id" BIGINT,
|
||||
"confirmed_at" TIMESTAMP(3),
|
||||
"is_resettle" BOOLEAN NOT NULL DEFAULT false,
|
||||
"reason" VARCHAR(500),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "settlement_batches_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "settlement_items" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"batch_id" BIGINT NOT NULL,
|
||||
"bet_id" BIGINT NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"result" VARCHAR(32) NOT NULL,
|
||||
"payout" DECIMAL(18,4) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "settlement_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "cashback_rules" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"name" VARCHAR(128) NOT NULL,
|
||||
"target_type" VARCHAR(32) NOT NULL,
|
||||
"target_id" BIGINT,
|
||||
"rate" DECIMAL(8,4) NOT NULL,
|
||||
"market_type" VARCHAR(64),
|
||||
"is_active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "cashback_rules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "cashback_batches" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"batch_no" VARCHAR(64) NOT NULL,
|
||||
"period_start" TIMESTAMP(3) NOT NULL,
|
||||
"period_end" TIMESTAMP(3) NOT NULL,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'PREVIEW',
|
||||
"total_amount" DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
"player_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"operator_id" BIGINT,
|
||||
"confirmed_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "cashback_batches_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "cashback_items" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"batch_id" BIGINT NOT NULL,
|
||||
"user_id" BIGINT NOT NULL,
|
||||
"effective_stake" DECIMAL(18,4) NOT NULL,
|
||||
"rate" DECIMAL(8,4) NOT NULL,
|
||||
"amount" DECIMAL(18,4) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "cashback_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "contents" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"content_type" VARCHAR(32) NOT NULL,
|
||||
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||
"status" VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
|
||||
"link_type" VARCHAR(32),
|
||||
"link_target" VARCHAR(500),
|
||||
"start_time" TIMESTAMP(3),
|
||||
"end_time" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "contents_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "content_translations" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"content_id" BIGINT NOT NULL,
|
||||
"locale" VARCHAR(10) NOT NULL,
|
||||
"title" VARCHAR(255),
|
||||
"body" TEXT,
|
||||
"image_url" VARCHAR(500),
|
||||
|
||||
CONSTRAINT "content_translations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "i18n_messages" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"msg_key" VARCHAR(128) NOT NULL,
|
||||
"locale" VARCHAR(10) NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "i18n_messages_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "system_configs" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"config_key" VARCHAR(128) NOT NULL,
|
||||
"config_value" TEXT NOT NULL,
|
||||
"description" VARCHAR(255),
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "system_configs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "audit_logs" (
|
||||
"id" BIGSERIAL NOT NULL,
|
||||
"operator_id" BIGINT,
|
||||
"operator_type" VARCHAR(20) NOT NULL,
|
||||
"action" VARCHAR(128) NOT NULL,
|
||||
"module" VARCHAR(64) NOT NULL,
|
||||
"target_type" VARCHAR(32),
|
||||
"target_id" VARCHAR(64),
|
||||
"before_data" TEXT,
|
||||
"after_data" TEXT,
|
||||
"ip_address" VARCHAR(45),
|
||||
"user_agent" VARCHAR(500),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_user_type_idx" ON "users"("user_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "users_parent_id_idx" ON "users"("parent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_auth_user_id_key" ON "user_auth"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_preferences_user_id_key" ON "user_preferences"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "roles_code_key" ON "roles"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "permissions_code_key" ON "permissions"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "admin_user_roles_user_id_key" ON "admin_user_roles"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_profiles_user_id_key" ON "agent_profiles"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_profiles_parent_agent_id_idx" ON "agent_profiles"("parent_agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_closure_descendant_id_idx" ON "agent_closure"("descendant_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_credit_transactions_agent_id_idx" ON "agent_credit_transactions"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_credit_transactions_operator_id_request_id_key" ON "agent_credit_transactions"("operator_id", "request_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "wallets_user_id_key" ON "wallets"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "wallet_transactions_transaction_id_key" ON "wallet_transactions"("transaction_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wallet_transactions_user_id_idx" ON "wallet_transactions"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wallet_transactions_wallet_id_idx" ON "wallet_transactions"("wallet_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "wallet_transactions_created_at_idx" ON "wallet_transactions"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "leagues_code_key" ON "leagues"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "teams_code_key" ON "teams"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "entity_translations_entity_type_entity_id_idx" ON "entity_translations"("entity_type", "entity_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "entity_translations_entity_type_entity_id_locale_field_name_key" ON "entity_translations"("entity_type", "entity_id", "locale", "field_name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "matches_status_idx" ON "matches"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "matches_start_time_idx" ON "matches"("start_time");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "matches_league_id_idx" ON "matches"("league_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "match_scores_match_id_key" ON "match_scores"("match_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "markets_match_id_idx" ON "markets"("match_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "markets_market_type_idx" ON "markets"("market_type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "market_selections_market_id_idx" ON "market_selections"("market_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "odds_change_logs_selection_id_idx" ON "odds_change_logs"("selection_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "bets_bet_no_key" ON "bets"("bet_no");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "bets_user_id_idx" ON "bets"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "bets_agent_id_idx" ON "bets"("agent_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "bets_status_idx" ON "bets"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "bets_placed_at_idx" ON "bets"("placed_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "bets_user_id_request_id_key" ON "bets"("user_id", "request_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "bet_selections_bet_id_idx" ON "bet_selections"("bet_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "bet_selections_match_id_idx" ON "bet_selections"("match_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "settlement_batches_batch_no_key" ON "settlement_batches"("batch_no");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "settlement_batches_match_id_idx" ON "settlement_batches"("match_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "settlement_items_batch_id_idx" ON "settlement_items"("batch_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "settlement_items_bet_id_idx" ON "settlement_items"("bet_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "cashback_batches_batch_no_key" ON "cashback_batches"("batch_no");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "cashback_items_batch_id_idx" ON "cashback_items"("batch_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "cashback_items_user_id_idx" ON "cashback_items"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "contents_content_type_status_idx" ON "contents"("content_type", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "content_translations_content_id_locale_key" ON "content_translations"("content_id", "locale");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "i18n_messages_msg_key_locale_key" ON "i18n_messages"("msg_key", "locale");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "system_configs_config_key_key" ON "system_configs"("config_key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "audit_logs_operator_id_idx" ON "audit_logs"("operator_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "audit_logs_module_idx" ON "audit_logs"("module");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs"("created_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "users" ADD CONSTRAINT "users_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_auth" ADD CONSTRAINT "user_auth_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permissions"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "admin_user_roles" ADD CONSTRAINT "admin_user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "admin_user_roles" ADD CONSTRAINT "admin_user_roles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_profiles" ADD CONSTRAINT "agent_profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wallets" ADD CONSTRAINT "wallets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "wallet_transactions" ADD CONSTRAINT "wallet_transactions_wallet_id_fkey" FOREIGN KEY ("wallet_id") REFERENCES "wallets"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "matches" ADD CONSTRAINT "matches_league_id_fkey" FOREIGN KEY ("league_id") REFERENCES "leagues"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "matches" ADD CONSTRAINT "matches_home_team_id_fkey" FOREIGN KEY ("home_team_id") REFERENCES "teams"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "matches" ADD CONSTRAINT "matches_away_team_id_fkey" FOREIGN KEY ("away_team_id") REFERENCES "teams"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "match_scores" ADD CONSTRAINT "match_scores_match_id_fkey" FOREIGN KEY ("match_id") REFERENCES "matches"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "markets" ADD CONSTRAINT "markets_match_id_fkey" FOREIGN KEY ("match_id") REFERENCES "matches"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "market_selections" ADD CONSTRAINT "market_selections_market_id_fkey" FOREIGN KEY ("market_id") REFERENCES "markets"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "odds_change_logs" ADD CONSTRAINT "odds_change_logs_selection_id_fkey" FOREIGN KEY ("selection_id") REFERENCES "market_selections"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "bets" ADD CONSTRAINT "bets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "bet_selections" ADD CONSTRAINT "bet_selections_bet_id_fkey" FOREIGN KEY ("bet_id") REFERENCES "bets"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "settlement_batches" ADD CONSTRAINT "settlement_batches_match_id_fkey" FOREIGN KEY ("match_id") REFERENCES "matches"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "settlement_items" ADD CONSTRAINT "settlement_items_batch_id_fkey" FOREIGN KEY ("batch_id") REFERENCES "settlement_batches"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "cashback_items" ADD CONSTRAINT "cashback_items_batch_id_fkey" FOREIGN KEY ("batch_id") REFERENCES "cashback_batches"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "content_translations" ADD CONSTRAINT "content_translations_content_id_fkey" FOREIGN KEY ("content_id") REFERENCES "contents"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -55,6 +55,8 @@ model UserPreference {
|
||||
id BigInt @id @default(autoincrement())
|
||||
userId BigInt @unique @map("user_id")
|
||||
locale String @default("en-US") @db.VarChar(10)
|
||||
phone String? @db.VarChar(32)
|
||||
email String? @db.VarChar(128)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
||||
@@ -3,6 +3,569 @@ import * as bcrypt from 'bcryptjs';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/** 为演示赛事补齐详情页玩法(与后台 markets 模板一致) */
|
||||
async function seedDemoMarkets(matchId: bigint) {
|
||||
const configs: Array<{
|
||||
marketType: string;
|
||||
period: string;
|
||||
lineValue?: number;
|
||||
sortOrder: number;
|
||||
selections: Array<{ code: string; name: string; odds: number }>;
|
||||
}> = [
|
||||
{
|
||||
marketType: 'FT_1X2',
|
||||
period: 'FT',
|
||||
sortOrder: 1,
|
||||
selections: [
|
||||
{ code: 'HOME', name: '主胜', odds: 2.5 },
|
||||
{ code: 'DRAW', name: '和', odds: 3.2 },
|
||||
{ code: 'AWAY', name: '客胜', odds: 2.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'FT_HANDICAP',
|
||||
period: 'FT',
|
||||
lineValue: -0.5,
|
||||
sortOrder: 2,
|
||||
selections: [
|
||||
{ code: 'HOME', name: '主 -0.5', odds: 1.9 },
|
||||
{ code: 'AWAY', name: '客 +0.5', odds: 1.9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'FT_OVER_UNDER',
|
||||
period: 'FT',
|
||||
lineValue: 2.5,
|
||||
sortOrder: 3,
|
||||
selections: [
|
||||
{ code: 'OVER', name: '大 2.5', odds: 1.85 },
|
||||
{ code: 'UNDER', name: '小 2.5', odds: 1.95 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'FT_ODD_EVEN',
|
||||
period: 'FT',
|
||||
sortOrder: 4,
|
||||
selections: [
|
||||
{ code: 'ODD', name: '单', odds: 1.9 },
|
||||
{ code: 'EVEN', name: '双', odds: 1.9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'HT_1X2',
|
||||
period: 'HT',
|
||||
sortOrder: 5,
|
||||
selections: [
|
||||
{ code: 'HOME', name: '半场主', odds: 3.0 },
|
||||
{ code: 'DRAW', name: '半场和', odds: 2.0 },
|
||||
{ code: 'AWAY', name: '半场客', odds: 3.5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'HT_HANDICAP',
|
||||
period: 'HT',
|
||||
lineValue: -0.5,
|
||||
sortOrder: 6,
|
||||
selections: [
|
||||
{ code: 'HOME', name: '半场主 -0.5', odds: 1.9 },
|
||||
{ code: 'AWAY', name: '半场客 +0.5', odds: 1.9 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'HT_OVER_UNDER',
|
||||
period: 'HT',
|
||||
lineValue: 1.5,
|
||||
sortOrder: 7,
|
||||
selections: [
|
||||
{ code: 'OVER', name: '半场大 1.5', odds: 2.0 },
|
||||
{ code: 'UNDER', name: '半场小 1.5', odds: 1.75 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'FT_CORRECT_SCORE',
|
||||
period: 'FT',
|
||||
sortOrder: 8,
|
||||
selections: [
|
||||
{ code: 'SCORE_1_0', name: '1-0', odds: 4.86 },
|
||||
{ code: 'SCORE_2_0', name: '2-0', odds: 5.22 },
|
||||
{ code: 'SCORE_2_1', name: '2-1', odds: 7.92 },
|
||||
{ code: 'SCORE_3_0', name: '3-0', odds: 8.28 },
|
||||
{ code: 'SCORE_0_0', name: '0-0', odds: 8.64 },
|
||||
{ code: 'SCORE_1_1', name: '1-1', odds: 7.47 },
|
||||
{ code: 'SCORE_2_2', name: '2-2', odds: 24.3 },
|
||||
{ code: 'SCORE_3_3', name: '3-3', odds: 175.5 },
|
||||
{ code: 'SCORE_0_1', name: '0-1', odds: 14.4 },
|
||||
{ code: 'SCORE_0_2', name: '0-2', odds: 45.9 },
|
||||
{ code: 'SCORE_1_2', name: '1-2', odds: 23.4 },
|
||||
{ code: 'SCORE_0_3', name: '0-3', odds: 207 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'HT_CORRECT_SCORE',
|
||||
period: 'HT',
|
||||
sortOrder: 9,
|
||||
selections: [
|
||||
{ code: 'SCORE_1_0', name: '1-0', odds: 4.5 },
|
||||
{ code: 'SCORE_2_0', name: '2-0', odds: 8.0 },
|
||||
{ code: 'SCORE_0_0', name: '0-0', odds: 5.5 },
|
||||
{ code: 'SCORE_1_1', name: '1-1', odds: 7.0 },
|
||||
{ code: 'SCORE_0_1', name: '0-1', odds: 6.5 },
|
||||
{ code: 'SCORE_0_2', name: '0-2', odds: 18.0 },
|
||||
],
|
||||
},
|
||||
{
|
||||
marketType: 'SH_CORRECT_SCORE',
|
||||
period: 'SH',
|
||||
sortOrder: 10,
|
||||
selections: [
|
||||
{ code: 'SCORE_1_0', name: '1-0', odds: 4.5 },
|
||||
{ code: 'SCORE_2_0', name: '2-0', odds: 8.0 },
|
||||
{ code: 'SCORE_0_0', name: '0-0', odds: 5.5 },
|
||||
{ code: 'SCORE_1_1', name: '1-1', odds: 7.0 },
|
||||
{ code: 'SCORE_0_1', name: '0-1', odds: 6.5 },
|
||||
{ code: 'SCORE_0_2', name: '0-2', odds: 18.0 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
for (const cfg of configs) {
|
||||
const exists = await prisma.market.findFirst({
|
||||
where: { matchId, marketType: cfg.marketType },
|
||||
include: { _count: { select: { selections: true } } },
|
||||
});
|
||||
if (exists) {
|
||||
const needRefresh =
|
||||
cfg.marketType.includes('CORRECT_SCORE') &&
|
||||
exists._count.selections < cfg.selections.length;
|
||||
if (needRefresh) {
|
||||
await prisma.market.delete({ where: { id: exists.id } });
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await prisma.market.create({
|
||||
data: {
|
||||
matchId,
|
||||
marketType: cfg.marketType,
|
||||
period: cfg.period,
|
||||
lineValue: cfg.lineValue,
|
||||
allowSingle: true,
|
||||
allowParlay: true,
|
||||
sortOrder: cfg.sortOrder,
|
||||
status: 'OPEN',
|
||||
selections: {
|
||||
create: cfg.selections.map((s, i) => ({
|
||||
selectionCode: s.code,
|
||||
selectionName: s.name,
|
||||
odds: s.odds,
|
||||
sortOrder: i,
|
||||
status: 'OPEN',
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertLeagueName(leagueId: bigint, names: Record<string, string>) {
|
||||
for (const [locale, value] of Object.entries(names)) {
|
||||
await prisma.entityTranslation.upsert({
|
||||
where: {
|
||||
entityType_entityId_locale_fieldName: {
|
||||
entityType: 'LEAGUE',
|
||||
entityId: leagueId,
|
||||
locale,
|
||||
fieldName: 'name',
|
||||
},
|
||||
},
|
||||
create: { entityType: 'LEAGUE', entityId: leagueId, locale, fieldName: 'name', value },
|
||||
update: { value },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertTeam(
|
||||
code: string,
|
||||
names: Record<string, string>,
|
||||
) {
|
||||
const team = await prisma.team.upsert({
|
||||
where: { code },
|
||||
create: { code },
|
||||
update: {},
|
||||
});
|
||||
for (const [locale, value] of Object.entries(names)) {
|
||||
await prisma.entityTranslation.upsert({
|
||||
where: {
|
||||
entityType_entityId_locale_fieldName: {
|
||||
entityType: 'TEAM',
|
||||
entityId: team.id,
|
||||
locale,
|
||||
fieldName: 'name',
|
||||
},
|
||||
},
|
||||
create: { entityType: 'TEAM', entityId: team.id, locale, fieldName: 'name', value },
|
||||
update: { value },
|
||||
});
|
||||
}
|
||||
return team;
|
||||
}
|
||||
|
||||
async function ensurePublishedMatch(opts: {
|
||||
leagueId: bigint;
|
||||
homeTeamId: bigint;
|
||||
awayTeamId: bigint;
|
||||
startTime: Date;
|
||||
isHot?: boolean;
|
||||
displayOrder?: number;
|
||||
}) {
|
||||
let match = await prisma.match.findFirst({
|
||||
where: {
|
||||
leagueId: opts.leagueId,
|
||||
homeTeamId: opts.homeTeamId,
|
||||
awayTeamId: opts.awayTeamId,
|
||||
status: 'PUBLISHED',
|
||||
},
|
||||
});
|
||||
if (!match) {
|
||||
match = await prisma.match.create({
|
||||
data: {
|
||||
leagueId: opts.leagueId,
|
||||
homeTeamId: opts.homeTeamId,
|
||||
awayTeamId: opts.awayTeamId,
|
||||
startTime: opts.startTime,
|
||||
status: 'PUBLISHED',
|
||||
isHot: opts.isHot ?? false,
|
||||
displayOrder: opts.displayOrder ?? 0,
|
||||
publishTime: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
match = await prisma.match.update({
|
||||
where: { id: match.id },
|
||||
data: {
|
||||
startTime: opts.startTime,
|
||||
isHot: opts.isHot ?? match.isHot,
|
||||
displayOrder: opts.displayOrder ?? match.displayOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
await seedDemoMarkets(match.id);
|
||||
return match;
|
||||
}
|
||||
|
||||
function hoursFromNow(hours: number) {
|
||||
return new Date(Date.now() + hours * 3600 * 1000);
|
||||
}
|
||||
|
||||
async function seedSportsDemo() {
|
||||
const epl = await prisma.league.upsert({
|
||||
where: { code: 'EPL' },
|
||||
create: { code: 'EPL' },
|
||||
update: {},
|
||||
});
|
||||
await upsertLeagueName(epl.id, { 'zh-CN': '英超', 'en-US': 'Premier League' });
|
||||
|
||||
const wc = await prisma.league.upsert({
|
||||
where: { code: 'WC2026' },
|
||||
create: { code: 'WC2026' },
|
||||
update: {},
|
||||
});
|
||||
await upsertLeagueName(wc.id, {
|
||||
'zh-CN': '2026世界杯(在加拿大,墨西哥和美国)',
|
||||
'en-US': '2026 FIFA World Cup',
|
||||
});
|
||||
|
||||
const teams: Array<[string, Record<string, string>]> = [
|
||||
['MUN', { 'zh-CN': '曼联', 'en-US': 'Man United' }],
|
||||
['CHE', { 'zh-CN': '切尔西', 'en-US': 'Chelsea' }],
|
||||
['MEX', { 'zh-CN': '墨西哥', 'en-US': 'Mexico' }],
|
||||
['RSA', { 'zh-CN': '南非', 'en-US': 'South Africa' }],
|
||||
['CZE', { 'zh-CN': '捷克', 'en-US': 'Czech Republic' }],
|
||||
['KOR', { 'zh-CN': '韩国', 'en-US': 'South Korea' }],
|
||||
['CAN', { 'zh-CN': '加拿大', 'en-US': 'Canada' }],
|
||||
['BIH', { 'zh-CN': '波黑', 'en-US': 'Bosnia' }],
|
||||
['USA', { 'zh-CN': '美国', 'en-US': 'USA' }],
|
||||
['PAR', { 'zh-CN': '巴拉圭', 'en-US': 'Paraguay' }],
|
||||
['SUI', { 'zh-CN': '瑞士', 'en-US': 'Switzerland' }],
|
||||
['BRA', { 'zh-CN': '巴西', 'en-US': 'Brazil' }],
|
||||
['SCO', { 'zh-CN': '苏格兰', 'en-US': 'Scotland' }],
|
||||
['TUR', { 'zh-CN': '土耳其', 'en-US': 'Turkey' }],
|
||||
['ARG', { 'zh-CN': '阿根廷', 'en-US': 'Argentina' }],
|
||||
['FRA', { 'zh-CN': '法国', 'en-US': 'France' }],
|
||||
];
|
||||
|
||||
const teamMap = new Map<string, { id: bigint }>();
|
||||
for (const [code, names] of teams) {
|
||||
teamMap.set(code, await upsertTeam(code, names));
|
||||
}
|
||||
|
||||
const get = (code: string) => {
|
||||
const t = teamMap.get(code);
|
||||
if (!t) throw new Error(`Team ${code} missing`);
|
||||
return t;
|
||||
};
|
||||
|
||||
// 英超:明日开赛 → 早盘
|
||||
await ensurePublishedMatch({
|
||||
leagueId: epl.id,
|
||||
homeTeamId: get('MUN').id,
|
||||
awayTeamId: get('CHE').id,
|
||||
startTime: hoursFromNow(26),
|
||||
isHot: true,
|
||||
displayOrder: 1,
|
||||
});
|
||||
|
||||
// 英超:今晚开赛 → 今日
|
||||
await ensurePublishedMatch({
|
||||
leagueId: epl.id,
|
||||
homeTeamId: get('CHE').id,
|
||||
awayTeamId: get('MUN').id,
|
||||
startTime: hoursFromNow(8),
|
||||
isHot: false,
|
||||
displayOrder: 2,
|
||||
});
|
||||
|
||||
const wcFixtures: Array<{
|
||||
home: string;
|
||||
away: string;
|
||||
start: Date;
|
||||
hot?: boolean;
|
||||
order: number;
|
||||
}> = [
|
||||
{ home: 'MEX', away: 'RSA', start: new Date('2026-06-12T03:00:00Z'), hot: true, order: 1 },
|
||||
{ home: 'CZE', away: 'KOR', start: new Date('2026-06-12T07:00:00Z'), order: 2 },
|
||||
{ home: 'CAN', away: 'BIH', start: new Date('2026-06-13T00:00:00Z'), order: 3 },
|
||||
{ home: 'USA', away: 'PAR', start: new Date('2026-06-13T03:00:00Z'), hot: true, order: 4 },
|
||||
{ home: 'SUI', away: 'BRA', start: new Date('2026-06-14T16:00:00Z'), order: 5 },
|
||||
{ home: 'SCO', away: 'TUR', start: new Date('2026-06-14T19:00:00Z'), order: 6 },
|
||||
{ home: 'FRA', away: 'ARG', start: new Date('2026-06-15T20:00:00Z'), hot: true, order: 7 },
|
||||
];
|
||||
|
||||
for (const f of wcFixtures) {
|
||||
await ensurePublishedMatch({
|
||||
leagueId: wc.id,
|
||||
homeTeamId: get(f.home).id,
|
||||
awayTeamId: get(f.away).id,
|
||||
startTime: f.start,
|
||||
isHot: f.hot,
|
||||
displayOrder: f.order,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(` Sports demo: ${wcFixtures.length + 2} published matches`);
|
||||
}
|
||||
|
||||
async function seedOutrightDemo() {
|
||||
const wc = await prisma.league.findUnique({ where: { code: 'WC2026' } });
|
||||
if (!wc) return;
|
||||
|
||||
const placeholder = await upsertTeam('OUT', { 'zh-CN': '冠军盘', 'en-US': 'Outright' });
|
||||
|
||||
const outrightOdds: Array<[string, Record<string, string>, number]> = [
|
||||
['FRA', { 'zh-CN': '法国', 'en-US': 'France' }, 4.95],
|
||||
['ESP', { 'zh-CN': '西班牙', 'en-US': 'Spain' }, 4.95],
|
||||
['ENG', { 'zh-CN': '英格兰', 'en-US': 'England' }, 6.3],
|
||||
['BRA', { 'zh-CN': '巴西', 'en-US': 'Brazil' }, 8.55],
|
||||
['ARG', { 'zh-CN': '阿根廷', 'en-US': 'Argentina' }, 8.55],
|
||||
['POR', { 'zh-CN': '葡萄牙', 'en-US': 'Portugal' }, 9.0],
|
||||
['GER', { 'zh-CN': '德国', 'en-US': 'Germany' }, 15.3],
|
||||
['NED', { 'zh-CN': '荷兰', 'en-US': 'Netherlands' }, 18.9],
|
||||
['NOR', { 'zh-CN': '挪威', 'en-US': 'Norway' }, 32.4],
|
||||
['BEL', { 'zh-CN': '比利时', 'en-US': 'Belgium' }, 35.1],
|
||||
['COL', { 'zh-CN': '哥伦比亚', 'en-US': 'Colombia' }, 45.9],
|
||||
['JPN', { 'zh-CN': '日本', 'en-US': 'Japan' }, 45.9],
|
||||
['URU', { 'zh-CN': '乌拉圭', 'en-US': 'Uruguay' }, 63.9],
|
||||
['USA', { 'zh-CN': '美国', 'en-US': 'USA' }, 63.9],
|
||||
['MAR', { 'zh-CN': '摩洛哥', 'en-US': 'Morocco' }, 63.9],
|
||||
['CRO', { 'zh-CN': '克罗地亚', 'en-US': 'Croatia' }, 81.0],
|
||||
['MEX', { 'zh-CN': '墨西哥', 'en-US': 'Mexico' }, 85.0],
|
||||
['SUI', { 'zh-CN': '瑞士', 'en-US': 'Switzerland' }, 90.0],
|
||||
['TUR', { 'zh-CN': '土耳其', 'en-US': 'Turkey' }, 95.0],
|
||||
['SEN', { 'zh-CN': '塞内加尔', 'en-US': 'Senegal' }, 100.0],
|
||||
];
|
||||
|
||||
for (const [code, names] of outrightOdds) {
|
||||
await upsertTeam(code, names);
|
||||
}
|
||||
|
||||
let match = await prisma.match.findFirst({
|
||||
where: { leagueId: wc.id, isOutright: true },
|
||||
});
|
||||
if (!match) {
|
||||
match = await prisma.match.create({
|
||||
data: {
|
||||
leagueId: wc.id,
|
||||
homeTeamId: placeholder.id,
|
||||
awayTeamId: placeholder.id,
|
||||
isOutright: true,
|
||||
startTime: new Date('2027-07-01T00:00:00Z'),
|
||||
status: 'PUBLISHED',
|
||||
publishTime: new Date(),
|
||||
isHot: true,
|
||||
displayOrder: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const marketExists = await prisma.market.findFirst({
|
||||
where: { matchId: match.id, marketType: 'OUTRIGHT_WINNER' },
|
||||
});
|
||||
if (!marketExists) {
|
||||
await prisma.market.create({
|
||||
data: {
|
||||
matchId: match.id,
|
||||
marketType: 'OUTRIGHT_WINNER',
|
||||
period: 'OUTRIGHT',
|
||||
allowSingle: true,
|
||||
allowParlay: false,
|
||||
sortOrder: 1,
|
||||
status: 'OPEN',
|
||||
selections: {
|
||||
create: outrightOdds.map(([code, names, odds], i) => ({
|
||||
selectionCode: code,
|
||||
selectionName: names['zh-CN'],
|
||||
odds,
|
||||
sortOrder: i,
|
||||
status: 'OPEN',
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(' Outright demo: World Cup winner market');
|
||||
}
|
||||
|
||||
async function seedPlayerDemo() {
|
||||
const player = await prisma.user.findUnique({
|
||||
where: { username: 'player1' },
|
||||
include: { wallet: true },
|
||||
});
|
||||
if (!player?.wallet) return;
|
||||
|
||||
await prisma.wallet.update({
|
||||
where: { id: player.wallet.id },
|
||||
data: { availableBalance: 88888.88 },
|
||||
});
|
||||
|
||||
await prisma.walletTransaction.upsert({
|
||||
where: { transactionId: 'DEMO-DEP-001' },
|
||||
create: {
|
||||
transactionId: 'DEMO-DEP-001',
|
||||
userId: player.id,
|
||||
walletId: player.wallet.id,
|
||||
transactionType: 'DEPOSIT',
|
||||
amount: 50000,
|
||||
balanceBefore: 1000,
|
||||
balanceAfter: 51000,
|
||||
frozenBefore: 0,
|
||||
frozenAfter: 0,
|
||||
remark: '演示充值',
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
await prisma.walletTransaction.upsert({
|
||||
where: { transactionId: 'DEMO-DEP-002' },
|
||||
create: {
|
||||
transactionId: 'DEMO-DEP-002',
|
||||
userId: player.id,
|
||||
walletId: player.wallet.id,
|
||||
transactionType: 'DEPOSIT',
|
||||
amount: 37888.88,
|
||||
balanceBefore: 51000,
|
||||
balanceAfter: 88888.88,
|
||||
frozenBefore: 0,
|
||||
frozenAfter: 0,
|
||||
remark: '演示充值(二笔)',
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const sampleSel = await prisma.marketSelection.findFirst({
|
||||
where: {
|
||||
status: 'OPEN',
|
||||
market: { marketType: 'FT_1X2', match: { status: 'PUBLISHED' } },
|
||||
},
|
||||
include: { market: { include: { match: true } } },
|
||||
});
|
||||
|
||||
if (sampleSel && !(await prisma.bet.findUnique({ where: { betNo: 'DEMO-BET-001' } }))) {
|
||||
const odds = Number(sampleSel.odds);
|
||||
const stake = 200;
|
||||
await prisma.bet.create({
|
||||
data: {
|
||||
betNo: 'DEMO-BET-001',
|
||||
userId: player.id,
|
||||
agentId: player.parentId,
|
||||
betType: 'SINGLE',
|
||||
stake,
|
||||
totalOdds: odds,
|
||||
potentialReturn: stake * odds,
|
||||
status: 'PENDING',
|
||||
requestId: 'seed-demo-bet-001',
|
||||
selections: {
|
||||
create: {
|
||||
matchId: sampleSel.market.matchId,
|
||||
marketId: sampleSel.marketId,
|
||||
selectionId: sampleSel.id,
|
||||
marketType: sampleSel.market.marketType,
|
||||
period: sampleSel.market.period,
|
||||
selectionNameSnapshot: sampleSel.selectionName,
|
||||
odds: sampleSel.odds,
|
||||
oddsVersion: sampleSel.oddsVersion,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const settledSel = await prisma.marketSelection.findFirst({
|
||||
where: {
|
||||
market: { marketType: 'FT_1X2' },
|
||||
selectionCode: 'DRAW',
|
||||
},
|
||||
include: { market: true },
|
||||
});
|
||||
|
||||
if (settledSel && !(await prisma.bet.findUnique({ where: { betNo: 'DEMO-BET-002' } }))) {
|
||||
const odds = Number(settledSel.odds);
|
||||
const stake = 50;
|
||||
await prisma.bet.create({
|
||||
data: {
|
||||
betNo: 'DEMO-BET-002',
|
||||
userId: player.id,
|
||||
agentId: player.parentId,
|
||||
betType: 'SINGLE',
|
||||
stake,
|
||||
totalOdds: odds,
|
||||
potentialReturn: stake * odds,
|
||||
actualReturn: stake * odds,
|
||||
status: 'WON',
|
||||
settlementStatus: 'SETTLED',
|
||||
settledAt: new Date(Date.now() - 86400000),
|
||||
requestId: 'seed-demo-bet-002',
|
||||
selections: {
|
||||
create: {
|
||||
matchId: settledSel.market.matchId,
|
||||
marketId: settledSel.marketId,
|
||||
selectionId: settledSel.id,
|
||||
marketType: settledSel.market.marketType,
|
||||
period: settledSel.market.period,
|
||||
selectionNameSnapshot: settledSel.selectionName,
|
||||
odds: settledSel.odds,
|
||||
oddsVersion: settledSel.oddsVersion,
|
||||
resultStatus: 'WIN',
|
||||
effectiveOdds: settledSel.odds,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(' Player demo: wallet + transactions + sample bets');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding database...');
|
||||
|
||||
@@ -107,70 +670,79 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
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 seedSportsDemo();
|
||||
await seedOutrightDemo();
|
||||
await seedPlayerDemo();
|
||||
|
||||
await prisma.content.create({
|
||||
data: {
|
||||
contentType: 'BANNER',
|
||||
status: 'ACTIVE',
|
||||
sortOrder: 1,
|
||||
linkType: 'ROUTE',
|
||||
linkTarget: '/football',
|
||||
translations: {
|
||||
create: [
|
||||
{ locale: 'zh-CN', title: '欢迎投注', body: '足球赛事火热进行中' },
|
||||
{ locale: 'en-US', title: 'Welcome', body: 'Football matches available' },
|
||||
{ locale: 'zh-CN', title: '欢迎投注', body: '足球赛事火热进行中', imageUrl: '/uploads/banners/welcome.svg' },
|
||||
{ locale: 'en-US', title: 'Welcome', body: 'Football matches available', imageUrl: '/uploads/banners/welcome.svg' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}).catch(() => {});
|
||||
|
||||
await prisma.content.create({
|
||||
data: {
|
||||
contentType: 'BANNER',
|
||||
status: 'ACTIVE',
|
||||
sortOrder: 2,
|
||||
translations: {
|
||||
create: [
|
||||
{ locale: 'zh-CN', title: '首存礼遇', body: '新会员专属优惠', imageUrl: '/uploads/banners/promo.svg' },
|
||||
{ locale: 'en-US', title: 'First Deposit', body: 'New member offer', imageUrl: '/uploads/banners/promo.svg' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}).catch(() => {});
|
||||
|
||||
await prisma.content.create({
|
||||
data: {
|
||||
contentType: 'BANNER',
|
||||
status: 'ACTIVE',
|
||||
sortOrder: 3,
|
||||
linkType: 'ROUTE',
|
||||
linkTarget: '/football',
|
||||
translations: {
|
||||
create: [
|
||||
{ locale: 'zh-CN', title: '热门赛事', body: '五大联赛天天有球', imageUrl: '/uploads/banners/hot-matches.svg' },
|
||||
{ locale: 'en-US', title: 'Hot Matches', body: 'Top leagues daily', imageUrl: '/uploads/banners/hot-matches.svg' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}).catch(() => {});
|
||||
|
||||
await prisma.content.create({
|
||||
data: {
|
||||
contentType: 'TICKER',
|
||||
status: 'ACTIVE',
|
||||
sortOrder: 1,
|
||||
translations: {
|
||||
create: [
|
||||
{ locale: 'zh-CN', body: '欢迎来到 TheBet365 · 热门赛事每日更新 · 请理性投注' },
|
||||
{ locale: 'en-US', body: 'Welcome to TheBet365 · Daily hot matches · Bet responsibly' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}).catch(() => {});
|
||||
|
||||
await prisma.content.create({
|
||||
data: {
|
||||
contentType: 'NOTICE',
|
||||
status: 'ACTIVE',
|
||||
sortOrder: 1,
|
||||
translations: {
|
||||
create: [
|
||||
{ locale: 'zh-CN', title: '系统维护通知:每周一 04:00-05:00 例行维护,敬请谅解' },
|
||||
{ locale: 'en-US', title: 'Maintenance: Every Mon 04:00-05:00 UTC' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
@@ -62,6 +63,16 @@ class LocaleDto {
|
||||
locale!: string;
|
||||
}
|
||||
|
||||
class UpdateProfileDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
}
|
||||
|
||||
@ApiTags('Player')
|
||||
@Controller('player')
|
||||
@UseGuards(JwtAuthGuard, PlayerGuard)
|
||||
@@ -88,6 +99,12 @@ export class PlayerController {
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Patch('profile')
|
||||
async updateProfile(@CurrentUser('id') userId: bigint, @Body() dto: UpdateProfileDto) {
|
||||
const user = await this.users.updateProfile(userId, dto);
|
||||
return jsonResponse(user);
|
||||
}
|
||||
|
||||
@Get('home')
|
||||
async home(@CurrentUser('locale') locale: string) {
|
||||
const [banners, notices, ticker, hotMatches, todayMatches] = await Promise.all([
|
||||
@@ -115,6 +132,12 @@ export class PlayerController {
|
||||
return jsonResponse(items);
|
||||
}
|
||||
|
||||
@Get('outrights')
|
||||
async listOutrights(@CurrentUser('locale') locale: string) {
|
||||
const items = await this.matches.listOutrights(locale);
|
||||
return jsonResponse(items);
|
||||
}
|
||||
|
||||
@Get('matches/:id')
|
||||
async matchDetail(@Param('id') id: string, @CurrentUser('locale') locale: string) {
|
||||
const match = await this.matches.getMatchDetail(BigInt(id), locale);
|
||||
@@ -149,11 +172,13 @@ export class PlayerController {
|
||||
@Get('bets')
|
||||
async myBets(
|
||||
@CurrentUser('id') userId: bigint,
|
||||
@CurrentUser('locale') locale: string,
|
||||
@Query('status') status?: string,
|
||||
@Query('page') page?: string,
|
||||
) {
|
||||
const result = await this.bets.getUserBets(userId, status, page ? parseInt(page) : 1);
|
||||
return jsonResponse(result);
|
||||
const items = await this.matches.enrichBetsForHistory(result.items, locale);
|
||||
return jsonResponse({ ...result, items });
|
||||
}
|
||||
|
||||
@Get('bets/:betNo')
|
||||
|
||||
@@ -96,9 +96,8 @@ export class MatchesService {
|
||||
leagueId: bigint;
|
||||
homeTeamId: bigint;
|
||||
awayTeamId: bigint;
|
||||
league?: unknown;
|
||||
homeTeam?: unknown;
|
||||
awayTeam?: unknown;
|
||||
homeTeam?: { code: string };
|
||||
awayTeam?: { code: string };
|
||||
markets?: unknown[];
|
||||
};
|
||||
const [leagueName, homeName, awayName] = await Promise.all([
|
||||
@@ -108,9 +107,13 @@ export class MatchesService {
|
||||
]);
|
||||
return {
|
||||
...match,
|
||||
id: m.id.toString(),
|
||||
leagueId: m.leagueId.toString(),
|
||||
leagueName,
|
||||
homeTeamName: homeName,
|
||||
awayTeamName: awayName,
|
||||
homeTeamCode: m.homeTeam?.code ?? '',
|
||||
awayTeamCode: m.awayTeam?.code ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,9 +121,12 @@ export class MatchesService {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
where: {
|
||||
status: 'PUBLISHED',
|
||||
isOutright: false,
|
||||
...(leagueId ? { leagueId } : {}),
|
||||
},
|
||||
include: {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
markets: {
|
||||
where: { status: 'OPEN' },
|
||||
include: { selections: { where: { status: 'OPEN' } } },
|
||||
@@ -136,8 +142,11 @@ export class MatchesService {
|
||||
const match = await this.prisma.match.findUnique({
|
||||
where: { id: matchId },
|
||||
include: {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
markets: {
|
||||
include: { selections: true },
|
||||
where: { status: 'OPEN' },
|
||||
include: { selections: { where: { status: 'OPEN' }, orderBy: { sortOrder: 'asc' } } },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
score: true,
|
||||
@@ -147,12 +156,180 @@ export class MatchesService {
|
||||
return this.enrichMatch(match, locale);
|
||||
}
|
||||
|
||||
async listOutrights(locale = 'en-US') {
|
||||
const matches = await this.prisma.match.findMany({
|
||||
where: { status: 'PUBLISHED', isOutright: true },
|
||||
include: {
|
||||
markets: {
|
||||
where: { marketType: 'OUTRIGHT_WINNER', status: 'OPEN' },
|
||||
include: {
|
||||
selections: {
|
||||
where: { status: 'OPEN' },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ displayOrder: 'asc' }, { startTime: 'asc' }],
|
||||
});
|
||||
|
||||
const results = [];
|
||||
for (const match of matches) {
|
||||
const leagueName = await this.getTranslation('LEAGUE', match.leagueId, locale);
|
||||
const market = match.markets[0];
|
||||
if (!market) continue;
|
||||
|
||||
const selections = await Promise.all(
|
||||
market.selections.map(async (sel) => {
|
||||
const teamCode = sel.selectionCode.replace(/^TEAM_/, '');
|
||||
const team = await this.prisma.team.findUnique({ where: { code: teamCode } });
|
||||
const teamName = team
|
||||
? await this.getTranslation('TEAM', team.id, locale)
|
||||
: sel.selectionName;
|
||||
return {
|
||||
id: sel.id.toString(),
|
||||
teamCode,
|
||||
teamName,
|
||||
odds: sel.odds.toString(),
|
||||
oddsVersion: sel.oddsVersion.toString(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
results.push({
|
||||
id: match.id.toString(),
|
||||
leagueId: match.leagueId.toString(),
|
||||
leagueName,
|
||||
title: `*${leagueName} 冠军`,
|
||||
marketId: market.id.toString(),
|
||||
selections,
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private marketLabelKey(marketType: string): string {
|
||||
const keys: Record<string, string> = {
|
||||
FT_1X2: '全场独赢',
|
||||
FT_HANDICAP: '全场让球',
|
||||
FT_OVER_UNDER: '全场大小',
|
||||
FT_ODD_EVEN: '全场单双',
|
||||
HT_1X2: '半场独赢',
|
||||
HT_HANDICAP: '半场让球',
|
||||
HT_OVER_UNDER: '半场大小',
|
||||
OUTRIGHT_WINNER: '冠军',
|
||||
FT_CORRECT_SCORE: '波胆',
|
||||
HT_CORRECT_SCORE: '上半场波胆',
|
||||
SH_CORRECT_SCORE: '下半场波胆',
|
||||
};
|
||||
return keys[marketType] ?? marketType;
|
||||
}
|
||||
|
||||
async enrichBetsForHistory(
|
||||
bets: Array<{
|
||||
betNo: string;
|
||||
betType: string;
|
||||
stake: unknown;
|
||||
totalOdds: unknown;
|
||||
potentialReturn: unknown;
|
||||
actualReturn: unknown;
|
||||
status: string;
|
||||
placedAt: Date;
|
||||
selections: Array<{
|
||||
matchId: bigint | null;
|
||||
marketType: string;
|
||||
selectionNameSnapshot: string;
|
||||
odds: unknown;
|
||||
resultStatus?: string | null;
|
||||
}>;
|
||||
}>,
|
||||
locale: string,
|
||||
) {
|
||||
const matchIds = [
|
||||
...new Set(
|
||||
bets.flatMap((b) =>
|
||||
b.selections.map((s) => s.matchId).filter((id): id is bigint => id != null),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
const matches =
|
||||
matchIds.length > 0
|
||||
? await this.prisma.match.findMany({
|
||||
where: { id: { in: matchIds } },
|
||||
include: { homeTeam: true, awayTeam: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const matchMeta = new Map<
|
||||
string,
|
||||
{ leagueName: string; matchTitle: string; isOutright: boolean }
|
||||
>();
|
||||
|
||||
for (const m of matches) {
|
||||
const [leagueName, homeName, awayName] = await Promise.all([
|
||||
this.getTranslation('LEAGUE', m.leagueId, locale),
|
||||
this.getTranslation('TEAM', m.homeTeamId, locale),
|
||||
this.getTranslation('TEAM', m.awayTeamId, locale),
|
||||
]);
|
||||
matchMeta.set(m.id.toString(), {
|
||||
leagueName,
|
||||
matchTitle: m.isOutright ? leagueName : `${homeName} vs ${awayName}`,
|
||||
isOutright: m.isOutright,
|
||||
});
|
||||
}
|
||||
|
||||
return bets.map((bet) => {
|
||||
const firstMatchId = bet.selections.find((s) => s.matchId)?.matchId?.toString();
|
||||
const meta = firstMatchId ? matchMeta.get(firstMatchId) : undefined;
|
||||
const isParlay = bet.betType === 'PARLAY' || bet.selections.length > 1;
|
||||
|
||||
const legs = bet.selections.map((sel) => {
|
||||
const mid = sel.matchId?.toString();
|
||||
const m = mid ? matchMeta.get(mid) : undefined;
|
||||
return {
|
||||
marketType: sel.marketType,
|
||||
marketLabel: this.marketLabelKey(sel.marketType),
|
||||
selectionName: sel.selectionNameSnapshot,
|
||||
odds: sel.odds,
|
||||
resultStatus: sel.resultStatus,
|
||||
matchTitle: m?.matchTitle ?? sel.selectionNameSnapshot,
|
||||
leagueName: m?.leagueName ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
betNo: bet.betNo,
|
||||
betType: bet.betType,
|
||||
stake: bet.stake,
|
||||
totalOdds: bet.totalOdds,
|
||||
potentialReturn: bet.potentialReturn,
|
||||
actualReturn: bet.actualReturn,
|
||||
status: bet.status,
|
||||
placedAt: bet.placedAt,
|
||||
leagueName: isParlay
|
||||
? 'Parlay'
|
||||
: meta?.leagueName ?? legs[0]?.leagueName ?? '',
|
||||
legCount: bet.selections.length,
|
||||
matchTitle: isParlay
|
||||
? ''
|
||||
: meta?.matchTitle ?? legs[0]?.matchTitle ?? bet.betNo,
|
||||
pickLabel: isParlay
|
||||
? ''
|
||||
: `${legs[0]?.marketLabel ?? ''}: ${legs[0]?.selectionName ?? ''}`,
|
||||
legs,
|
||||
isParlay,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async autoCloseMatches() {
|
||||
const now = new Date();
|
||||
await this.prisma.match.updateMany({
|
||||
where: {
|
||||
status: 'PUBLISHED',
|
||||
isOutright: false,
|
||||
startTime: { lte: now },
|
||||
},
|
||||
data: { status: 'CLOSED', closeTime: now },
|
||||
|
||||
@@ -12,6 +12,17 @@ export class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateProfile(userId: bigint, data: { phone?: string; email?: string }) {
|
||||
const phone = data.phone?.trim() || null;
|
||||
const email = data.email?.trim() || null;
|
||||
await this.prisma.userPreference.upsert({
|
||||
where: { userId },
|
||||
create: { userId, phone, email },
|
||||
update: { phone, email },
|
||||
});
|
||||
return this.findById(userId);
|
||||
}
|
||||
|
||||
async updateLocale(userId: bigint, locale: string) {
|
||||
await this.prisma.user.update({
|
||||
where: { id: userId },
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { join } from 'path';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
const uploadDir = process.env.UPLOAD_DIR || join(__dirname, '..', '..', 'uploads');
|
||||
app.useStaticAssets(uploadDir, { prefix: '/uploads/' });
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
app.enableCors({ origin: true, credentials: true });
|
||||
|
||||
@@ -32,10 +32,18 @@ export function generateBatchNo(prefix: string): string {
|
||||
return `${prefix}${ts}`;
|
||||
}
|
||||
|
||||
function isDecimalLike(obj: object): obj is { toJSON: () => string } {
|
||||
return (
|
||||
typeof (obj as { toJSON?: unknown }).toJSON === 'function' &&
|
||||
typeof (obj as { toFixed?: unknown }).toFixed === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
export function serializeBigInt(obj: unknown): unknown {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
if (typeof obj === 'bigint') return obj.toString();
|
||||
if (obj instanceof Date) return obj.toISOString();
|
||||
if (typeof obj === 'object' && isDecimalLike(obj)) return obj.toJSON();
|
||||
if (Array.isArray(obj)) return obj.map(serializeBigInt);
|
||||
if (typeof obj === 'object') {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
Reference in New Issue
Block a user