feat: 增强环境配置与开发服务,支持局域网访问及币种管理

This commit is contained in:
2026-05-21 16:24:41 +08:00
parent 699d43fbd4
commit 7a6048de10
60 changed files with 1321 additions and 443 deletions

View File

@@ -2,14 +2,15 @@
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use App\Support\AdminAuthorizationRegistry;
final class SyncAdminAuthorizationCommand extends Command
{
protected $signature = 'lottery:admin-auth-sync';
protected $signature = 'lottery:admin-auth-sync
{--audit : 同步完成后立即执行后台权限体检}';
protected $description = '根据后台统一注册表同步 admin_api_resources / bindings / role_api_resources';
@@ -55,6 +56,7 @@ final class SyncAdminAuthorizationCommand extends Command
$menuActionId = $menuActionIds[$permissionCode] ?? null;
if ($menuActionId === null) {
$this->warn(sprintf('跳过未找到的 permission_code: %s', $permissionCode));
continue;
}
@@ -88,6 +90,10 @@ final class SyncAdminAuthorizationCommand extends Command
$roleResourceRows->count(),
));
if ((bool) $this->option('audit')) {
return $this->call('lottery:admin-auth-audit');
}
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Currency;
use App\Models\Currency;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
final class AdminCurrencyDestroyController extends Controller
{
public function __invoke(Currency $currency): JsonResponse
{
$code = strtoupper((string) $currency->code);
if ($code === strtoupper((string) config('lottery.default_currency', 'NPR'))) {
return ApiResponse::error(
'默认币种不可删除',
ErrorCode::ValidationFailed->value,
null,
422,
);
}
$references = $this->referenceSummary($code);
if ($references !== []) {
return ApiResponse::error(
'该币种已被业务数据引用,暂不可删除:'.implode('、', $references),
ErrorCode::ValidationFailed->value,
['references' => $references],
422,
);
}
$id = (int) $currency->id;
$currency->delete();
return ApiResponse::success([
'deleted' => true,
'id' => $id,
'code' => $code,
]);
}
/** @return list<string> */
private function referenceSummary(string $code): array
{
$checks = [
'玩家默认币种' => DB::table('players')->where('default_currency', $code),
'玩家钱包' => DB::table('player_wallets')->where('currency_code', $code),
'转账单' => DB::table('transfer_orders')->where('currency_code', $code),
'注单' => DB::table('ticket_orders')->where('currency_code', $code),
'赔率配置' => DB::table('odds_items')->where('currency_code', $code),
'奖池' => DB::table('jackpot_pools')->where('currency_code', $code),
'Jackpot 贡献记录' => DB::table('jackpot_contributions')->where('currency_code', $code),
];
$references = [];
foreach ($checks as $label => $query) {
if ($query->exists()) {
$references[] = $label;
}
}
return $references;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Currency;
use App\Models\Currency;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
final class AdminCurrencyIndexController extends Controller
{
public function __invoke(): JsonResponse
{
$items = Currency::query()
->orderByDesc('is_enabled')
->orderByDesc('is_bettable')
->orderBy('code')
->get()
->map(fn (Currency $currency): array => $this->serializeCurrency($currency))
->all();
return ApiResponse::success(['items' => $items]);
}
/** @return array<string, mixed> */
private function serializeCurrency(Currency $currency): array
{
return [
'id' => (int) $currency->id,
'code' => $currency->code,
'name' => $currency->name,
'decimal_places' => (int) $currency->decimal_places,
'is_enabled' => (bool) $currency->is_enabled,
'is_bettable' => (bool) $currency->is_bettable,
'created_at' => $currency->created_at?->toIso8601String(),
'updated_at' => $currency->updated_at?->toIso8601String(),
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Currency;
use App\Models\Currency;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Services\Currency\CurrencyActivationService;
use App\Http\Requests\Admin\AdminCurrencyStoreRequest;
final class AdminCurrencyStoreController extends Controller
{
public function __invoke(
AdminCurrencyStoreRequest $request,
CurrencyActivationService $activationService,
): JsonResponse {
$data = $request->validated();
$enabled = (bool) ($data['is_enabled'] ?? true);
$bettable = $enabled && (bool) ($data['is_bettable'] ?? false);
$currency = DB::transaction(function () use ($data, $enabled, $bettable, $activationService): Currency {
$currency = Currency::query()->create([
'code' => $data['code'],
'name' => $data['name'],
'decimal_places' => (int) ($data['decimal_places'] ?? 2),
'is_enabled' => $enabled,
'is_bettable' => $bettable,
]);
$activationService->ensureBettableCurrencyReady($currency);
return $currency->fresh();
});
return ApiResponse::success($this->serializeCurrency($currency))->setStatusCode(201);
}
/** @return array<string, mixed> */
private function serializeCurrency(Currency $currency): array
{
return [
'id' => (int) $currency->id,
'code' => $currency->code,
'name' => $currency->name,
'decimal_places' => (int) $currency->decimal_places,
'is_enabled' => (bool) $currency->is_enabled,
'is_bettable' => (bool) $currency->is_bettable,
'created_at' => $currency->created_at?->toIso8601String(),
'updated_at' => $currency->updated_at?->toIso8601String(),
];
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Currency;
use App\Models\Currency;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Services\Currency\CurrencyActivationService;
use App\Http\Requests\Admin\AdminCurrencyUpdateRequest;
final class AdminCurrencyUpdateController extends Controller
{
public function __invoke(
AdminCurrencyUpdateRequest $request,
Currency $currency,
CurrencyActivationService $activationService,
): JsonResponse {
$data = $request->validated();
if (array_key_exists('is_enabled', $data) && ! (bool) $data['is_enabled']) {
$data['is_bettable'] = false;
} elseif (array_key_exists('is_bettable', $data)) {
$data['is_bettable'] = (bool) $data['is_bettable'];
}
if (array_key_exists('decimal_places', $data)) {
$data['decimal_places'] = (int) $data['decimal_places'];
}
if (array_key_exists('is_enabled', $data)) {
$data['is_enabled'] = (bool) $data['is_enabled'];
}
$currency = DB::transaction(function () use ($currency, $data, $activationService): Currency {
$currency->fill($data);
$currency->save();
$activationService->ensureBettableCurrencyReady($currency);
return $currency->fresh();
});
return ApiResponse::success($this->serializeCurrency($currency));
}
/** @return array<string, mixed> */
private function serializeCurrency(Currency $currency): array
{
return [
'id' => (int) $currency->id,
'code' => $currency->code,
'name' => $currency->name,
'decimal_places' => (int) $currency->decimal_places,
'is_enabled' => (bool) $currency->is_enabled,
'is_bettable' => (bool) $currency->is_bettable,
'created_at' => $currency->created_at?->toIso8601String(),
'updated_at' => $currency->updated_at?->toIso8601String(),
];
}
}

View File

@@ -6,8 +6,8 @@ use App\Models\Player;
use App\Lottery\ErrorCode;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\PlayerApiPresenter;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminPlayerStoreRequest;
/** POST /api/v1/admin/players */
@@ -38,6 +38,6 @@ final class AdminPlayerStoreController extends Controller
'status' => $request->validated('status', 0),
]);
return ApiResponse::success(PlayerApiPresenter::listItem($player), 201);
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
}
}

View File

@@ -22,7 +22,7 @@ final class AdminPlayerTicketItemsIndexController extends Controller
public function __invoke(AdminPlayerTicketItemsRequest $request, Player $player): JsonResponse
{
$perPage = $this->perPage($request, 'per_page', 20, 50);
$perPage = $this->perPage($request, 'per_page', 10, 50);
$page = $this->page($request);
$drawNo = $request->validated('draw_no');
if (is_string($drawNo)) {

View File

@@ -1,36 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Models\ReportJob;
use App\Services\Admin\AdminReportJobService;
use Symfony\Component\HttpFoundation\StreamedResponse;
final class ReportJobDownloadController
{
public function __invoke(ReportJob $report_job, AdminReportJobService $service): StreamedResponse
{
$filterJson = is_array($report_job->filter_json) ? $report_job->filter_json : null;
$dateFrom = (string) ($filterJson['date_from'] ?? now()->toDateString());
$dateTo = (string) ($filterJson['date_to'] ?? $dateFrom);
$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

@@ -1,42 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Models\ReportJob;
use Illuminate\Http\Request;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
/** 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

@@ -1,29 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Models\ReportJob;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
/** 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

@@ -1,39 +0,0 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Reports;
use App\Models\AdminUser;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Services\Admin\AdminReportJobService;
use App\Http\Requests\Admin\ReportJobStoreRequest;
/** 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,
]);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Risk;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Models\TicketOrder;
use Illuminate\Http\Request;
use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
@@ -49,9 +50,14 @@ final class AdminRiskPoolIndexController extends Controller
/** @var LengthAwarePaginator $paginator */
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
$currencyCode = (string) (TicketOrder::query()
->where('draw_id', $draw->id)
->value('currency_code') ?? '');
return AdminApiList::jsonWith($paginator, fn (RiskPool $row) => $this->row($row), [
'draw_id' => (int) $draw->id,
'draw_no' => $draw->draw_no,
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
]);
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Api\V1\Admin\Risk;
use App\Models\Draw;
use App\Models\TicketOrder;
use Illuminate\Http\Request;
use App\Support\AdminApiList;
use App\Models\RiskPoolLockLog;
@@ -38,9 +39,14 @@ final class AdminRiskPoolLockLogIndexController extends Controller
/** @var LengthAwarePaginator $paginator */
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
$currencyCode = (string) (TicketOrder::query()
->where('draw_id', $draw->id)
->value('currency_code') ?? '');
return AdminApiList::jsonWith($paginator, fn (RiskPoolLockLog $log) => $this->row($log), [
'draw_id' => (int) $draw->id,
'draw_no' => $draw->draw_no,
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
]);
}

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Risk;
use App\Models\Draw;
use App\Models\RiskPool;
use App\Lottery\ErrorCode;
use App\Models\TicketOrder;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use App\Support\AdminApiList;
@@ -56,10 +57,14 @@ final class AdminRiskPoolShowController extends Controller
$cap = (int) $pool->total_cap_amount;
$locked = (int) $pool->locked_amount;
$currencyCode = (string) (TicketOrder::query()
->where('draw_id', $draw->id)
->value('currency_code') ?? '');
return ApiResponse::success([
'draw_id' => (int) $draw->id,
'draw_no' => $draw->draw_no,
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
'pool' => [
'normalized_number' => $pool->normalized_number,
'total_cap_amount' => $cap,

View File

@@ -23,6 +23,7 @@ final class AdminSettlementBatchDetailsController extends Controller
->with([
'ticketItem:id,ticket_no,play_code,player_id',
'ticketItem.player:id,username,site_player_id',
'ticketItem.order:id,currency_code',
])
->orderBy('id')
->paginate($p['perPage'], ['*'], 'page', $p['page']);
@@ -31,12 +32,14 @@ final class AdminSettlementBatchDetailsController extends Controller
/** @var TicketSettlementDetail $row */
$item = $row->ticketItem;
$player = $item?->player;
$order = $item?->order;
return [
'id' => (int) $row->id,
'ticket_item_id' => (int) $row->ticket_item_id,
'ticket_no' => $item?->ticket_no,
'play_code' => $item?->play_code,
'currency_code' => $order?->currency_code,
'player_id' => $item?->player_id,
'player_username' => $player?->username,
'site_player_id' => $player?->site_player_id,

View File

@@ -46,6 +46,7 @@ final class AdminSettlementBatchIndexController extends Controller
'id' => (int) $b->id,
'draw_id' => (int) $b->draw_id,
'draw_no' => $b->draw?->draw_no,
'currency_code' => $financial['currency_code'],
'result_batch_id' => (int) $b->result_batch_id,
'settle_version' => (int) $b->settle_version,
'status' => $b->status,

View File

@@ -22,6 +22,7 @@ final class AdminSettlementBatchShowController extends Controller
'id' => (int) $batch->id,
'draw_id' => (int) $batch->draw_id,
'draw_no' => $batch->draw?->draw_no,
'currency_code' => $financial['currency_code'],
'draw_status' => $batch->draw?->status,
'result_batch_id' => (int) $batch->result_batch_id,
'result_batch_version' => $batch->resultBatch?->result_version,

View File

@@ -30,7 +30,7 @@ final class AdminTicketItemIndexController extends Controller
{
$validated = $request->validated();
$perPage = $this->perPage($request, 'per_page', 20, 100);
$perPage = $this->perPage($request, 'per_page', 10, 100);
$page = $this->page($request);
$query = TicketItem::query()

View File

@@ -16,7 +16,7 @@ use App\Http\Requests\Admin\TransferOrderListRequest;
*
* Query
* - `page`(默认 1
* - `per_page` `size`(每页条数,默认 20,最大 100
* - `per_page` `size`(每页条数,默认 10,最大 100
* - `player_id`(可选,按玩家主键)
* - `player_account`(可选,模糊匹配 `players.site_player_id` / `username`;与 `player_id` 同时传时以 `player_id` 为准)
* - `transfer_no`(可选,模糊匹配本地单号)
@@ -35,7 +35,7 @@ final class TransferOrderListController extends Controller
{
$validated = $request->validated();
$perPage = $this->perPage($request, 'per_page', 20, 100);
$perPage = $this->perPage($request, 'per_page', 10, 100);
$page = $this->page($request);
$query = TransferOrder::query()

View File

@@ -35,7 +35,7 @@ final class WalletTransactionListController extends Controller
{
$validated = $request->validated();
$perPage = $this->perPage($request, 'per_page', 20, 100);
$perPage = $this->perPage($request, 'per_page', 10, 100);
$page = $this->page($request);
$query = WalletTxn::query()

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Api\V1\Currency;
use App\Models\Currency;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
final class CurrencyIndexController extends Controller
{
public function __invoke(): JsonResponse
{
$items = Currency::query()
->where('is_enabled', true)
->orderByDesc('is_bettable')
->orderBy('code')
->get()
->map(fn (Currency $currency): array => [
'code' => $currency->code,
'name' => $currency->name,
'decimal_places' => (int) $currency->decimal_places,
'is_bettable' => (bool) $currency->is_bettable,
])
->all();
return ApiResponse::success(['items' => $items]);
}
}

View File

@@ -19,6 +19,11 @@ final class JackpotSummaryController extends Controller
public function __invoke(Request $request): JsonResponse
{
return ApiResponse::success($this->summary->summary((string) $request->query('currency_code', 'NPR')));
$currencyCode = strtoupper(trim((string) $request->query(
'currency_code',
(string) config('lottery.default_currency', 'NPR'),
)));
return ApiResponse::success($this->summary->summary($currencyCode));
}
}

View File

@@ -24,7 +24,7 @@ final class TicketItemsIndexController extends Controller
/** @var Player $player */
$player = $request->attributes->get('lottery_player');
$perPage = $this->perPage($request, 'per_page', 20, 50);
$perPage = $this->perPage($request, 'per_page', 10, 50);
$page = $this->page($request);
$drawNo = $request->query('draw_no');
$statusInput = $request->query('status', []);

View File

@@ -77,7 +77,7 @@ final class WalletBalanceController extends Controller
{
$code = CurrencyResolver::resolve($request, $player, 'currency');
if (! CurrencyResolver::isValid($code)) {
if (! CurrencyResolver::isEnabled($code)) {
return ApiResponse::error(
__('wallet.invalid_currency'),
ErrorCode::WalletInvalidCurrency->value,

View File

@@ -57,7 +57,7 @@ final class WalletTransferInController extends Controller
{
$code = CurrencyResolver::resolve($request, $player, 'currency');
if (! CurrencyResolver::isValid($code)) {
if (! CurrencyResolver::isEnabled($code)) {
return ApiResponse::error(
__('wallet.invalid_currency'),
ErrorCode::WalletInvalidCurrency->value,

View File

@@ -57,7 +57,7 @@ final class WalletTransferOutController extends Controller
{
$code = CurrencyResolver::resolve($request, $player, 'currency');
if (! CurrencyResolver::isValid($code)) {
if (! CurrencyResolver::isEnabled($code)) {
return ApiResponse::error(
__('wallet.invalid_currency'),
ErrorCode::WalletInvalidCurrency->value,

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
final class AdminCurrencyStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'code' => ['required', 'string', 'max:16', 'regex:/^[A-Z0-9]{1,16}$/', Rule::unique('currencies', 'code')],
'name' => ['required', 'string', 'max:64'],
'decimal_places' => ['sometimes', 'integer', 'min:0', 'max:12'],
'is_enabled' => ['sometimes', 'boolean'],
'is_bettable' => ['sometimes', 'boolean'],
];
}
protected function prepareForValidation(): void
{
if ($this->has('code')) {
$this->merge([
'code' => strtoupper(substr(trim((string) $this->input('code')), 0, 16)),
]);
}
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
final class AdminCurrencyUpdateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:64'],
'decimal_places' => ['sometimes', 'integer', 'min:0', 'max:12'],
'is_enabled' => ['sometimes', 'boolean'],
'is_bettable' => ['sometimes', 'boolean'],
];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Requests\Admin;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
/**
@@ -23,8 +24,18 @@ final class AdminPlayerStoreRequest extends FormRequest
'site_player_id' => ['required', 'string', 'max:128'],
'username' => ['nullable', 'string', 'max:128'],
'nickname' => ['nullable', 'string', 'max:128'],
'default_currency' => ['sometimes', 'string', 'max:16'],
'default_currency' => ['sometimes', 'string', 'max:16', Rule::exists('currencies', 'code')],
'status' => ['sometimes', 'integer', 'in:0,1,2'],
];
}
protected function prepareForValidation(): void
{
if (! $this->has('default_currency')) {
return;
}
$code = strtoupper(substr(trim((string) $this->input('default_currency')), 0, 16));
$this->merge(['default_currency' => $code]);
}
}

View File

@@ -2,8 +2,8 @@
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Foundation\Http\FormRequest;
/**
* 玩家更新请求。
@@ -22,7 +22,18 @@ final class AdminPlayerUpdateRequest extends FormRequest
return [
'username' => ['sometimes', 'string', 'max:128'],
'nickname' => ['sometimes', 'nullable', 'string', 'max:128'],
'default_currency' => ['sometimes', 'string', 'max:16', Rule::exists('currencies', 'code')],
'status' => ['sometimes', 'integer', Rule::in([0, 1, 2])],
];
}
protected function prepareForValidation(): void
{
if (! $this->has('default_currency')) {
return;
}
$code = strtoupper(substr(trim((string) $this->input('default_currency')), 0, 16));
$this->merge(['default_currency' => $code]);
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* 报表任务创建请求。
*
* @see ReportJobStoreController
*/
final class ReportJobStoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, array<int, mixed>>
*/
public function rules(): array
{
return [
'report_type' => ['required', 'string', Rule::in(self::reportTypes())],
'export_format' => ['sometimes', 'string', Rule::in(['csv', 'xlsx'])],
'parameters' => ['sometimes', 'array'],
'parameters.date_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
'parameters.date_to' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'after_or_equal:parameters.date_from'],
'filter_json' => ['sometimes', 'array'],
'filter_json.date_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
'filter_json.date_to' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'after_or_equal:filter_json.date_from'],
];
}
/**
* @return list<string>
*/
public static function reportTypes(): array
{
return [
'draw_profit_summary',
'daily_profit_summary',
'player_win_loss',
'wallet_transfer_report',
'hot_number_risk_report',
'play_dimension_report',
'sold_out_number_report',
'rebate_commission_report',
'audit_operation_report',
'wallet_txns_daily',
'transfer_orders_daily',
];
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class ReportJob extends Model
{
protected $table = 'report_jobs';
protected $fillable = [
'job_no',
'admin_user_id',
'report_type',
'export_format',
'filter_json',
'status',
'output_path',
'error_message',
'finished_at',
];
protected function casts(): array
{
return [
'filter_json' => 'array',
'finished_at' => 'datetime',
];
}
/** @return BelongsTo<AdminUser, ReportJob> */
public function adminUser(): BelongsTo
{
return $this->belongsTo(AdminUser::class, 'admin_user_id');
}
}

View File

@@ -1,116 +0,0 @@
<?php
namespace App\Services\Admin;
use App\Models\AdminUser;
use App\Models\ReportJob;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Services\AuditLogger;
use Illuminate\Support\Facades\DB;
/**
* 报表导出任务:落库 `report_jobs`(阶段 7;异步生成可后续接队列)。
*/
final class AdminReportJobService
{
/**
* @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 {
$jobNo = 'RPT'.now()->format('YmdHis').strtoupper(Str::random(4));
$params = $this->extractReportParameters($request, $filterJson);
$dateFrom = (string) ($params['date_from'] ?? now()->toDateString());
$dateTo = (string) ($params['date_to'] ?? $dateFrom);
$job = ReportJob::query()->create([
'job_no' => $jobNo,
'admin_user_id' => (int) $admin->getKey(),
'report_type' => $reportType,
'export_format' => $exportFormat,
'filter_json' => $filterJson,
'status' => 'completed',
'output_path' => 'reports/'.$this->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;
});
}
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>>
*/
public function reportRows(string $reportType, ?array $filterJson): array
{
$dateFrom = (string) ($filterJson['date_from'] ?? now()->toDateString());
$dateTo = (string) ($filterJson['date_to'] ?? $dateFrom);
return match ($reportType) {
'daily_profit_summary' => [
['日期', '下注', '派彩', '盈亏'],
[$dateFrom, 1000, 600, 400],
[$dateTo, 1200, 500, 700],
],
'audit_operation_report' => [
['模块', '操作', '操作者', '时间', 'IP'],
['report_jobs', 'enqueue', 'admin', now()->toIso8601String(), '127.0.0.1'],
],
default => [
['报表类型', '开始日期', '结束日期'],
[$this->reportLabel($reportType), $dateFrom, $dateTo],
],
};
}
/**
* @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 [];
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace App\Services\Currency;
use App\Models\Currency;
use App\Models\OddsItem;
use App\Models\PlayType;
use App\Models\JackpotPool;
use App\Models\OddsVersion;
use Illuminate\Support\Facades\DB;
use App\Support\OddsStandardScopes;
/**
* 为新开通下注能力的币种补齐最小可运营数据。
*/
final class CurrencyActivationService
{
public function ensureBettableCurrencyReady(Currency $currency): void
{
if (! $currency->is_enabled || ! $currency->is_bettable) {
return;
}
DB::transaction(function () use ($currency): void {
$currencyCode = strtoupper((string) $currency->code);
$this->ensureJackpotPool($currencyCode);
$this->ensureOddsItems($currencyCode);
});
}
private function ensureJackpotPool(string $currencyCode): void
{
$exists = JackpotPool::query()
->where('currency_code', $currencyCode)
->exists();
if ($exists) {
return;
}
$source = JackpotPool::query()
->where('currency_code', '!=', $currencyCode)
->orderByDesc('status')
->orderBy('currency_code')
->first();
JackpotPool::query()->create([
'currency_code' => $currencyCode,
'current_amount' => 0,
'contribution_rate' => (string) ($source?->contribution_rate ?? '0.0200'),
'trigger_threshold' => (int) ($source?->trigger_threshold ?? 100_000_000),
'payout_rate' => (string) ($source?->payout_rate ?? '0.5000'),
'force_trigger_draw_gap' => (int) ($source?->force_trigger_draw_gap ?? 100),
'min_bet_amount' => (int) ($source?->min_bet_amount ?? 100),
'combo_trigger_play_codes' => $source?->combo_trigger_play_codes,
'status' => (int) ($source?->status ?? 0),
'last_trigger_draw_id' => null,
]);
}
private function ensureOddsItems(string $currencyCode): void
{
OddsVersion::query()
->orderBy('id')
->each(function (OddsVersion $version) use ($currencyCode): void {
$this->ensureOddsItemsForVersion($version, $currencyCode);
});
}
private function ensureOddsItemsForVersion(OddsVersion $version, string $currencyCode): void
{
$hasTargetCurrency = OddsItem::query()
->where('version_id', $version->id)
->where('currency_code', $currencyCode)
->exists();
if ($hasTargetCurrency) {
OddsStandardScopes::syncMissingForVersion($version);
return;
}
$sourceCurrency = OddsItem::query()
->where('version_id', $version->id)
->where('currency_code', '!=', $currencyCode)
->orderBy('currency_code')
->value('currency_code');
if (is_string($sourceCurrency) && $sourceCurrency !== '') {
OddsItem::query()
->where('version_id', $version->id)
->where('currency_code', $sourceCurrency)
->orderBy('play_code')
->orderBy('prize_scope')
->get()
->each(function (OddsItem $item) use ($version, $currencyCode): void {
OddsItem::query()->firstOrCreate(
[
'version_id' => $version->id,
'play_code' => $item->play_code,
'prize_scope' => $item->prize_scope,
'currency_code' => $currencyCode,
],
[
'odds_value' => (int) $item->odds_value,
'rebate_rate' => (float) $item->rebate_rate,
'commission_rate' => (float) $item->commission_rate,
'extra_config_json' => $item->extra_config_json,
],
);
});
OddsStandardScopes::syncMissingForVersion($version);
return;
}
foreach (PlayType::query()->orderBy('sort_order')->orderBy('play_code')->get() as $playType) {
foreach (OddsStandardScopes::PRESET_ODDS_BY_SCOPE as $scope => $oddsValue) {
OddsItem::query()->firstOrCreate(
[
'version_id' => $version->id,
'play_code' => $playType->play_code,
'prize_scope' => $scope,
'currency_code' => $currencyCode,
],
[
'odds_value' => $oddsValue,
'rebate_rate' => 0,
'commission_rate' => 0,
'extra_config_json' => null,
],
);
}
}
}
}

View File

@@ -12,7 +12,7 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
*/
final class AdminApiList
{
public const DEFAULT_PER_PAGE = 25;
public const DEFAULT_PER_PAGE = 10;
public const MAX_PER_PAGE = 100;

View File

@@ -27,6 +27,7 @@ final class AdminAuthorizationRegistry
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.wallet.view']],
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.tickets.view']],
['slug' => 'prd.player_freeze.manage', 'name' => '冻结/解冻玩家·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.freeze']],
['slug' => 'prd.currency.manage', 'name' => '币种管理·可管理', 'nav_segment' => 'settings', 'permission_codes' => ['service.currency.manage']],
['slug' => 'prd.wallet_reconcile.manage', 'name' => '钱包对账·可管理', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.manage', 'service.reconcile.manage']],
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']],
@@ -50,11 +51,6 @@ final class AdminAuthorizationRegistry
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.review', 'settlement.batch.view']],
['slug' => 'prd.payout.view', 'name' => '派彩确认·查看', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.view']],
['slug' => 'prd.report.all', 'name' => '报表·全部', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view', 'service.reports.export']],
['slug' => 'prd.report.risk', 'name' => '报表·风控', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view']],
['slug' => 'prd.report.finance', 'name' => '报表·财务', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view', 'service.reports.export']],
['slug' => 'prd.report.player', 'name' => '报表·单用户', 'nav_segment' => 'reports', 'permission_codes' => ['service.reports.view']],
['slug' => 'prd.audit.all', 'name' => '审计日志·全部', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
@@ -74,6 +70,7 @@ final class AdminAuthorizationRegistry
'admin_users' => '管理列表',
'admin_roles' => '角色管理',
'players' => '玩家列表',
'currencies' => '币种管理',
'wallet' => '钱包流水',
'draws' => '期号列表',
'config' => '运营配置',
@@ -82,7 +79,6 @@ final class AdminAuthorizationRegistry
'jackpot' => 'Jackpot',
'reconcile' => '对账',
'tickets' => '玩家注单',
'reports' => '报表导出',
'audit' => '审计日志',
'settings' => '系统设置',
];
@@ -113,17 +109,17 @@ final class AdminAuthorizationRegistry
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin'],
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'requiredAny' => ['prd.admin_role.manage']],
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs']],
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.users.manage', 'prd.users.view_finance']],
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view']],
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'requiredAny' => ['prd.currency.manage']],
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']],
['segment' => 'config', 'label' => 'Configuration', 'href' => '/admin/config', 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.risk_cap.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.jackpot.manage', 'prd.jackpot.view']],
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'requiredAny' => ['prd.draw_result.view', 'prd.draw_result.manage']],
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'requiredAny' => ['prd.users.view_cs', 'prd.users.manage', 'prd.users.view_finance', 'prd.draw_result.view', 'prd.draw_result.manage', 'prd.payout.view', 'prd.payout.review', 'prd.payout.manage', 'prd.report.player']],
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'requiredAny' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'requiredAny' => ['prd.users.view_cs', 'prd.users.manage', 'prd.users.view_finance', 'prd.draw_result.view', 'prd.draw_result.manage', 'prd.payout.view', 'prd.payout.review', 'prd.payout.manage']],
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'requiredAny' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance']],
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings'],
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
];
}
@@ -180,15 +176,16 @@ final class AdminAuthorizationRegistry
'admin_users' => ['prd.admin_user.manage'],
'admin_roles' => ['prd.admin_role.manage'],
'players' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage'],
'currencies' => ['prd.currency.manage'],
'wallet' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.view_finance'],
'draws' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage'],
'config' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.risk_cap.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.jackpot.manage', 'prd.jackpot.view'],
'risk' => ['prd.draw_result.manage', 'prd.draw_result.view'],
'settlement' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view'],
'reconcile' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'],
'tickets' => ['prd.users.view_cs', 'prd.users.manage', 'prd.report.player'],
'reports' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player'],
'tickets' => ['prd.users.view_cs', 'prd.users.manage'],
'audit' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance'],
'settings' => [],
];
if (isset($explicit[$segment])) {
@@ -358,6 +355,10 @@ final class AdminAuthorizationRegistry
['code' => 'admin.config.risk-cap-versions.destroy', 'module_code' => 'config', 'name' => '删除封顶版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}', 'route_name' => 'api.v1.admin.config.risk-cap-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.rebate.manage', 'prd.jackpot.manage']],
['code' => 'admin.settings.index', 'module_code' => 'settings', 'name' => '系统设置列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settings', 'route_name' => 'api.v1.admin.settings.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage']],
['code' => 'admin.settings.update', 'module_code' => 'settings', 'name' => '系统设置更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/{key}', 'route_name' => 'api.v1.admin.settings.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage']],
['code' => 'admin.currencies.index', 'module_code' => 'settings', 'name' => '币种列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.currency.manage']],
['code' => 'admin.currencies.store', 'module_code' => 'settings', 'name' => '创建币种', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
['code' => 'admin.currencies.update', 'module_code' => 'settings', 'name' => '更新币种', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
['code' => 'admin.currencies.destroy', 'module_code' => 'settings', 'name' => '删除币种', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
['code' => 'admin.draws.index', 'module_code' => 'draw', 'name' => '期开奖列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws', 'route_name' => 'api.v1.admin.draws.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
['code' => 'admin.draws.show', 'module_code' => 'draw', 'name' => '期开奖详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/draws/{draw}', 'route_name' => 'api.v1.admin.draws.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.draw_result.manage', 'prd.draw_result.view']],
@@ -410,10 +411,6 @@ final class AdminAuthorizationRegistry
['code' => 'admin.reconcile-jobs.items.index', 'module_code' => 'reconcile', 'name' => '对账任务明细', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reconcile-jobs/{reconcile_job}/items', 'route_name' => 'api.v1.admin.reconcile-jobs.items.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
['code' => 'admin.reconcile-jobs.store', 'module_code' => 'reconcile', 'name' => '创建对账任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/reconcile-jobs', 'route_name' => 'api.v1.admin.reconcile-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage']],
['code' => 'admin.report-jobs.index', 'module_code' => 'report', 'name' => '报表任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.report-jobs.store', 'module_code' => 'report', 'name' => '创建报表任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.report-jobs.show', 'module_code' => 'report', 'name' => '报表任务详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs/{report_job}', 'route_name' => 'api.v1.admin.report-jobs.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
['code' => 'admin.report-jobs.download', 'module_code' => 'report', 'name' => '下载报表任务', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs/{report_job}/download', 'route_name' => 'api.v1.admin.report-jobs.download', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
];
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Support;
use App\Models\Player;
use App\Models\Currency;
use Illuminate\Http\Request;
/**
@@ -52,6 +53,27 @@ final class CurrencyResolver
return preg_match('/^[A-Z0-9]{1,16}$/', $code) === 1;
}
/**
* 币种是否已配置于主数据表。
*/
public static function exists(string $code): bool
{
return self::isValid($code)
&& Currency::query()->where('code', strtoupper($code))->exists();
}
/**
* 币种是否处于启用状态。
*/
public static function isEnabled(string $code): bool
{
return self::isValid($code)
&& Currency::query()
->where('code', strtoupper($code))
->where('is_enabled', true)
->exists();
}
/**
* 解析并验证币种码,无效时返回 null
*/

View File

@@ -21,7 +21,7 @@ trait PaginationTrait
* @param int $default 默认值
* @param int $max 最大值上限
*/
protected function perPage(Request $request, string $key = 'per_page', int $default = 20, int $max = 50): int
protected function perPage(Request $request, string $key = 'per_page', int $default = 10, int $max = 50): int
{
$value = (int) $request->query($key, $request->query('size', $default));
@@ -41,7 +41,7 @@ trait PaginationTrait
*
* @return array{page: int, per_page: int}
*/
protected function paginationMeta(Request $request, int $defaultPerPage = 20, int $maxPerPage = 50): array
protected function paginationMeta(Request $request, int $defaultPerPage = 10, int $maxPerPage = 50): array
{
return [
'page' => $this->page($request),

View File

@@ -7,14 +7,16 @@ use App\Models\SettlementBatch;
final class SettlementBatchFinancialSummary
{
/**
* @return array{total_bet_amount: int, total_actual_deduct: int, platform_profit: int}
* @return array{total_bet_amount: int, total_actual_deduct: int, platform_profit: int, currency_code: ?string}
*/
public static function forBatch(SettlementBatch $batch): array
{
$totals = $batch->details()
->join('ticket_items', 'ticket_items.id', '=', 'ticket_settlement_details.ticket_item_id')
->join('ticket_orders', 'ticket_orders.id', '=', 'ticket_items.order_id')
->selectRaw('COALESCE(SUM(ticket_items.total_bet_amount), 0) as total_bet_amount')
->selectRaw('COALESCE(SUM(ticket_items.actual_deduct_amount), 0) as total_actual_deduct')
->selectRaw('MIN(ticket_orders.currency_code) as currency_code')
->first();
$totalBet = (int) ($totals?->total_bet_amount ?? 0);
@@ -25,6 +27,9 @@ final class SettlementBatchFinancialSummary
'total_bet_amount' => $totalBet,
'total_actual_deduct' => $totalActualDeduct,
'platform_profit' => $totalActualDeduct - $totalPayout,
'currency_code' => is_string($totals?->currency_code) && $totals->currency_code !== ''
? $totals->currency_code
: null,
];
}
}