feat: 更新玩法配置管理,简化字段并增强功能

- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。
- 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。
- 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
This commit is contained in:
2026-05-25 14:34:24 +08:00
parent 270d2e9af1
commit e27a00f260
74 changed files with 4469 additions and 280 deletions

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Services\Admin;
use App\Models\AdminUser;
/**
* 仪表盘可筛选分析数据:区间汇总、日趋势、玩法拆解。
*/
final class AdminDashboardAnalyticsBuilder
{
public function __construct(
private readonly AdminReportQueryService $reportQuery,
) {}
/**
* @param array{
* period?: string,
* date_from?: string|null,
* date_to?: string|null,
* metric?: string,
* play_code?: string|null
* } $filters
*
* @return array<string, mixed>|null
*/
public function build(AdminUser $admin, array $filters): ?array
{
if (! $this->canView($admin)) {
return null;
}
$period = (string) ($filters['period'] ?? 'last_7_days');
$metric = (string) ($filters['metric'] ?? 'overview');
$playCode = isset($filters['play_code']) && $filters['play_code'] !== ''
? (string) $filters['play_code']
: null;
$range = $this->reportQuery->resolveDashboardPeriod(
$period,
isset($filters['date_from']) ? (string) $filters['date_from'] : null,
isset($filters['date_to']) ? (string) $filters['date_to'] : null,
);
$dateFrom = $range['date_from'];
$dateTo = $range['date_to'];
$trend = $this->reportQuery->dailyProfitSeriesFilled($dateFrom, $dateTo);
return [
'period' => $period,
'metric' => $metric,
'play_code' => $playCode,
'date_from' => $dateFrom,
'date_to' => $dateTo,
'currency_code' => $this->reportQuery->resolvePeriodCurrencyCode($dateFrom, $dateTo),
'summary' => $this->reportQuery->periodFinanceTotals($dateFrom, $dateTo),
'daily_series' => $trend['series'],
'chart_meta' => [
'chart_date_from' => $trend['chart_date_from'],
'chart_date_to' => $trend['chart_date_to'],
'truncated' => $trend['truncated'],
'span_days' => $trend['span_days'],
],
'play_breakdown' => $this->reportQuery->playDimensionBreakdownRows(
$dateFrom,
$dateTo,
$playCode,
),
];
}
private function canView(AdminUser $admin): bool
{
return $admin->hasAdminPermission('prd.dashboard.view')
|| $admin->hasAdminPermission('prd.draw_result.manage')
|| $admin->hasAdminPermission('prd.draw_result.view')
|| $admin->hasAdminPermission('prd.risk.view')
|| $admin->hasAdminPermission('prd.risk.manage')
|| $admin->hasAdminPermission('prd.report.view');
}
}

View File

@@ -22,6 +22,7 @@ final class AdminDashboardSnapshotBuilder
{
public function __construct(
private readonly DrawHallSnapshotBuilder $hallSnapshot,
private readonly AdminReportQueryService $reportQuery,
) {}
/** @return array<string, mixed> */
@@ -34,6 +35,8 @@ final class AdminDashboardSnapshotBuilder
$out = [
'hall' => $hall,
'resolved_draw' => null,
'today_finance' => null,
'lifetime_finance' => null,
'finance' => null,
'draw' => null,
'risk' => null,
@@ -67,6 +70,8 @@ final class AdminDashboardSnapshotBuilder
];
if ($canDraw) {
$out['today_finance'] = $this->todayFinanceSummary();
$out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals();
$out['finance'] = $this->financeSummary($draw);
$out['draw'] = $this->drawPanel($draw);
$out['risk'] = $this->riskPanel($draw);
@@ -81,8 +86,11 @@ final class AdminDashboardSnapshotBuilder
private function canDrawFinanceAndRisk(AdminUser $admin): bool
{
return $admin->hasAdminPermission('prd.draw_result.manage')
|| $admin->hasAdminPermission('prd.draw_result.view');
return $admin->hasAdminPermission('prd.dashboard.view')
|| $admin->hasAdminPermission('prd.draw_result.manage')
|| $admin->hasAdminPermission('prd.draw_result.view')
|| $admin->hasAdminPermission('prd.risk.view')
|| $admin->hasAdminPermission('prd.risk.manage');
}
private function canWalletReconcile(AdminUser $admin): bool
@@ -99,6 +107,36 @@ final class AdminDashboardSnapshotBuilder
->count();
}
/**
* 按业务日汇总「今日」投注/派彩/盈亏(与报表中心 daily-profit 口径一致)。
*
* @return array<string, mixed>
*/
private function todayFinanceSummary(): array
{
$today = now()->toDateString();
$rows = $this->reportQuery->dailyProfitRows($today, $today);
$row = $rows[0] ?? [
'business_date' => $today,
'total_bet_minor' => 0,
'total_payout_minor' => 0,
'approx_house_gross_minor' => 0,
];
$currencyCode = (string) (TicketOrder::query()
->join('draws', 'draws.id', '=', 'ticket_orders.draw_id')
->where('draws.business_date', $today)
->value('ticket_orders.currency_code') ?? '');
return [
'business_date' => (string) $row['business_date'],
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
'total_bet_minor' => (int) $row['total_bet_minor'],
'total_payout_minor' => (int) $row['total_payout_minor'],
'approx_house_gross_minor' => (int) $row['approx_house_gross_minor'],
];
}
/** @return array<string, mixed> */
private function financeSummary(Draw $draw): array
{

View File

@@ -28,6 +28,180 @@ final class AdminReportQueryService
return ['date_from' => $dateFrom, 'date_to' => $dateTo];
}
/**
* @return array{date_from: string, date_to: string}
*/
public function resolveDashboardPeriod(string $period, ?string $dateFrom, ?string $dateTo): array
{
$today = now()->toDateString();
$range = match ($period) {
'today' => ['date_from' => $today, 'date_to' => $today],
'last_7_days' => [
'date_from' => now()->subDays(6)->toDateString(),
'date_to' => $today,
],
'last_30_days' => [
'date_from' => now()->subDays(29)->toDateString(),
'date_to' => $today,
],
'this_month' => [
'date_from' => now()->startOfMonth()->toDateString(),
'date_to' => $today,
],
'lifetime' => $this->lifetimeBusinessDateBounds(),
'custom' => [
'date_from' => $dateFrom !== null && $dateFrom !== '' ? $dateFrom : $today,
'date_to' => $dateTo !== null && $dateTo !== '' ? $dateTo : $today,
],
default => ['date_from' => $today, 'date_to' => $today],
};
$from = $range['date_from'];
$to = $range['date_to'];
if ($from > $to) {
[$from, $to] = [$to, $from];
}
return ['date_from' => $from, 'date_to' => $to];
}
/**
* @return array{date_from: string, date_to: string}
*/
private function lifetimeBusinessDateBounds(): array
{
$today = now()->toDateString();
$bounds = DB::table('draws as d')
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
->selectRaw('MIN(d.business_date) as date_from')
->selectRaw('MAX(d.business_date) as date_to')
->first();
$from = $this->formatBusinessDateValue($bounds?->date_from) ?? $today;
$to = $this->formatBusinessDateValue($bounds?->date_to) ?? $today;
return ['date_from' => $from, 'date_to' => $to];
}
/**
* @return array{
* total_bet_minor: int,
* total_payout_minor: int,
* approx_house_gross_minor: int,
* draw_count: int,
* business_day_count: int
* }
*/
public function periodFinanceTotals(string $dateFrom, string $dateTo): array
{
$rows = $this->dailyProfitRows($dateFrom, $dateTo);
$totalBet = 0;
$totalPayout = 0;
$totalGross = 0;
foreach ($rows as $row) {
$totalBet += (int) $row['total_bet_minor'];
$totalPayout += (int) $row['total_payout_minor'];
$totalGross += (int) $row['approx_house_gross_minor'];
}
$activity = DB::table('draws as d')
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
->whereBetween('d.business_date', [$dateFrom, $dateTo])
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
->first();
return [
'total_bet_minor' => $totalBet,
'total_payout_minor' => $totalPayout,
'approx_house_gross_minor' => $totalGross,
'draw_count' => (int) ($activity->draw_count ?? 0),
'business_day_count' => (int) ($activity->business_day_count ?? 0),
];
}
/**
* 连续业务日序列(无数据日补零),用于趋势图。
*
* @return list<array<string, mixed>>
*/
public function dailyProfitSeriesFilled(string $dateFrom, string $dateTo, int $maxDays = 90): array
{
$from = Carbon::parse($dateFrom)->startOfDay();
$to = Carbon::parse($dateTo)->startOfDay();
$spanDays = (int) $from->diffInDays($to) + 1;
$chartFrom = $dateFrom;
$chartTo = $dateTo;
$truncated = false;
if ($spanDays > $maxDays) {
$chartFrom = $to->copy()->subDays($maxDays - 1)->format('Y-m-d');
$truncated = true;
}
$indexed = collect($this->dailyProfitRows($chartFrom, $chartTo))->keyBy('business_date');
$cursor = Carbon::parse($chartFrom)->startOfDay();
$end = Carbon::parse($chartTo)->startOfDay();
$series = [];
while ($cursor <= $end) {
$key = $cursor->format('Y-m-d');
$series[] = $indexed[$key] ?? [
'business_date' => $key,
'total_bet_minor' => 0,
'total_payout_minor' => 0,
'approx_house_gross_minor' => 0,
];
$cursor->addDay();
}
return [
'series' => $series,
'chart_date_from' => $chartFrom,
'chart_date_to' => $chartTo,
'truncated' => $truncated,
'span_days' => $spanDays,
];
}
/**
* @return list<array<string, mixed>>
*/
public function playDimensionBreakdownRows(
string $dateFrom,
string $dateTo,
?string $playCode = null,
int $limit = 12,
): array {
return $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo)
->orderByDesc('total_bet_minor')
->limit($limit)
->get()
->map(static function (object $row): array {
return [
'play_code' => (string) $row->play_code,
'dimension' => (int) $row->dimension,
'total_bet_minor' => (int) $row->total_bet_minor,
'total_payout_minor' => (int) $row->total_payout_minor,
'approx_house_gross_minor' => (int) $row->approx_house_gross_minor,
];
})
->values()
->all();
}
public function resolvePeriodCurrencyCode(string $dateFrom, string $dateTo): ?string
{
$currencyCode = (string) (DB::table('ticket_orders as o')
->join('draws as d', 'd.id', '=', 'o.draw_id')
->whereBetween('d.business_date', [$dateFrom, $dateTo])
->orderByDesc('o.id')
->value('o.currency_code') ?? '');
return $currencyCode !== '' ? $currencyCode : null;
}
public function dailyProfitPaginated(string $dateFrom, string $dateTo, int $page, int $perPage): LengthAwarePaginator
{
$rows = $this->dailyProfitRows($dateFrom, $dateTo);
@@ -80,6 +254,57 @@ final class AdminReportQueryService
->all();
}
/**
* 全平台历史累计投注/派彩/盈亏(与 daily-profit 同口径,不限业务日)。
*
* @return array{
* currency_code: ?string,
* total_bet_minor: int,
* total_payout_minor: int,
* approx_house_gross_minor: int,
* draw_count: int,
* business_day_count: int,
* date_from: ?string,
* date_to: ?string
* }
*/
public function platformLifetimeTotals(): array
{
$totalBetMinor = (int) DB::table('ticket_orders')->sum('total_actual_deduct');
$payoutAgg = DB::table('ticket_items')
->selectRaw('COALESCE(SUM(win_amount), 0) as win_minor, COALESCE(SUM(jackpot_win_amount), 0) as jackpot_minor')
->first();
$totalPayoutMinor = (int) ($payoutAgg->win_minor ?? 0) + (int) ($payoutAgg->jackpot_minor ?? 0);
$activity = DB::table('draws as d')
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
->selectRaw('MIN(d.business_date) as date_from')
->selectRaw('MAX(d.business_date) as date_to')
->first();
$drawCount = (int) ($activity->draw_count ?? 0);
$businessDayCount = (int) ($activity->business_day_count ?? 0);
$dateFrom = $this->formatBusinessDateValue($activity?->date_from);
$dateTo = $this->formatBusinessDateValue($activity?->date_to);
$currencyCode = (string) (DB::table('ticket_orders')->orderByDesc('id')->value('currency_code') ?? '');
return [
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
'total_bet_minor' => $totalBetMinor,
'total_payout_minor' => $totalPayoutMinor,
'approx_house_gross_minor' => $totalBetMinor - $totalPayoutMinor,
'draw_count' => $drawCount,
'business_day_count' => $businessDayCount,
'date_from' => $dateFrom,
'date_to' => $dateTo,
];
}
public function playerWinLossPaginated(
?int $playerId,
string $dateFrom,
@@ -340,4 +565,26 @@ final class AdminReportQueryService
return $query;
}
private function formatBusinessDateValue(mixed $value): ?string
{
if ($value === null) {
return null;
}
if ($value instanceof Carbon) {
return $value->format('Y-m-d');
}
$raw = trim((string) $value);
if ($raw === '') {
return null;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}/', $raw, $m) === 1) {
return substr($m[0], 0, 10);
}
return $raw;
}
}