@@ -1,4 +1,5 @@
|
||||
import { Controller, Post, Body, HttpCode } from "@nestjs/common";
|
||||
import { Throttle } from "@nestjs/throttler";
|
||||
import { I18n, I18nContext } from "nestjs-i18n";
|
||||
import { ApiTags, ApiOperation, ApiOkResponse } from "@nestjs/swagger";
|
||||
import { AuthService } from "./auth.service";
|
||||
@@ -21,6 +22,7 @@ export class AuthController {
|
||||
|
||||
@Post("register")
|
||||
@Public()
|
||||
@Throttle({ default: { limit: 10, ttl: 60000 } })
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: "Register a new user" })
|
||||
@ApiOkResponse({
|
||||
@@ -37,6 +39,7 @@ export class AuthController {
|
||||
|
||||
@Post("login")
|
||||
@Public()
|
||||
@Throttle({ default: { limit: 5, ttl: 60000 } })
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: "Login with email and password" })
|
||||
@ApiOkResponse({ description: "Login successful", type: TokenResponseDto })
|
||||
@@ -50,6 +53,7 @@ export class AuthController {
|
||||
|
||||
@Post("refresh")
|
||||
@Public()
|
||||
@Throttle({ default: { limit: 10, ttl: 60000 } })
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: "Refresh access token" })
|
||||
@ApiOkResponse({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "../../../database/prisma.service";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -108,16 +109,21 @@ export class FrequencyEngineService {
|
||||
venue: "home" | "away",
|
||||
oddsBand: string,
|
||||
): Promise<TeamFrequencyRow | null> {
|
||||
const venueColumn = venue === "home" ? "m.home_team_id" : "m.away_team_id";
|
||||
const oddsSelection = venue === "home" ? "'1'" : "'2'";
|
||||
// venue is a typed literal ("home"|"away") — safe to use with Prisma.raw()
|
||||
const venueColumnRaw = Prisma.raw(
|
||||
venue === "home" ? "m.home_team_id" : "m.away_team_id",
|
||||
);
|
||||
const oddsSelectionValue = venue === "home" ? "1" : "2";
|
||||
const winConditionRaw = Prisma.raw(
|
||||
venue === "home" ? "score_home > score_away" : "score_away > score_home",
|
||||
);
|
||||
const bandRange = this.parseBandRange(oddsBand);
|
||||
|
||||
if (!bandRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rows = await this.prisma.$queryRawUnsafe<TeamFrequencyRow[]>(
|
||||
`
|
||||
const rows = await this.prisma.$queryRaw<TeamFrequencyRow[]>(Prisma.sql`
|
||||
WITH team_matches AS (
|
||||
SELECT
|
||||
m.id AS match_id,
|
||||
@@ -127,32 +133,26 @@ export class FrequencyEngineService {
|
||||
CAST(os.odd_value AS DECIMAL) AS team_odds
|
||||
FROM matches m
|
||||
JOIN odd_categories oc ON oc.match_id = m.id AND oc.name = 'Maç Sonucu'
|
||||
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id AND os.name = ${oddsSelection}
|
||||
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id AND os.name = ${oddsSelectionValue}
|
||||
WHERE m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND ${venueColumn} = $1
|
||||
AND CAST(os.odd_value AS DECIMAL) >= $2
|
||||
AND CAST(os.odd_value AS DECIMAL) < $3
|
||||
AND ${venueColumnRaw} = ${teamId}
|
||||
AND CAST(os.odd_value AS DECIMAL) >= ${bandRange.min}
|
||||
AND CAST(os.odd_value AS DECIMAL) < ${bandRange.max}
|
||||
)
|
||||
SELECT
|
||||
$1::text AS team_id,
|
||||
$4::text AS venue,
|
||||
$5::text AS odds_band,
|
||||
${teamId}::text AS team_id,
|
||||
${venue}::text AS venue,
|
||||
${oddsBand}::text AS odds_band,
|
||||
COUNT(*)::int AS total_matches,
|
||||
COALESCE(AVG(CASE WHEN total_goals > 1 THEN 1.0 ELSE 0.0 END), 0)::float AS ou15_rate,
|
||||
COALESCE(AVG(CASE WHEN total_goals > 2 THEN 1.0 ELSE 0.0 END), 0)::float AS ou25_rate,
|
||||
COALESCE(AVG(CASE WHEN total_goals > 3 THEN 1.0 ELSE 0.0 END), 0)::float AS ou35_rate,
|
||||
COALESCE(AVG(CASE WHEN score_home > 0 AND score_away > 0 THEN 1.0 ELSE 0.0 END), 0)::float AS btts_rate,
|
||||
COALESCE(AVG(CASE WHEN ${venue === "home" ? "score_home > score_away" : "score_away > score_home"} THEN 1.0 ELSE 0.0 END), 0)::float AS win_rate,
|
||||
COALESCE(AVG(CASE WHEN ${winConditionRaw} THEN 1.0 ELSE 0.0 END), 0)::float AS win_rate,
|
||||
COALESCE(AVG(total_goals), 0)::float AS avg_goals
|
||||
FROM team_matches
|
||||
`,
|
||||
teamId,
|
||||
bandRange.min,
|
||||
bandRange.max,
|
||||
venue,
|
||||
oddsBand,
|
||||
);
|
||||
`);
|
||||
|
||||
if (!rows.length || rows[0].total_matches < MIN_MATCHES) {
|
||||
return null;
|
||||
@@ -335,8 +335,7 @@ export class FrequencyEngineService {
|
||||
|
||||
if (matchIds && matchIds.length > 0) {
|
||||
// Belirli maçlar istendi
|
||||
return this.prisma.$queryRawUnsafe<UpcomingMatchRow[]>(
|
||||
`
|
||||
return this.prisma.$queryRaw<UpcomingMatchRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
lm.id AS match_id,
|
||||
lm.home_team_id,
|
||||
@@ -359,18 +358,15 @@ export class FrequencyEngineService {
|
||||
LEFT JOIN teams ht ON lm.home_team_id = ht.id
|
||||
LEFT JOIN teams at ON lm.away_team_id = at.id
|
||||
LEFT JOIN leagues l ON lm.league_id = l.id
|
||||
WHERE lm.id = ANY($1)
|
||||
WHERE lm.id = ANY(${matchIds})
|
||||
AND lm.odds IS NOT NULL
|
||||
AND lm.odds != 'null'::jsonb
|
||||
ORDER BY lm.mst_utc ASC
|
||||
`,
|
||||
matchIds,
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
// Otomatik: yaklaşan tüm maçlar
|
||||
return this.prisma.$queryRawUnsafe<UpcomingMatchRow[]>(
|
||||
`
|
||||
return this.prisma.$queryRaw<UpcomingMatchRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
lm.id AS match_id,
|
||||
lm.home_team_id,
|
||||
@@ -393,26 +389,22 @@ export class FrequencyEngineService {
|
||||
LEFT JOIN teams ht ON lm.home_team_id = ht.id
|
||||
LEFT JOIN teams at ON lm.away_team_id = at.id
|
||||
LEFT JOIN leagues l ON lm.league_id = l.id
|
||||
WHERE lm.mst_utc >= $1
|
||||
WHERE lm.mst_utc >= ${BigInt(nowMs)}
|
||||
AND lm.sport = 'football'
|
||||
AND lm.odds IS NOT NULL
|
||||
AND lm.odds != 'null'::jsonb
|
||||
AND (lm.status IS NULL OR lm.status NOT IN ('FT', 'AET', 'PEN', 'ABD', 'CANC', 'PST', 'SUSP', 'INT', 'AWD', 'WO'))
|
||||
AND (lm.state IS NULL OR lm.state NOT IN ('after', 'postponed', 'cancelled', 'abandoned'))
|
||||
ORDER BY lm.mst_utc ASC
|
||||
LIMIT $2
|
||||
`,
|
||||
BigInt(nowMs),
|
||||
limit,
|
||||
);
|
||||
LIMIT ${limit}
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lig bazlı gol profili.
|
||||
*/
|
||||
async getLeagueProfile(leagueId: string): Promise<LeagueProfileRow | null> {
|
||||
const rows = await this.prisma.$queryRawUnsafe<LeagueProfileRow[]>(
|
||||
`
|
||||
const rows = await this.prisma.$queryRaw<LeagueProfileRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
m.league_id,
|
||||
l.name AS league_name,
|
||||
@@ -424,12 +416,10 @@ export class FrequencyEngineService {
|
||||
JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.league_id = $1
|
||||
AND m.league_id = ${leagueId}
|
||||
GROUP BY m.league_id, l.name
|
||||
HAVING COUNT(*) >= 20
|
||||
`,
|
||||
leagueId,
|
||||
);
|
||||
`);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import {
|
||||
Sport,
|
||||
@@ -858,11 +859,11 @@ export class FeederPersistenceService {
|
||||
// Use raw SQL for performance — Prisma's { some: {} } relation filters
|
||||
// generate heavy correlated subqueries that hang on Raspberry Pi with
|
||||
// large tables (15M+ odd_selections, 3M+ participations).
|
||||
const result = await this.prisma.$queryRawUnsafe<Array<{ id: string }>>(
|
||||
`
|
||||
const result = await this.prisma.$queryRaw<Array<{ id: string }>>(
|
||||
Prisma.sql`
|
||||
SELECT m.id
|
||||
FROM matches m
|
||||
WHERE m.id = ANY($1::text[])
|
||||
WHERE m.id = ANY(${matchIds}::text[])
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
AND (
|
||||
(m.sport = 'football'
|
||||
@@ -875,7 +876,6 @@ export class FeederPersistenceService {
|
||||
AND EXISTS (SELECT 1 FROM basketball_player_stats bps WHERE bps.match_id = m.id))
|
||||
)
|
||||
`,
|
||||
matchIds,
|
||||
);
|
||||
|
||||
return result.map((r) => r.id);
|
||||
|
||||
Reference in New Issue
Block a user