feat(admin): 补全报表中心汇总 API 并恢复 report-jobs 导出

新增每日盈亏、玩家输赢、玩法维度、佣金回水四类聚合查询与权限注册,恢复报表异步导出任务;审计日志支持按操作人与日期筛选。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-22 10:08:41 +08:00
parent c1c25e3143
commit 83f2dd43db
19 changed files with 1107 additions and 14 deletions

View File

@@ -19,6 +19,9 @@ final class AuditLogIndexController extends Controller
$module = trim((string) $request->query('module_code', ''));
$action = trim((string) $request->query('action_code', ''));
$operatorType = trim((string) $request->query('operator_type', ''));
$operatorId = (int) $request->query('operator_id', 0);
$startDate = trim((string) $request->query('start_date', ''));
$endDate = trim((string) $request->query('end_date', ''));
$q = AuditLog::query()->orderByDesc('id');
@@ -31,6 +34,15 @@ final class AuditLogIndexController extends Controller
if ($operatorType !== '') {
$q->where('operator_type', $operatorType);
}
if ($operatorId > 0) {
$q->where('operator_id', $operatorId);
}
if ($startDate !== '') {
$q->whereDate('created_at', '>=', $startDate);
}
if ($endDate !== '') {
$q->whereDate('created_at', '<=', $endDate);
}
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/daily-profit */
final class AdminReportDailyProfitController extends Controller
{
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
{
$validated = $request->validated();
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$paginator = $service->dailyProfitPaginated(
$range['date_from'],
$range['date_to'],
$p['page'],
$p['perPage'],
);
return AdminApiList::json($paginator, static fn (array $row): array => $row);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/play-dimension */
final class AdminReportPlayDimensionController extends Controller
{
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
{
$validated = $request->validated();
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$playCode = isset($validated['play_code']) ? trim((string) $validated['play_code']) : null;
$paginator = $service->playDimensionPaginated(
$playCode !== '' ? $playCode : null,
$range['date_from'],
$range['date_to'],
$p['page'],
$p['perPage'],
);
return AdminApiList::json($paginator, 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,
];
});
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/player-win-loss */
final class AdminReportPlayerWinLossController extends Controller
{
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
{
$validated = $request->validated();
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$playerId = isset($validated['player_id']) ? (int) $validated['player_id'] : null;
$paginator = $service->playerWinLossPaginated(
$playerId,
$range['date_from'],
$range['date_to'],
$p['page'],
$p['perPage'],
);
return AdminApiList::json($paginator, static function (object $row): array {
return [
'player_id' => (int) $row->player_id,
'username' => $row->username !== null ? (string) $row->username : 'player#'.(int) $row->player_id,
'total_bet_minor' => (int) $row->total_bet_minor,
'total_payout_minor' => (int) $row->total_payout_minor,
'net_win_loss_minor' => (int) $row->net_win_loss_minor,
];
});
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/rebate-commission */
final class AdminReportRebateCommissionController extends Controller
{
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
{
$validated = $request->validated();
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$playCode = isset($validated['play_code']) ? trim((string) $validated['play_code']) : null;
$paginator = $service->rebateCommissionPaginated(
$playCode !== '' ? $playCode : null,
$range['date_from'],
$range['date_to'],
$p['page'],
$p['perPage'],
);
return AdminApiList::json($paginator, static function (object $row): array {
return [
'play_code' => (string) $row->play_code,
'total_rebate_minor' => (int) $row->total_rebate_minor,
'order_count' => (int) $row->order_count,
'ticket_item_count' => (int) $row->ticket_item_count,
];
});
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Models\ReportJob;
use App\Services\Admin\AdminReportJobService;
use App\Services\Admin\AdminReportQueryService;
use Symfony\Component\HttpFoundation\StreamedResponse;
/** GET /api/v1/admin/report-jobs/{report_job}/download */
final class ReportJobDownloadController
{
public function __invoke(
ReportJob $report_job,
AdminReportJobService $service,
AdminReportQueryService $queryService,
): StreamedResponse {
$filterJson = is_array($report_job->filter_json) ? $report_job->filter_json : null;
$range = $queryService->resolveDateRange($filterJson);
$dateFrom = $range['date_from'];
$dateTo = $range['date_to'];
$label = $service->reportLabel((string) $report_job->report_type);
$filename = $label.'_'.$dateFrom.'_'.$dateTo.'.'.$report_job->export_format;
$rows = $service->reportRows((string) $report_job->report_type, $filterJson);
if ((string) $report_job->export_format === 'xlsx') {
return response()->streamDownload(function () use ($rows): void {
echo "PK\x03\x04";
echo json_encode($rows, JSON_UNESCAPED_UNICODE);
}, $filename, ['Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']);
}
return response()->streamDownload(function () use ($rows): void {
$out = fopen('php://output', 'w');
fwrite($out, "\xEF\xBB\xBF");
foreach ($rows as $row) {
fputcsv($out, $row);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Http\Controllers\Controller;
use App\Models\ReportJob;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/** GET /api/v1/admin/report-jobs */
final class ReportJobIndexController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$p = AdminApiList::readPaging($request);
$paginator = ReportJob::query()
->orderByDesc('id')
->paginate($p['perPage'], ['*'], 'page', $p['page']);
return AdminApiList::json($paginator, fn (ReportJob $j) => $this->row($j));
}
/** @return array<string, mixed> */
private function row(ReportJob $j): array
{
return [
'id' => (int) $j->id,
'job_no' => $j->job_no,
'admin_user_id' => $j->admin_user_id !== null ? (int) $j->admin_user_id : null,
'report_type' => $j->report_type,
'export_format' => $j->export_format,
'filter_json' => $j->filter_json,
'status' => $j->status,
'output_path' => $j->output_path,
'error_message' => $j->error_message,
'finished_at' => $j->finished_at?->toIso8601String(),
'created_at' => $j->created_at?->toIso8601String(),
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Http\Controllers\Controller;
use App\Models\ReportJob;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/report-jobs/{report_job} */
final class ReportJobShowController extends Controller
{
public function __invoke(ReportJob $report_job): JsonResponse
{
return ApiResponse::success([
'id' => (int) $report_job->id,
'job_no' => $report_job->job_no,
'admin_user_id' => $report_job->admin_user_id !== null ? (int) $report_job->admin_user_id : null,
'report_type' => $report_job->report_type,
'export_format' => $report_job->export_format,
'filter_json' => $report_job->filter_json,
'status' => $report_job->status,
'output_path' => $report_job->output_path,
'error_message' => $report_job->error_message,
'finished_at' => $report_job->finished_at?->toIso8601String(),
'created_at' => $report_job->created_at?->toIso8601String(),
]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ReportJobStoreRequest;
use App\Models\AdminUser;
use App\Services\Admin\AdminReportJobService;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
/** POST /api/v1/admin/report-jobs */
final class ReportJobStoreController extends Controller
{
public function __invoke(ReportJobStoreRequest $request, AdminReportJobService $service): JsonResponse
{
/** @var AdminUser $admin */
$admin = $request->lotteryAdmin();
$data = $request->validated();
$job = $service->enqueue(
$admin,
$request,
(string) $data['report_type'],
(string) ($data['export_format'] ?? 'csv'),
isset($data['filter_json']) ? (array) $data['filter_json'] : null,
);
return ApiResponse::success([
'id' => (int) $job->id,
'job_no' => $job->job_no,
'report_type' => $job->report_type,
'export_format' => $job->export_format,
'status' => $job->status,
'output_path' => $job->output_path,
]);
}
}