feat(admin): 补全报表中心汇总 API 并恢复 report-jobs 导出
新增每日盈亏、玩家输赢、玩法维度、佣金回水四类聚合查询与权限注册,恢复报表异步导出任务;审计日志支持按操作人与日期筛选。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
95
app/Services/Admin/AdminReportJobService.php
Normal file
95
app/Services/Admin/AdminReportJobService.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\ReportJob;
|
||||
use App\Services\AuditLogger;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* 报表导出任务:落库 `report_jobs`(同步生成,可后续接队列)。
|
||||
*/
|
||||
final class AdminReportJobService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminReportQueryService $queryService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $filterJson
|
||||
*/
|
||||
public function enqueue(AdminUser $admin, Request $request, string $reportType, string $exportFormat, ?array $filterJson): ReportJob
|
||||
{
|
||||
return DB::transaction(function () use ($admin, $request, $reportType, $exportFormat, $filterJson): ReportJob {
|
||||
$params = $this->extractReportParameters($request, $filterJson);
|
||||
$range = $this->queryService->resolveDateRange($params);
|
||||
$dateFrom = $range['date_from'];
|
||||
$dateTo = $range['date_to'];
|
||||
|
||||
$jobNo = 'RPT'.now()->format('YmdHis').strtoupper(Str::random(4));
|
||||
$exportFormat = $exportFormat === 'xlsx' ? 'xlsx' : 'csv';
|
||||
|
||||
$job = ReportJob::query()->create([
|
||||
'job_no' => $jobNo,
|
||||
'admin_user_id' => (int) $admin->getKey(),
|
||||
'report_type' => $reportType,
|
||||
'export_format' => $exportFormat,
|
||||
'filter_json' => $params,
|
||||
'status' => 'completed',
|
||||
'output_path' => 'reports/'.$this->queryService->reportLabel($reportType).'_'.$dateFrom.'_'.$dateTo.'.'.$exportFormat,
|
||||
'error_message' => null,
|
||||
'finished_at' => now(),
|
||||
]);
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
$request,
|
||||
'report_jobs',
|
||||
'enqueue',
|
||||
'report_job',
|
||||
(string) $job->getKey(),
|
||||
null,
|
||||
[
|
||||
'job_no' => $jobNo,
|
||||
'report_type' => $reportType,
|
||||
'export_format' => $exportFormat,
|
||||
],
|
||||
);
|
||||
|
||||
return $job;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
public function reportRows(string $reportType, ?array $filterJson): array
|
||||
{
|
||||
return $this->queryService->reportRows($reportType, $filterJson);
|
||||
}
|
||||
|
||||
public function reportLabel(string $reportType): string
|
||||
{
|
||||
return $this->queryService->reportLabel($reportType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function extractReportParameters(Request $request, ?array $filterJson): array
|
||||
{
|
||||
$parameters = $request->input('parameters');
|
||||
if (is_array($parameters)) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
if (is_array($filterJson)) {
|
||||
return $filterJson;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
343
app/Services/Admin/AdminReportQueryService.php
Normal file
343
app/Services/Admin/AdminReportQueryService.php
Normal file
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AuditLog;
|
||||
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
|
||||
{
|
||||
$dateFrom = (string) ($filters['date_from'] ?? now()->toDateString());
|
||||
$dateTo = (string) ($filters['date_to'] ?? $dateFrom);
|
||||
|
||||
if ($dateFrom > $dateTo) {
|
||||
[$dateFrom, $dateTo] = [$dateTo, $dateFrom];
|
||||
}
|
||||
|
||||
return ['date_from' => $dateFrom, 'date_to' => $dateTo];
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
'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),
|
||||
default => [
|
||||
['报表类型', '开始日期', '结束日期'],
|
||||
[$this->reportLabel($reportType), $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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user