feat: 更新玩法配置管理,简化字段并增强功能
- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。 - 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。 - 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
This commit is contained in:
82
app/Services/Admin/AdminDashboardAnalyticsBuilder.php
Normal file
82
app/Services/Admin/AdminDashboardAnalyticsBuilder.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user