feat: 添加 Laravel Reverb 支持,更新 .env.example 文件以配置 WebSocket,增强彩票调度功能,更新 API 路由以支持期号管理与结果发布

This commit is contained in:
2026-05-09 17:40:49 +08:00
parent 781cf10928
commit aeaf124096
42 changed files with 3886 additions and 5 deletions

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Draw;
use App\Http\Controllers\Controller;
use App\Models\Draw;
use App\Support\ApiResponse;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* GET /api/v1/admin/draws 期号列表。
*/
final class AdminDrawIndexController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$perPage = min(max((int) $request->integer('per_page', 25), 1), 100);
$drawNo = trim((string) $request->query('draw_no', ''));
$status = trim((string) $request->query('status', ''));
$q = Draw::query()->orderByDesc('draw_time')->orderByDesc('id');
if ($drawNo !== '') {
$q->where('draw_no', 'like', '%'.$drawNo.'%');
}
if ($status !== '') {
$q->where('status', $status);
}
/** @var \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator */
$paginator = $q->paginate($perPage);
return ApiResponse::success([
'items' => collect($paginator->items())->map(fn (Draw $row) => $this->row($row))->all(),
'meta' => [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'last_page' => $paginator->lastPage(),
],
]);
}
/** @return array<string, mixed> */
private function row(Draw $draw): array
{
return [
'id' => (int) $draw->id,
'draw_no' => $draw->draw_no,
'business_date' => $draw->business_date instanceof Carbon
? $draw->business_date->format('Y-m-d')
: (string) $draw->business_date,
'sequence_no' => (int) $draw->sequence_no,
'status' => $draw->status,
'start_time' => $draw->start_time?->toIso8601String(),
'close_time' => $draw->close_time?->toIso8601String(),
'draw_time' => $draw->draw_time?->toIso8601String(),
'cooling_end_time' => $draw->cooling_end_time?->toIso8601String(),
'result_source' => $draw->result_source,
'current_result_version' => (int) $draw->current_result_version,
'settle_version' => (int) $draw->settle_version,
'is_reopened' => (bool) $draw->is_reopened,
'updated_at' => $draw->updated_at?->toIso8601String(),
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Draw;
use App\Http\Controllers\Controller;
use App\Models\Draw;
use App\Models\DrawResultBatch;
use App\Models\DrawResultItem;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
/**
* GET /api/v1/admin/draws/{draw}/result-batches 开奖批次与号码(审核/结果核对)。
*/
final class AdminDrawResultBatchesIndexController extends Controller
{
public function __invoke(Draw $draw): JsonResponse
{
$batches = $draw->resultBatches()
->with(['items' => function ($q): void {
$q->orderBy('prize_type')->orderBy('prize_index');
}])
->orderByDesc('result_version')
->get();
return ApiResponse::success([
'draw_id' => (int) $draw->id,
'draw_no' => $draw->draw_no,
'draw_status' => $draw->status,
'batches' => $batches->map(fn (DrawResultBatch $b) => $this->serializeBatch($b))->all(),
]);
}
/** @return array<string, mixed> */
private function serializeBatch(DrawResultBatch $batch): array
{
return [
'id' => (int) $batch->id,
'result_version' => (int) $batch->result_version,
'source_type' => $batch->source_type,
'rng_seed_hash' => $batch->rng_seed_hash,
'status' => $batch->status,
'created_by' => $batch->created_by,
'confirmed_by' => $batch->confirmed_by,
'confirmed_at' => $batch->confirmed_at?->toIso8601String(),
'created_at' => $batch->created_at?->toIso8601String(),
'updated_at' => $batch->updated_at?->toIso8601String(),
'items' => $batch->items->map(fn (DrawResultItem $item) => [
'prize_type' => $item->prize_type,
'prize_index' => (int) $item->prize_index,
'number_4d' => $item->number_4d,
'suffix_3d' => $item->suffix_3d,
'suffix_2d' => $item->suffix_2d,
'head_digit' => $item->head_digit,
'tail_digit' => $item->tail_digit,
])->values()->all(),
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Draw;
use App\Http\Controllers\Controller;
use App\Lottery\DrawResultBatchStatus;
use App\Models\Draw;
use App\Services\Draw\DrawHallSnapshotBuilder;
use App\Support\ApiResponse;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
/**
* GET /api/v1/admin/draws/{draw} 当期状态明细(后台查看 DB 为准 + 大厅展示态预览)。
*/
final class AdminDrawShowController extends Controller
{
public function __construct(
private readonly DrawHallSnapshotBuilder $hallPreview,
) {}
public function __invoke(Draw $draw): JsonResponse
{
$nowUtc = now()->utc();
$batchCounts = [
'total' => $draw->resultBatches()->count(),
'pending_review' => $draw->resultBatches()
->where('status', DrawResultBatchStatus::PendingReview->value)
->count(),
'published' => $draw->resultBatches()
->where('status', DrawResultBatchStatus::Published->value)
->count(),
];
return ApiResponse::success([
'id' => (int) $draw->id,
'draw_no' => $draw->draw_no,
'business_date' => $draw->business_date instanceof Carbon
? $draw->business_date->format('Y-m-d')
: (string) $draw->business_date,
'sequence_no' => (int) $draw->sequence_no,
/** 数据库当期状态(权威) */
'status' => $draw->status,
/** 与玩家大厅 snapshot 对齐的展示态(未跑 tick 时可能与 status 不一致) */
'hall_preview_status' => $this->hallPreview->effectiveHallDisplayStatus($draw, $nowUtc),
'start_time' => $draw->start_time?->toIso8601String(),
'close_time' => $draw->close_time?->toIso8601String(),
'draw_time' => $draw->draw_time?->toIso8601String(),
'cooling_end_time' => $draw->cooling_end_time?->toIso8601String(),
'result_source' => $draw->result_source,
'current_result_version' => (int) $draw->current_result_version,
'settle_version' => (int) $draw->settle_version,
'is_reopened' => (bool) $draw->is_reopened,
'created_at' => $draw->created_at?->toIso8601String(),
'updated_at' => $draw->updated_at?->toIso8601String(),
'result_batch_counts' => $batchCounts,
]);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Draw;
use App\Http\Controllers\Controller;
use App\Lottery\ErrorCode;
use App\Models\AdminUser;
use App\Models\Draw;
use App\Models\DrawResultBatch;
use App\Services\Draw\DrawPublishService;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* POST /api/v1/admin/draws/{draw}/result-batches/{batch}/publish 人工审核发布 RNG 批次。
*/
class DrawResultBatchPublishController extends Controller
{
public function __construct(
private readonly DrawPublishService $publishService,
) {}
public function __invoke(Request $request, Draw $draw, DrawResultBatch $batch): JsonResponse
{
$admin = $request->user();
if (! $admin instanceof AdminUser) {
return ApiResponse::error(
trans('admin.unauthenticated', [], $request->lotteryLocale()),
ErrorCode::AdminUnauthenticated->value,
null,
401,
);
}
if ((int) $batch->draw_id !== (int) $draw->id) {
return ApiResponse::error(
trans('api.not_found', [], $request->lotteryLocale()),
ErrorCode::NotFound->value,
null,
404,
);
}
try {
$this->publishService->publishManualBatch($batch, $admin);
} catch (\RuntimeException) {
return ApiResponse::error(
trans('api.client_error', [], $request->lotteryLocale()),
ErrorCode::ClientHttpError->value,
null,
409,
);
}
$draw->refresh();
return ApiResponse::success([
'draw_no' => $draw->draw_no,
'status' => $draw->status,
'result_version' => (int) $draw->current_result_version,
]);
}
}