- 更新多个控制器和服务,使用 LotterySettings 服务获取彩票相关配置,如默认币种、开奖间隔、下注窗口等,提升代码一致性与可维护性。 - 移除 .env.example 中不再使用的配置项,建议通过后台管理进行设置。
983 lines
34 KiB
PHP
983 lines
34 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin;
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\Draw;
|
|
use App\Models\RiskPool;
|
|
use App\Models\RiskPoolLockLog;
|
|
use App\Models\SettlementBatch;
|
|
use App\Models\TicketItem;
|
|
use App\Models\TicketOrder;
|
|
use App\Models\TransferOrder;
|
|
use App\Models\WalletTxn;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Pagination\LengthAwarePaginator as PaginatorInstance;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* 报表中心聚合查询(模块十三)。
|
|
*/
|
|
final class AdminReportQueryService
|
|
{
|
|
/**
|
|
* @return array{date_from: string, date_to: string}
|
|
*/
|
|
public function resolveDateRange(?array $filters): array
|
|
{
|
|
$fromRaw = trim((string) ($filters['date_from'] ?? ''));
|
|
$toRaw = trim((string) ($filters['date_to'] ?? ''));
|
|
|
|
// 未传日期时按历史全量范围导出/查询,避免默认“仅今天”导致空数据。
|
|
if ($fromRaw === '' && $toRaw === '') {
|
|
return $this->lifetimeBusinessDateBounds();
|
|
}
|
|
|
|
$dateFrom = $fromRaw !== '' ? $fromRaw : $toRaw;
|
|
$dateTo = $toRaw !== '' ? $toRaw : $dateFrom;
|
|
|
|
if ($dateFrom > $dateTo) {
|
|
[$dateFrom, $dateTo] = [$dateTo, $dateFrom];
|
|
}
|
|
|
|
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);
|
|
$total = count($rows);
|
|
$offset = max(0, ($page - 1) * $perPage);
|
|
$items = array_slice($rows, $offset, $perPage);
|
|
|
|
return new PaginatorInstance($items, $total, $perPage, $page, [
|
|
'path' => PaginatorInstance::resolveCurrentPath(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function dailyProfitRows(string $dateFrom, string $dateTo): array
|
|
{
|
|
$betSub = DB::table('ticket_orders')
|
|
->selectRaw('draw_id, SUM(total_actual_deduct) as total_bet_minor')
|
|
->groupBy('draw_id');
|
|
$payoutSub = DB::table('ticket_items')
|
|
->selectRaw('draw_id, SUM(win_amount + jackpot_win_amount) as total_payout_minor')
|
|
->groupBy('draw_id');
|
|
|
|
return DB::table('draws as d')
|
|
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
|
->leftJoinSub($betSub, 'b', 'b.draw_id', '=', 'd.id')
|
|
->leftJoinSub($payoutSub, 'p', 'p.draw_id', '=', 'd.id')
|
|
->groupBy('d.business_date')
|
|
->orderBy('d.business_date')
|
|
->get([
|
|
'd.business_date',
|
|
DB::raw('COALESCE(SUM(b.total_bet_minor), 0) as total_bet_minor'),
|
|
DB::raw('COALESCE(SUM(p.total_payout_minor), 0) as total_payout_minor'),
|
|
DB::raw('COALESCE(SUM(b.total_bet_minor), 0) - COALESCE(SUM(p.total_payout_minor), 0) as approx_house_gross_minor'),
|
|
])
|
|
->map(static function (object $row): array {
|
|
$businessDate = $row->business_date instanceof Carbon
|
|
? $row->business_date->format('Y-m-d')
|
|
: (string) $row->business_date;
|
|
|
|
return [
|
|
'business_date' => $businessDate,
|
|
'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();
|
|
}
|
|
|
|
/**
|
|
* 全平台历史累计投注/派彩/盈亏(与 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,
|
|
string $dateTo,
|
|
int $page,
|
|
int $perPage,
|
|
): LengthAwarePaginator {
|
|
$query = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo);
|
|
|
|
return $query->paginate($perPage, ['*'], 'page', $page);
|
|
}
|
|
|
|
public function playDimensionPaginated(
|
|
?string $playCode,
|
|
string $dateFrom,
|
|
string $dateTo,
|
|
int $page,
|
|
int $perPage,
|
|
): LengthAwarePaginator {
|
|
$query = $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo);
|
|
|
|
return $query->paginate($perPage, ['*'], 'page', $page);
|
|
}
|
|
|
|
public function rebateCommissionPaginated(
|
|
?string $playCode,
|
|
string $dateFrom,
|
|
string $dateTo,
|
|
int $page,
|
|
int $perPage,
|
|
): LengthAwarePaginator {
|
|
$query = $this->rebateCommissionBaseQuery($playCode, $dateFrom, $dateTo);
|
|
|
|
return $query->paginate($perPage, ['*'], 'page', $page);
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
public function reportRows(string $reportType, ?array $filterJson): array
|
|
{
|
|
$range = $this->resolveDateRange($filterJson);
|
|
$dateFrom = $range['date_from'];
|
|
$dateTo = $range['date_to'];
|
|
|
|
return match ($reportType) {
|
|
'draw_profit_summary' => $this->drawProfitExportRows($filterJson),
|
|
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo),
|
|
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo),
|
|
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo),
|
|
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo),
|
|
'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
|
|
'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo),
|
|
'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo),
|
|
'hot_number_risk_report' => $this->hotNumberRiskExportRows($filterJson),
|
|
'sold_out_number_report' => $this->soldOutNumberExportRows($filterJson),
|
|
default => [
|
|
['报表类型', '开始日期', '结束日期'],
|
|
[$this->reportLabel($reportType), $dateFrom, $dateTo],
|
|
],
|
|
};
|
|
}
|
|
|
|
public function resolveOutputPathSuffix(string $reportType, ?array $filterJson, string $dateFrom, string $dateTo): string
|
|
{
|
|
if (in_array($reportType, ['draw_profit_summary', 'hot_number_risk_report', 'sold_out_number_report'], true)) {
|
|
$draw = $this->resolveDrawForReport($filterJson);
|
|
if ($draw !== null) {
|
|
$suffix = (string) $draw->draw_no;
|
|
$number = $this->normalizedNumberFromFilters($filterJson);
|
|
if ($reportType === 'hot_number_risk_report' && $number !== null) {
|
|
return $suffix.'_'.$number;
|
|
}
|
|
|
|
return $suffix;
|
|
}
|
|
}
|
|
|
|
return $dateFrom.'_'.$dateTo;
|
|
}
|
|
|
|
public function reportLabel(string $reportType): string
|
|
{
|
|
return match ($reportType) {
|
|
'draw_profit_summary' => '期号盈亏',
|
|
'daily_profit_summary' => '每日盈亏汇总',
|
|
'player_win_loss' => '玩家输赢报表',
|
|
'wallet_transfer_report', 'wallet_txns_daily', 'transfer_orders_daily' => '玩家转入转出报表',
|
|
'hot_number_risk_report' => '热门号码风险报表',
|
|
'play_dimension_report' => '玩法维度报表',
|
|
'sold_out_number_report' => '售罄号码报表',
|
|
'rebate_commission_report' => '佣金回水报表',
|
|
'audit_operation_report' => '后台操作审计报表',
|
|
default => $reportType,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function dailyProfitExportRows(string $dateFrom, string $dateTo): array
|
|
{
|
|
$rows = [
|
|
['日期', '下注', '派彩', '盈亏'],
|
|
];
|
|
foreach ($this->dailyProfitRows($dateFrom, $dateTo) as $row) {
|
|
$rows[] = [
|
|
$row['business_date'],
|
|
$row['total_bet_minor'],
|
|
$row['total_payout_minor'],
|
|
$row['approx_house_gross_minor'],
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function playerWinLossExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
|
{
|
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
|
$rows = [
|
|
['玩家ID', '用户名', '下注', '派彩', '净输赢'],
|
|
];
|
|
$items = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo)->get();
|
|
foreach ($items as $row) {
|
|
$rows[] = [
|
|
(int) $row->player_id,
|
|
(string) $row->username,
|
|
(int) $row->total_bet_minor,
|
|
(int) $row->total_payout_minor,
|
|
(int) $row->net_win_loss_minor,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function playDimensionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
|
{
|
|
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
|
$rows = [
|
|
['玩法', '维度', '下注', '派彩', '盈亏'],
|
|
];
|
|
$items = $this->playDimensionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get();
|
|
foreach ($items as $row) {
|
|
$rows[] = [
|
|
(string) $row->play_code,
|
|
(int) $row->dimension,
|
|
(int) $row->total_bet_minor,
|
|
(int) $row->total_payout_minor,
|
|
(int) $row->approx_house_gross_minor,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function rebateCommissionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
|
{
|
|
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
|
$rows = [
|
|
['玩法', '回水', '订单数', '注单数'],
|
|
];
|
|
$items = $this->rebateCommissionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get();
|
|
foreach ($items as $row) {
|
|
$rows[] = [
|
|
(string) $row->play_code,
|
|
(int) $row->total_rebate_minor,
|
|
(int) $row->order_count,
|
|
(int) $row->ticket_item_count,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function auditExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
|
{
|
|
$operatorId = isset($filterJson['operator_id']) ? (int) $filterJson['operator_id'] : null;
|
|
$rows = [
|
|
['ID', '操作者类型', '操作者ID', '模块', '操作', 'IP', '时间'],
|
|
];
|
|
|
|
$q = AuditLog::query()->orderByDesc('id');
|
|
if ($operatorId !== null && $operatorId > 0) {
|
|
$q->where('operator_id', $operatorId);
|
|
}
|
|
$q->whereDate('created_at', '>=', $dateFrom)
|
|
->whereDate('created_at', '<=', $dateTo);
|
|
|
|
foreach ($q->limit(5000)->get() as $log) {
|
|
$rows[] = [
|
|
(int) $log->id,
|
|
(string) $log->operator_type,
|
|
(int) $log->operator_id,
|
|
(string) $log->module_code,
|
|
(string) $log->action_code,
|
|
(string) ($log->ip ?? ''),
|
|
$log->created_at?->toIso8601String() ?? '',
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/** @return \Illuminate\Database\Query\Builder */
|
|
private function playerWinLossBaseQuery(?int $playerId, string $dateFrom, string $dateTo)
|
|
{
|
|
$query = DB::table('ticket_items as ti')
|
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
|
->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
|
|
->selectRaw('ti.player_id')
|
|
->selectRaw('p.username as username')
|
|
->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
|
|
->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
|
->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as net_win_loss_minor')
|
|
->whereDate('o.created_at', '>=', $dateFrom)
|
|
->whereDate('o.created_at', '<=', $dateTo)
|
|
->groupBy('ti.player_id', 'p.username')
|
|
->orderByDesc('net_win_loss_minor');
|
|
|
|
if ($playerId !== null && $playerId > 0) {
|
|
$query->where('ti.player_id', $playerId);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/** @return \Illuminate\Database\Query\Builder */
|
|
private function playDimensionBaseQuery(?string $playCode, string $dateFrom, string $dateTo)
|
|
{
|
|
$query = DB::table('ticket_items as ti')
|
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
|
->selectRaw('ti.play_code')
|
|
->selectRaw('ti.dimension')
|
|
->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
|
|
->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
|
->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as approx_house_gross_minor')
|
|
->whereDate('o.created_at', '>=', $dateFrom)
|
|
->whereDate('o.created_at', '<=', $dateTo)
|
|
->groupBy('ti.play_code', 'ti.dimension')
|
|
->orderBy('ti.play_code')
|
|
->orderBy('ti.dimension');
|
|
|
|
if ($playCode !== null && $playCode !== '') {
|
|
$query->where('ti.play_code', $playCode);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/** @return \Illuminate\Database\Query\Builder */
|
|
private function rebateCommissionBaseQuery(?string $playCode, string $dateFrom, string $dateTo)
|
|
{
|
|
$query = DB::table('ticket_items as ti')
|
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
|
->selectRaw('ti.play_code')
|
|
->selectRaw('SUM(ti.total_bet_amount - ti.actual_deduct_amount) as total_rebate_minor')
|
|
->selectRaw('COUNT(DISTINCT o.id) as order_count')
|
|
->selectRaw('COUNT(ti.id) as ticket_item_count')
|
|
->whereDate('o.created_at', '>=', $dateFrom)
|
|
->whereDate('o.created_at', '<=', $dateTo)
|
|
->groupBy('ti.play_code')
|
|
->orderBy('ti.play_code');
|
|
|
|
if ($playCode !== null && $playCode !== '') {
|
|
$query->where('ti.play_code', $playCode);
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function drawProfitExportRows(?array $filterJson): array
|
|
{
|
|
$draw = $this->resolveDrawForReport($filterJson);
|
|
if ($draw === null) {
|
|
return [['提示', '请提供 draw_id 或 draw_no']];
|
|
}
|
|
|
|
$drawId = (int) $draw->id;
|
|
$totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct');
|
|
$orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count();
|
|
$itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count();
|
|
$currencyCode = (string) (TicketOrder::query()->where('draw_id', $drawId)->value('currency_code') ?? '');
|
|
$totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount');
|
|
$totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount');
|
|
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
|
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
|
|
|
$rows = [
|
|
[
|
|
'行类型', '期号', '状态', '币种', '订单数', '注单数', '下注', '派彩', '平台盈亏',
|
|
'结算批次ID', '结算状态', '批次数', '中奖数', '批次派彩', '批次Jackpot', '完成时间',
|
|
],
|
|
[
|
|
'summary',
|
|
$draw->draw_no,
|
|
$draw->status,
|
|
$currencyCode !== '' ? $currencyCode : null,
|
|
$orderCount,
|
|
$itemCount,
|
|
$totalBetMinor,
|
|
$totalPayoutMinor,
|
|
$approxHouseGrossMinor,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
],
|
|
];
|
|
|
|
$batches = SettlementBatch::query()
|
|
->where('draw_id', $drawId)
|
|
->orderByDesc('id')
|
|
->limit(100)
|
|
->get();
|
|
|
|
foreach ($batches as $batch) {
|
|
$rows[] = [
|
|
'settlement_batch',
|
|
$draw->draw_no,
|
|
$draw->status,
|
|
$currencyCode !== '' ? $currencyCode : null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
(int) $batch->id,
|
|
$batch->status,
|
|
(int) $batch->total_ticket_count,
|
|
(int) $batch->total_win_count,
|
|
(int) $batch->total_payout_amount,
|
|
(int) $batch->total_jackpot_payout_amount,
|
|
$batch->finished_at?->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function soldOutNumberExportRows(?array $filterJson): array
|
|
{
|
|
$draw = $this->resolveDrawForReport($filterJson);
|
|
if ($draw === null) {
|
|
return [['提示', '请提供 draw_id 或 draw_no']];
|
|
}
|
|
|
|
$rows = [
|
|
['期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%'],
|
|
];
|
|
|
|
$pools = RiskPool::query()
|
|
->where('draw_id', $draw->id)
|
|
->where('sold_out_status', 1)
|
|
->orderBy('normalized_number')
|
|
->limit(10000)
|
|
->get();
|
|
|
|
foreach ($pools as $pool) {
|
|
$cap = (int) $pool->total_cap_amount;
|
|
$locked = (int) $pool->locked_amount;
|
|
$rows[] = [
|
|
$draw->draw_no,
|
|
$pool->normalized_number,
|
|
$cap,
|
|
$locked,
|
|
(int) $pool->remaining_amount,
|
|
'是',
|
|
$this->riskUsageRatioPercent($cap, $locked),
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function hotNumberRiskExportRows(?array $filterJson): array
|
|
{
|
|
$draw = $this->resolveDrawForReport($filterJson);
|
|
if ($draw === null) {
|
|
return [['提示', '请提供 draw_id 或 draw_no']];
|
|
}
|
|
|
|
$number = $this->normalizedNumberFromFilters($filterJson);
|
|
$rows = [
|
|
[
|
|
'行类型', '期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%',
|
|
'日志ID', '动作', '金额', '玩法', '注单号', '玩家ID', '原因', '时间',
|
|
],
|
|
];
|
|
|
|
if ($number !== null) {
|
|
$pool = RiskPool::query()
|
|
->where('draw_id', $draw->id)
|
|
->where('normalized_number', $number)
|
|
->first();
|
|
|
|
if ($pool === null) {
|
|
$rows[] = ['pool', $draw->draw_no, $number, null, null, null, null, null, null, null, null, null, null, null, null, null];
|
|
|
|
return $rows;
|
|
}
|
|
|
|
$cap = (int) $pool->total_cap_amount;
|
|
$locked = (int) $pool->locked_amount;
|
|
$rows[] = [
|
|
'pool',
|
|
$draw->draw_no,
|
|
$pool->normalized_number,
|
|
$cap,
|
|
$locked,
|
|
(int) $pool->remaining_amount,
|
|
(int) $pool->sold_out_status === 1 ? '是' : '否',
|
|
$this->riskUsageRatioPercent($cap, $locked),
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
];
|
|
|
|
$logs = RiskPoolLockLog::query()
|
|
->where('draw_id', $draw->id)
|
|
->where('normalized_number', $number)
|
|
->with(['ticketItem:id,ticket_no,play_code,player_id'])
|
|
->orderByDesc('id')
|
|
->limit(5000)
|
|
->get();
|
|
|
|
foreach ($logs as $log) {
|
|
$rows[] = [
|
|
'lock_log',
|
|
$draw->draw_no,
|
|
$number,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
(int) $log->id,
|
|
$log->action_type,
|
|
(int) $log->amount,
|
|
$log->ticketItem?->play_code,
|
|
$log->ticketItem?->ticket_no,
|
|
$log->ticketItem?->player_id,
|
|
$log->source_reason,
|
|
$log->created_at?->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
$pools = RiskPool::query()
|
|
->where('draw_id', $draw->id)
|
|
->orderByDesc('locked_amount')
|
|
->limit(10000)
|
|
->get();
|
|
|
|
foreach ($pools as $pool) {
|
|
$cap = (int) $pool->total_cap_amount;
|
|
$locked = (int) $pool->locked_amount;
|
|
$rows[] = [
|
|
'pool',
|
|
$draw->draw_no,
|
|
$pool->normalized_number,
|
|
$cap,
|
|
$locked,
|
|
(int) $pool->remaining_amount,
|
|
(int) $pool->sold_out_status === 1 ? '是' : '否',
|
|
$this->riskUsageRatioPercent($cap, $locked),
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
null,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
|
{
|
|
$rows = [
|
|
['转账单号', '玩家ID', '用户名', '昵称', '方向', '币种', '金额', '状态', '外部单号', '失败原因', '创建时间', '完成时间'],
|
|
];
|
|
|
|
$query = TransferOrder::query()
|
|
->with(['player:id,username,nickname'])
|
|
->orderByDesc('id');
|
|
|
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
|
if ($playerId !== null && $playerId > 0) {
|
|
$query->where('player_id', $playerId);
|
|
}
|
|
|
|
$query->where('created_at', '>=', $dateFrom.' 00:00:00')
|
|
->where('created_at', '<=', $dateTo.' 23:59:59');
|
|
|
|
foreach ($query->limit(10000)->get() as $order) {
|
|
$player = $order->player;
|
|
$rows[] = [
|
|
$order->transfer_no,
|
|
(int) $order->player_id,
|
|
$player?->username,
|
|
$player?->nickname,
|
|
$order->direction,
|
|
$order->currency_code,
|
|
(int) $order->amount,
|
|
$order->status,
|
|
$order->external_ref_no,
|
|
$order->fail_reason,
|
|
$order->created_at?->toIso8601String(),
|
|
$order->finished_at?->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<int, string|int|float|null>>
|
|
*/
|
|
private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
|
{
|
|
$rows = [
|
|
['流水号', '玩家ID', '用户名', '业务类型', '业务单号', '方向', '金额', '变动前余额', '变动后余额', '状态', '外部单号', '备注', '创建时间'],
|
|
];
|
|
|
|
$query = WalletTxn::query()
|
|
->with(['player:id,username'])
|
|
->orderByDesc('id');
|
|
|
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
|
if ($playerId !== null && $playerId > 0) {
|
|
$query->where('player_id', $playerId);
|
|
}
|
|
|
|
$query->where('created_at', '>=', $dateFrom.' 00:00:00')
|
|
->where('created_at', '<=', $dateTo.' 23:59:59');
|
|
|
|
foreach ($query->limit(10000)->get() as $txn) {
|
|
$rows[] = [
|
|
$txn->txn_no,
|
|
(int) $txn->player_id,
|
|
$txn->player?->username,
|
|
$txn->biz_type,
|
|
$txn->biz_no,
|
|
(int) $txn->direction,
|
|
(int) $txn->amount,
|
|
(int) $txn->balance_before,
|
|
(int) $txn->balance_after,
|
|
$txn->status,
|
|
$txn->external_ref_no,
|
|
$txn->remark,
|
|
$txn->created_at?->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
private function resolveDrawForReport(?array $filterJson): ?Draw
|
|
{
|
|
if (! is_array($filterJson)) {
|
|
return null;
|
|
}
|
|
|
|
if (! empty($filterJson['draw_id'])) {
|
|
return Draw::query()->find((int) $filterJson['draw_id']);
|
|
}
|
|
|
|
$drawNo = trim((string) ($filterJson['draw_no'] ?? ''));
|
|
if ($drawNo !== '') {
|
|
return Draw::query()->where('draw_no', $drawNo)->first();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function normalizedNumberFromFilters(?array $filterJson): ?string
|
|
{
|
|
if (! is_array($filterJson)) {
|
|
return null;
|
|
}
|
|
|
|
$number = trim((string) ($filterJson['normalized_number'] ?? ''));
|
|
if (preg_match('/^[0-9]{4}$/', $number) === 1) {
|
|
return $number;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function riskUsageRatioPercent(int $cap, int $locked): ?string
|
|
{
|
|
if ($cap <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return (string) round($locked / $cap * 100, 2);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|