feat(risk): 支持按开注商隔离风险池并新增经营报表
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

This commit is contained in:
2026-07-10 10:23:30 +08:00
parent d4779660c8
commit 2ea3e810a0
31 changed files with 610 additions and 58 deletions

View File

@@ -36,6 +36,7 @@ final class RiskCapItemsReplaceController extends Controller
$data = $request->validate([
'items' => ['required', 'array', 'min:1'],
'items.*.provider_code' => ['sometimes', 'string', 'max:32', 'regex:/^[A-Za-z0-9_-]+$/'],
'items.*.draw_id' => ['sometimes', 'nullable', 'integer', 'exists:draws,id'],
'items.*.normalized_number' => ['required', 'string', 'size:4', 'regex:/^[0-9]{4}$/'],
'items.*.cap_amount' => ['required', 'integer', 'min:1'],

View File

@@ -0,0 +1,41 @@
<?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 App\Support\AdminScopePolicy;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/provider-profit */
final class AdminReportProviderProfitController extends Controller
{
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$validated = $request->validated();
$paging = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$scope = AdminScopePolicy::resolveContext($request, $admin, 'site_code', 'agent_node_id');
$paginator = $service->providerProfitPaginated(
$range['date_from'], $range['date_to'], $paging['page'], $paging['perPage'], $scope,
);
return AdminApiList::jsonWith($paginator, static function (object $row): array {
return [
'provider_code' => (string) $row->provider_code,
'provider_name' => (string) $row->provider_name,
'ticket_item_count' => (int) $row->ticket_item_count,
'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,
];
}, [
'currency_code' => $service->resolvePeriodCurrencyCode($range['date_from'], $range['date_to'], $scope),
]);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\V1\Admin\Risk;
use App\Models\BetProvider;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Models\TicketOrder;
@@ -25,9 +26,13 @@ final class AdminRiskPoolIndexController extends Controller
$highRiskOnly = $request->boolean('high_risk_only');
$activeOnly = $request->boolean('active_only');
$number = trim((string) $request->query('normalized_number', ''));
$providerCode = strtoupper(trim((string) $request->query('provider_code', '')));
$sort = trim((string) $request->query('sort', 'usage_desc'));
$q = RiskPool::query()->where('draw_id', $draw->id);
if ($providerCode !== '') {
$q->where('provider_code', $providerCode);
}
if ($soldOutOnly) {
$q->where('sold_out_status', 1);
@@ -47,11 +52,12 @@ final class AdminRiskPoolIndexController extends Controller
}
match ($sort) {
'locked_desc' => $q->orderByDesc('locked_amount')->orderBy('normalized_number'),
'remaining_asc' => $q->orderBy('remaining_amount')->orderBy('normalized_number'),
'number_asc' => $q->orderBy('normalized_number'),
'locked_desc' => $q->orderByDesc('locked_amount')->orderBy('provider_code')->orderBy('normalized_number'),
'remaining_asc' => $q->orderBy('remaining_amount')->orderBy('provider_code')->orderBy('normalized_number'),
'number_asc' => $q->orderBy('provider_code')->orderBy('normalized_number'),
default => $q->orderByRaw('(locked_amount * 1.0 / NULLIF(total_cap_amount, 0)) DESC')
->orderByDesc('locked_amount')
->orderBy('provider_code')
->orderBy('normalized_number'),
};
@@ -76,6 +82,7 @@ final class AdminRiskPoolIndexController extends Controller
$locked = (int) $pool->locked_amount;
return [
'provider_code' => $pool->provider_code ?? BetProvider::DEFAULT_CODE,
'normalized_number' => $pool->normalized_number,
'total_cap_amount' => $cap,
'locked_amount' => $locked,

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\V1\Admin\Risk;
use App\Models\BetProvider;
use App\Models\Draw;
use App\Models\TicketOrder;
use Illuminate\Http\Request;
@@ -58,6 +59,7 @@ final class AdminRiskPoolLockLogIndexController extends Controller
{
return [
'id' => (int) $log->id,
'provider_code' => $log->provider_code ?? BetProvider::DEFAULT_CODE,
'normalized_number' => $log->normalized_number,
'action_type' => $log->action_type,
'amount' => (int) $log->amount,
@@ -76,6 +78,7 @@ final class AdminRiskPoolLockLogIndexController extends Controller
$lastAt = $row->last_at ?? null;
return [
'provider_code' => (string) ($row->provider_code ?? BetProvider::DEFAULT_CODE),
'ticket_item_id' => (int) $row->ticket_item_id,
'ticket_no' => (string) $row->ticket_no,
'play_code' => (string) $row->play_code,

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\V1\Admin\Risk;
use App\Models\BetProvider;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Lottery\ErrorCode;
@@ -21,7 +22,8 @@ final class AdminRiskPoolManualStatusController extends Controller
) {}
public function close(Request $request, Draw $draw, string $number_4d): JsonResponse
{
$pool = $this->updateStatus($draw, $number_4d, true, 'close', 'admin_manual_close');
$providerCode = strtoupper(trim((string) $request->query('provider_code', BetProvider::DEFAULT_CODE)));
$pool = $this->updateStatus($draw, $providerCode, $number_4d, true, 'close', 'admin_manual_close');
if ($pool === null) {
return ApiMessage::errorResponse($request, 'not_found', ErrorCode::ClientHttpError->value, null, 404);
@@ -32,7 +34,8 @@ final class AdminRiskPoolManualStatusController extends Controller
public function recover(Request $request, Draw $draw, string $number_4d): JsonResponse
{
$pool = $this->updateStatus($draw, $number_4d, false, 'recover', 'admin_manual_recover');
$providerCode = strtoupper(trim((string) $request->query('provider_code', BetProvider::DEFAULT_CODE)));
$pool = $this->updateStatus($draw, $providerCode, $number_4d, false, 'recover', 'admin_manual_recover');
if ($pool === null) {
return ApiMessage::errorResponse($request, 'not_found', ErrorCode::ClientHttpError->value, null, 404);
@@ -48,14 +51,16 @@ final class AdminRiskPoolManualStatusController extends Controller
private function updateStatus(
Draw $draw,
string $providerCode,
string $number4d,
bool $soldOut,
string $actionType,
string $reason,
): ?RiskPool {
return DB::transaction(function () use ($draw, $number4d, $soldOut, $actionType, $reason): ?RiskPool {
return DB::transaction(function () use ($draw, $providerCode, $number4d, $soldOut, $actionType, $reason): ?RiskPool {
$pool = RiskPool::query()
->where('draw_id', $draw->id)
->where('provider_code', $providerCode)
->where('normalized_number', $number4d)
->lockForUpdate()
->first();
@@ -76,11 +81,12 @@ final class AdminRiskPoolManualStatusController extends Controller
])->save();
if ($targetStatus === 1) {
$this->riskPoolService->publishManualSoldOut($draw, $number4d);
$this->riskPoolService->publishManualSoldOut($draw, $number4d, $providerCode);
}
RiskPoolLockLog::query()->create([
'draw_id' => $draw->id,
'provider_code' => $providerCode,
'normalized_number' => $number4d,
'ticket_item_id' => null,
'action_type' => $actionType,
@@ -106,6 +112,7 @@ final class AdminRiskPoolManualStatusController extends Controller
$locked = (int) $pool->locked_amount;
return [
'provider_code' => $pool->provider_code ?? BetProvider::DEFAULT_CODE,
'normalized_number' => $pool->normalized_number,
'total_cap_amount' => $cap,
'locked_amount' => $locked,

View File

@@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\V1\Admin\Risk;
use App\Models\BetProvider;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Lottery\ErrorCode;
@@ -30,8 +31,11 @@ final class AdminRiskPoolShowController extends Controller
);
}
$providerCode = strtoupper(trim((string) $request->query('provider_code', BetProvider::DEFAULT_CODE)));
$pool = RiskPool::query()
->where('draw_id', $draw->id)
->where('provider_code', $providerCode)
->where('normalized_number', $number_4d)
->first();
@@ -49,6 +53,7 @@ final class AdminRiskPoolShowController extends Controller
/** @var LengthAwarePaginator $paginator */
$paginator = RiskPoolLockLog::query()
->where('draw_id', $draw->id)
->where('provider_code', $providerCode)
->where('normalized_number', $number_4d)
->with(['ticketItem:id,ticket_no,play_code,player_id'])
->orderByDesc('created_at')
@@ -66,6 +71,7 @@ final class AdminRiskPoolShowController extends Controller
'draw_no' => $draw->draw_no,
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
'pool' => [
'provider_code' => $pool->provider_code ?? BetProvider::DEFAULT_CODE,
'normalized_number' => $pool->normalized_number,
'total_cap_amount' => $cap,
'locked_amount' => $locked,
@@ -84,6 +90,7 @@ final class AdminRiskPoolShowController extends Controller
{
return [
'id' => (int) $log->id,
'provider_code' => $log->provider_code ?? BetProvider::DEFAULT_CODE,
'action_type' => $log->action_type,
'amount' => (int) $log->amount,
'source_reason' => $log->source_reason,

View File

@@ -47,6 +47,7 @@ final class ReportJobStoreRequest extends ApiFormRequest
'wallet_transfer_report',
'hot_number_risk_report',
'play_dimension_report',
'provider_profit_report',
'sold_out_number_report',
'audit_operation_report',
'wallet_txns_daily',