feat: 增强环境配置与开发服务,支持局域网访问及币种管理
This commit is contained in:
@@ -12,6 +12,10 @@ APP_KEY=
|
|||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
# 应用根 URL(生成链接、邮件、部分驱动依赖;与 php artisan serve 端口一致时带上 :8000)
|
# 应用根 URL(生成链接、邮件、部分驱动依赖;与 php artisan serve 端口一致时带上 :8000)
|
||||||
APP_URL=http://localhost:8000
|
APP_URL=http://localhost:8000
|
||||||
|
# 开发服务监听地址:本机开发可保持 127.0.0.1;需要局域网访问时改为 0.0.0.0
|
||||||
|
APP_BIND_HOST=127.0.0.1
|
||||||
|
# Vite 监听地址:本机开发可保持 127.0.0.1;需要局域网访问时改为 0.0.0.0
|
||||||
|
VITE_HOST=0.0.0.0
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 语言与假数据(config/app.php)
|
# 语言与假数据(config/app.php)
|
||||||
@@ -103,6 +107,8 @@ BROADCAST_CONNECTION=reverb
|
|||||||
REVERB_APP_ID=
|
REVERB_APP_ID=
|
||||||
REVERB_APP_KEY=
|
REVERB_APP_KEY=
|
||||||
REVERB_APP_SECRET=
|
REVERB_APP_SECRET=
|
||||||
|
# Reverb 服务监听地址:本机直连可用 127.0.0.1;需要局域网访问时改为 0.0.0.0
|
||||||
|
REVERB_SERVER_HOST=0.0.0.0
|
||||||
REVERB_HOST=localhost
|
REVERB_HOST=localhost
|
||||||
REVERB_PORT=8080
|
REVERB_PORT=8080
|
||||||
REVERB_SCHEME=http
|
REVERB_SCHEME=http
|
||||||
@@ -225,4 +231,6 @@ DEV_SEED_WALLET_BALANCE_MINOR=125000
|
|||||||
DEV_SEED_WALLET_FROZEN_MINOR=0
|
DEV_SEED_WALLET_FROZEN_MINOR=0
|
||||||
|
|
||||||
# Sanctum SPA 场景:与 API 不同端口的前端域名列表,逗号分隔,用于有状态 Cookie 鉴权
|
# Sanctum SPA 场景:与 API 不同端口的前端域名列表,逗号分隔,用于有状态 Cookie 鉴权
|
||||||
|
# 如果要走局域网访问,把真实 IP 和端口补进来,例如:
|
||||||
|
# SANCTUM_STATEFUL_DOMAINS=localhost,127.0.0.1,::1,192.168.0.101:3800,192.168.0.101:3801,192.168.0.101:8000
|
||||||
SANCTUM_STATEFUL_DOMAINS=
|
SANCTUM_STATEFUL_DOMAINS=
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -69,6 +69,23 @@ php artisan schedule:work
|
|||||||
|
|
||||||
只做 HTTP / 降级轮询、不测 WebSocket 时:**终端 2、3 可先不开**;要完整大厅 WS,则 **三项都开**。
|
只做 HTTP / 降级轮询、不测 WebSocket 时:**终端 2、3 可先不开**;要完整大厅 WS,则 **三项都开**。
|
||||||
|
|
||||||
|
## 统一配置说明
|
||||||
|
|
||||||
|
这套后端把「运行监听地址」和「对外访问地址」分开管理,避免上线时到处改常量:
|
||||||
|
|
||||||
|
- `APP_URL`:对外生成链接、邮件、重定向时使用的应用根地址
|
||||||
|
- `APP_BIND_HOST`:`php artisan serve` 监听哪块网卡
|
||||||
|
- `VITE_HOST`:`npm run dev` / Vite 监听哪块网卡
|
||||||
|
- `REVERB_SERVER_HOST`:`php artisan reverb:start` 监听哪块网卡
|
||||||
|
- `REVERB_HOST`:浏览器连接 Reverb 时看到的主机名或 IP
|
||||||
|
- `SANCTUM_STATEFUL_DOMAINS`:允许带 Cookie 的前端来源列表
|
||||||
|
|
||||||
|
如果你要用局域网地址访问,比如 `http://192.168.0.101:8000`,通常只需要:
|
||||||
|
|
||||||
|
1. 把 `APP_BIND_HOST`、`VITE_HOST` 和 `REVERB_SERVER_HOST` 改成 `0.0.0.0`
|
||||||
|
2. 把 `APP_URL`、`REVERB_HOST`、`SANCTUM_STATEFUL_DOMAINS` 改成你的局域网 IP
|
||||||
|
3. 前端的 `NEXT_PUBLIC_*` 变量也同步改成同一个局域网地址
|
||||||
|
|
||||||
## 后台权限体检
|
## 后台权限体检
|
||||||
|
|
||||||
后台权限现在提供了一条可直接接入 CI 的体检命令,用来检查:
|
后台权限现在提供了一条可直接接入 CI 的体检命令,用来检查:
|
||||||
@@ -83,6 +100,21 @@ php artisan schedule:work
|
|||||||
php artisan lottery:admin-auth-audit
|
php artisan lottery:admin-auth-audit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果你新增了后台接口、权限动作或资源绑定,推荐按这条标准流程走:
|
||||||
|
|
||||||
|
1. 只改 `app/Support/AdminAuthorizationRegistry.php`:
|
||||||
|
- 新接口资源加到 `resources()`
|
||||||
|
- 如需新的 legacy 权限分组,再补 `permissionDefinitions()` / 导航分组
|
||||||
|
2. 执行同步命令,把注册表写回数据库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan lottery:admin-auth-sync --audit
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 确认体检通过后再提交代码
|
||||||
|
|
||||||
|
除非是历史数据修复或首发引导场景,后续不要再为单个后台接口单独写 `admin_api_resources` 补丁 migration,优先走“注册表 + 同步命令”这条主路径。
|
||||||
|
|
||||||
或通过 Composer 脚本执行:
|
或通过 Composer 脚本执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use App\Support\AdminAuthorizationRegistry;
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
|
|
||||||
final class SyncAdminAuthorizationCommand extends Command
|
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';
|
protected $description = '根据后台统一注册表同步 admin_api_resources / bindings / role_api_resources';
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ final class SyncAdminAuthorizationCommand extends Command
|
|||||||
$menuActionId = $menuActionIds[$permissionCode] ?? null;
|
$menuActionId = $menuActionIds[$permissionCode] ?? null;
|
||||||
if ($menuActionId === null) {
|
if ($menuActionId === null) {
|
||||||
$this->warn(sprintf('跳过未找到的 permission_code: %s', $permissionCode));
|
$this->warn(sprintf('跳过未找到的 permission_code: %s', $permissionCode));
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +90,10 @@ final class SyncAdminAuthorizationCommand extends Command
|
|||||||
$roleResourceRows->count(),
|
$roleResourceRows->count(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if ((bool) $this->option('audit')) {
|
||||||
|
return $this->call('lottery:admin-auth-audit');
|
||||||
|
}
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ use App\Models\Player;
|
|||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
use App\Support\ApiResponse;
|
use App\Support\ApiResponse;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Support\PlayerApiPresenter;
|
use App\Support\PlayerApiPresenter;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\AdminPlayerStoreRequest;
|
use App\Http\Requests\Admin\AdminPlayerStoreRequest;
|
||||||
|
|
||||||
/** POST /api/v1/admin/players */
|
/** POST /api/v1/admin/players */
|
||||||
@@ -38,6 +38,6 @@ final class AdminPlayerStoreController extends Controller
|
|||||||
'status' => $request->validated('status', 0),
|
'status' => $request->validated('status', 0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return ApiResponse::success(PlayerApiPresenter::listItem($player), 201);
|
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class AdminPlayerTicketItemsIndexController extends Controller
|
|||||||
|
|
||||||
public function __invoke(AdminPlayerTicketItemsRequest $request, Player $player): JsonResponse
|
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);
|
$page = $this->page($request);
|
||||||
$drawNo = $request->validated('draw_no');
|
$drawNo = $request->validated('draw_no');
|
||||||
if (is_string($drawNo)) {
|
if (is_string($drawNo)) {
|
||||||
|
|||||||
@@ -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']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Risk;
|
|||||||
|
|
||||||
use App\Models\Draw;
|
use App\Models\Draw;
|
||||||
use App\Models\RiskPool;
|
use App\Models\RiskPool;
|
||||||
|
use App\Models\TicketOrder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Support\AdminApiList;
|
use App\Support\AdminApiList;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -49,9 +50,14 @@ final class AdminRiskPoolIndexController extends Controller
|
|||||||
/** @var LengthAwarePaginator $paginator */
|
/** @var LengthAwarePaginator $paginator */
|
||||||
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
$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), [
|
return AdminApiList::jsonWith($paginator, fn (RiskPool $row) => $this->row($row), [
|
||||||
'draw_id' => (int) $draw->id,
|
'draw_id' => (int) $draw->id,
|
||||||
'draw_no' => $draw->draw_no,
|
'draw_no' => $draw->draw_no,
|
||||||
|
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Api\V1\Admin\Risk;
|
namespace App\Http\Controllers\Api\V1\Admin\Risk;
|
||||||
|
|
||||||
use App\Models\Draw;
|
use App\Models\Draw;
|
||||||
|
use App\Models\TicketOrder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Support\AdminApiList;
|
use App\Support\AdminApiList;
|
||||||
use App\Models\RiskPoolLockLog;
|
use App\Models\RiskPoolLockLog;
|
||||||
@@ -38,9 +39,14 @@ final class AdminRiskPoolLockLogIndexController extends Controller
|
|||||||
/** @var LengthAwarePaginator $paginator */
|
/** @var LengthAwarePaginator $paginator */
|
||||||
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
$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), [
|
return AdminApiList::jsonWith($paginator, fn (RiskPoolLockLog $log) => $this->row($log), [
|
||||||
'draw_id' => (int) $draw->id,
|
'draw_id' => (int) $draw->id,
|
||||||
'draw_no' => $draw->draw_no,
|
'draw_no' => $draw->draw_no,
|
||||||
|
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Risk;
|
|||||||
use App\Models\Draw;
|
use App\Models\Draw;
|
||||||
use App\Models\RiskPool;
|
use App\Models\RiskPool;
|
||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
|
use App\Models\TicketOrder;
|
||||||
use App\Support\ApiResponse;
|
use App\Support\ApiResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Support\AdminApiList;
|
use App\Support\AdminApiList;
|
||||||
@@ -56,10 +57,14 @@ final class AdminRiskPoolShowController extends Controller
|
|||||||
|
|
||||||
$cap = (int) $pool->total_cap_amount;
|
$cap = (int) $pool->total_cap_amount;
|
||||||
$locked = (int) $pool->locked_amount;
|
$locked = (int) $pool->locked_amount;
|
||||||
|
$currencyCode = (string) (TicketOrder::query()
|
||||||
|
->where('draw_id', $draw->id)
|
||||||
|
->value('currency_code') ?? '');
|
||||||
|
|
||||||
return ApiResponse::success([
|
return ApiResponse::success([
|
||||||
'draw_id' => (int) $draw->id,
|
'draw_id' => (int) $draw->id,
|
||||||
'draw_no' => $draw->draw_no,
|
'draw_no' => $draw->draw_no,
|
||||||
|
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
||||||
'pool' => [
|
'pool' => [
|
||||||
'normalized_number' => $pool->normalized_number,
|
'normalized_number' => $pool->normalized_number,
|
||||||
'total_cap_amount' => $cap,
|
'total_cap_amount' => $cap,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ final class AdminSettlementBatchDetailsController extends Controller
|
|||||||
->with([
|
->with([
|
||||||
'ticketItem:id,ticket_no,play_code,player_id',
|
'ticketItem:id,ticket_no,play_code,player_id',
|
||||||
'ticketItem.player:id,username,site_player_id',
|
'ticketItem.player:id,username,site_player_id',
|
||||||
|
'ticketItem.order:id,currency_code',
|
||||||
])
|
])
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
||||||
@@ -31,12 +32,14 @@ final class AdminSettlementBatchDetailsController extends Controller
|
|||||||
/** @var TicketSettlementDetail $row */
|
/** @var TicketSettlementDetail $row */
|
||||||
$item = $row->ticketItem;
|
$item = $row->ticketItem;
|
||||||
$player = $item?->player;
|
$player = $item?->player;
|
||||||
|
$order = $item?->order;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $row->id,
|
'id' => (int) $row->id,
|
||||||
'ticket_item_id' => (int) $row->ticket_item_id,
|
'ticket_item_id' => (int) $row->ticket_item_id,
|
||||||
'ticket_no' => $item?->ticket_no,
|
'ticket_no' => $item?->ticket_no,
|
||||||
'play_code' => $item?->play_code,
|
'play_code' => $item?->play_code,
|
||||||
|
'currency_code' => $order?->currency_code,
|
||||||
'player_id' => $item?->player_id,
|
'player_id' => $item?->player_id,
|
||||||
'player_username' => $player?->username,
|
'player_username' => $player?->username,
|
||||||
'site_player_id' => $player?->site_player_id,
|
'site_player_id' => $player?->site_player_id,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ final class AdminSettlementBatchIndexController extends Controller
|
|||||||
'id' => (int) $b->id,
|
'id' => (int) $b->id,
|
||||||
'draw_id' => (int) $b->draw_id,
|
'draw_id' => (int) $b->draw_id,
|
||||||
'draw_no' => $b->draw?->draw_no,
|
'draw_no' => $b->draw?->draw_no,
|
||||||
|
'currency_code' => $financial['currency_code'],
|
||||||
'result_batch_id' => (int) $b->result_batch_id,
|
'result_batch_id' => (int) $b->result_batch_id,
|
||||||
'settle_version' => (int) $b->settle_version,
|
'settle_version' => (int) $b->settle_version,
|
||||||
'status' => $b->status,
|
'status' => $b->status,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ final class AdminSettlementBatchShowController extends Controller
|
|||||||
'id' => (int) $batch->id,
|
'id' => (int) $batch->id,
|
||||||
'draw_id' => (int) $batch->draw_id,
|
'draw_id' => (int) $batch->draw_id,
|
||||||
'draw_no' => $batch->draw?->draw_no,
|
'draw_no' => $batch->draw?->draw_no,
|
||||||
|
'currency_code' => $financial['currency_code'],
|
||||||
'draw_status' => $batch->draw?->status,
|
'draw_status' => $batch->draw?->status,
|
||||||
'result_batch_id' => (int) $batch->result_batch_id,
|
'result_batch_id' => (int) $batch->result_batch_id,
|
||||||
'result_batch_version' => $batch->resultBatch?->result_version,
|
'result_batch_version' => $batch->resultBatch?->result_version,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ final class AdminTicketItemIndexController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
$perPage = $this->perPage($request, 'per_page', 20, 100);
|
$perPage = $this->perPage($request, 'per_page', 10, 100);
|
||||||
$page = $this->page($request);
|
$page = $this->page($request);
|
||||||
|
|
||||||
$query = TicketItem::query()
|
$query = TicketItem::query()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use App\Http\Requests\Admin\TransferOrderListRequest;
|
|||||||
*
|
*
|
||||||
* Query:
|
* Query:
|
||||||
* - `page`(默认 1)
|
* - `page`(默认 1)
|
||||||
* - `per_page` 或 `size`(每页条数,默认 20,最大 100)
|
* - `per_page` 或 `size`(每页条数,默认 10,最大 100)
|
||||||
* - `player_id`(可选,按玩家主键)
|
* - `player_id`(可选,按玩家主键)
|
||||||
* - `player_account`(可选,模糊匹配 `players.site_player_id` / `username`;与 `player_id` 同时传时以 `player_id` 为准)
|
* - `player_account`(可选,模糊匹配 `players.site_player_id` / `username`;与 `player_id` 同时传时以 `player_id` 为准)
|
||||||
* - `transfer_no`(可选,模糊匹配本地单号)
|
* - `transfer_no`(可选,模糊匹配本地单号)
|
||||||
@@ -35,7 +35,7 @@ final class TransferOrderListController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
$perPage = $this->perPage($request, 'per_page', 20, 100);
|
$perPage = $this->perPage($request, 'per_page', 10, 100);
|
||||||
$page = $this->page($request);
|
$page = $this->page($request);
|
||||||
|
|
||||||
$query = TransferOrder::query()
|
$query = TransferOrder::query()
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ final class WalletTransactionListController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
$perPage = $this->perPage($request, 'per_page', 20, 100);
|
$perPage = $this->perPage($request, 'per_page', 10, 100);
|
||||||
$page = $this->page($request);
|
$page = $this->page($request);
|
||||||
|
|
||||||
$query = WalletTxn::query()
|
$query = WalletTxn::query()
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,11 @@ final class JackpotSummaryController extends Controller
|
|||||||
|
|
||||||
public function __invoke(Request $request): JsonResponse
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ final class TicketItemsIndexController extends Controller
|
|||||||
/** @var Player $player */
|
/** @var Player $player */
|
||||||
$player = $request->attributes->get('lottery_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);
|
$page = $this->page($request);
|
||||||
$drawNo = $request->query('draw_no');
|
$drawNo = $request->query('draw_no');
|
||||||
$statusInput = $request->query('status', []);
|
$statusInput = $request->query('status', []);
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ final class WalletBalanceController extends Controller
|
|||||||
{
|
{
|
||||||
$code = CurrencyResolver::resolve($request, $player, 'currency');
|
$code = CurrencyResolver::resolve($request, $player, 'currency');
|
||||||
|
|
||||||
if (! CurrencyResolver::isValid($code)) {
|
if (! CurrencyResolver::isEnabled($code)) {
|
||||||
return ApiResponse::error(
|
return ApiResponse::error(
|
||||||
__('wallet.invalid_currency'),
|
__('wallet.invalid_currency'),
|
||||||
ErrorCode::WalletInvalidCurrency->value,
|
ErrorCode::WalletInvalidCurrency->value,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ final class WalletTransferInController extends Controller
|
|||||||
{
|
{
|
||||||
$code = CurrencyResolver::resolve($request, $player, 'currency');
|
$code = CurrencyResolver::resolve($request, $player, 'currency');
|
||||||
|
|
||||||
if (! CurrencyResolver::isValid($code)) {
|
if (! CurrencyResolver::isEnabled($code)) {
|
||||||
return ApiResponse::error(
|
return ApiResponse::error(
|
||||||
__('wallet.invalid_currency'),
|
__('wallet.invalid_currency'),
|
||||||
ErrorCode::WalletInvalidCurrency->value,
|
ErrorCode::WalletInvalidCurrency->value,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ final class WalletTransferOutController extends Controller
|
|||||||
{
|
{
|
||||||
$code = CurrencyResolver::resolve($request, $player, 'currency');
|
$code = CurrencyResolver::resolve($request, $player, 'currency');
|
||||||
|
|
||||||
if (! CurrencyResolver::isValid($code)) {
|
if (! CurrencyResolver::isEnabled($code)) {
|
||||||
return ApiResponse::error(
|
return ApiResponse::error(
|
||||||
__('wallet.invalid_currency'),
|
__('wallet.invalid_currency'),
|
||||||
ErrorCode::WalletInvalidCurrency->value,
|
ErrorCode::WalletInvalidCurrency->value,
|
||||||
|
|||||||
34
app/Http/Requests/Admin/AdminCurrencyStoreRequest.php
Normal file
34
app/Http/Requests/Admin/AdminCurrencyStoreRequest.php
Normal 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)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Http/Requests/Admin/AdminCurrencyUpdateRequest.php
Normal file
23
app/Http/Requests/Admin/AdminCurrencyUpdateRequest.php
Normal 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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin;
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,8 +24,18 @@ final class AdminPlayerStoreRequest extends FormRequest
|
|||||||
'site_player_id' => ['required', 'string', 'max:128'],
|
'site_player_id' => ['required', 'string', 'max:128'],
|
||||||
'username' => ['nullable', 'string', 'max:128'],
|
'username' => ['nullable', 'string', 'max:128'],
|
||||||
'nickname' => ['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'],
|
'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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin;
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 玩家更新请求。
|
* 玩家更新请求。
|
||||||
@@ -22,7 +22,18 @@ final class AdminPlayerUpdateRequest extends FormRequest
|
|||||||
return [
|
return [
|
||||||
'username' => ['sometimes', 'string', 'max:128'],
|
'username' => ['sometimes', 'string', 'max:128'],
|
||||||
'nickname' => ['sometimes', 'nullable', '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])],
|
'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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
138
app/Services/Currency/CurrencyActivationService.php
Normal file
138
app/Services/Currency/CurrencyActivationService.php
Normal 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,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|||||||
*/
|
*/
|
||||||
final class AdminApiList
|
final class AdminApiList
|
||||||
{
|
{
|
||||||
public const DEFAULT_PER_PAGE = 25;
|
public const DEFAULT_PER_PAGE = 10;
|
||||||
|
|
||||||
public const MAX_PER_PAGE = 100;
|
public const MAX_PER_PAGE = 100;
|
||||||
|
|
||||||
|
|||||||
@@ -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_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.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.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.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']],
|
['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.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.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.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.self', 'name' => '审计日志·自身相关', 'nav_segment' => 'audit', 'permission_codes' => ['service.audit.view']],
|
||||||
['slug' => 'prd.audit.finance', '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_users' => '管理列表',
|
||||||
'admin_roles' => '角色管理',
|
'admin_roles' => '角色管理',
|
||||||
'players' => '玩家列表',
|
'players' => '玩家列表',
|
||||||
|
'currencies' => '币种管理',
|
||||||
'wallet' => '钱包流水',
|
'wallet' => '钱包流水',
|
||||||
'draws' => '期号列表',
|
'draws' => '期号列表',
|
||||||
'config' => '运营配置',
|
'config' => '运营配置',
|
||||||
@@ -82,7 +79,6 @@ final class AdminAuthorizationRegistry
|
|||||||
'jackpot' => 'Jackpot',
|
'jackpot' => 'Jackpot',
|
||||||
'reconcile' => '对账',
|
'reconcile' => '对账',
|
||||||
'tickets' => '玩家注单',
|
'tickets' => '玩家注单',
|
||||||
'reports' => '报表导出',
|
|
||||||
'audit' => '审计日志',
|
'audit' => '审计日志',
|
||||||
'settings' => '系统设置',
|
'settings' => '系统设置',
|
||||||
];
|
];
|
||||||
@@ -113,17 +109,17 @@ final class AdminAuthorizationRegistry
|
|||||||
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin'],
|
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin'],
|
||||||
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
|
['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' => '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' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.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.users.manage', 'prd.users.view_finance']],
|
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'requiredAny' => ['prd.currency.manage']],
|
||||||
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view']],
|
['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' => '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' => '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' => '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' => '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' => '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' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'requiredAny' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player']],
|
|
||||||
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'requiredAny' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance']],
|
['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_users' => ['prd.admin_user.manage'],
|
||||||
'admin_roles' => ['prd.admin_role.manage'],
|
'admin_roles' => ['prd.admin_role.manage'],
|
||||||
'players' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.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'],
|
'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'],
|
'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'],
|
'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'],
|
'risk' => ['prd.draw_result.manage', 'prd.draw_result.view'],
|
||||||
'settlement' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.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'],
|
'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'],
|
'tickets' => ['prd.users.view_cs', 'prd.users.manage'],
|
||||||
'reports' => ['prd.report.all', 'prd.report.risk', 'prd.report.finance', 'prd.report.player'],
|
|
||||||
'audit' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance'],
|
'audit' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance'],
|
||||||
|
'settings' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isset($explicit[$segment])) {
|
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.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.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.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.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']],
|
['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.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.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']],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
|
use App\Models\Currency;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,6 +53,27 @@ final class CurrencyResolver
|
|||||||
return preg_match('/^[A-Z0-9]{1,16}$/', $code) === 1;
|
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。
|
* 解析并验证币种码,无效时返回 null。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ trait PaginationTrait
|
|||||||
* @param int $default 默认值
|
* @param int $default 默认值
|
||||||
* @param int $max 最大值上限
|
* @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));
|
$value = (int) $request->query($key, $request->query('size', $default));
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ trait PaginationTrait
|
|||||||
*
|
*
|
||||||
* @return array{page: int, per_page: int}
|
* @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 [
|
return [
|
||||||
'page' => $this->page($request),
|
'page' => $this->page($request),
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ use App\Models\SettlementBatch;
|
|||||||
final class SettlementBatchFinancialSummary
|
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
|
public static function forBatch(SettlementBatch $batch): array
|
||||||
{
|
{
|
||||||
$totals = $batch->details()
|
$totals = $batch->details()
|
||||||
->join('ticket_items', 'ticket_items.id', '=', 'ticket_settlement_details.ticket_item_id')
|
->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.total_bet_amount), 0) as total_bet_amount')
|
||||||
->selectRaw('COALESCE(SUM(ticket_items.actual_deduct_amount), 0) as total_actual_deduct')
|
->selectRaw('COALESCE(SUM(ticket_items.actual_deduct_amount), 0) as total_actual_deduct')
|
||||||
|
->selectRaw('MIN(ticket_orders.currency_code) as currency_code')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$totalBet = (int) ($totals?->total_bet_amount ?? 0);
|
$totalBet = (int) ($totals?->total_bet_amount ?? 0);
|
||||||
@@ -25,6 +27,9 @@ final class SettlementBatchFinancialSummary
|
|||||||
'total_bet_amount' => $totalBet,
|
'total_bet_amount' => $totalBet,
|
||||||
'total_actual_deduct' => $totalActualDeduct,
|
'total_actual_deduct' => $totalActualDeduct,
|
||||||
'platform_profit' => $totalActualDeduct - $totalPayout,
|
'platform_profit' => $totalActualDeduct - $totalPayout,
|
||||||
|
'currency_code' => is_string($totals?->currency_code) && $totals->currency_code !== ''
|
||||||
|
? $totals->currency_code
|
||||||
|
: null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,11 @@
|
|||||||
],
|
],
|
||||||
"dev": [
|
"dev": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#f472b6\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#f472b6\" \"php artisan serve --host=\\\"${APP_BIND_HOST:-127.0.0.1}\\\"\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
],
|
],
|
||||||
"dev:realtime": [
|
"dev:realtime": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74,#34d399,#f472b6\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"php artisan reverb:start\" \"php artisan schedule:work\" \"npm run dev\" --names=server,queue,logs,reverb,schedule,vite --kill-others"
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74,#34d399,#f472b6\" \"php artisan serve --host=\\\"${APP_BIND_HOST:-127.0.0.1}\\\"\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"php artisan reverb:start --host=\\\"${REVERB_SERVER_HOST:-0.0.0.0}\\\" --hostname=\\\"${REVERB_HOST:-localhost}\\\" --port=\\\"${REVERB_PORT:-8080}\\\"\" \"php artisan schedule:work\" \"npm run dev\" --names=server,queue,logs,reverb,schedule,vite --kill-others"
|
||||||
],
|
],
|
||||||
"dev:schedule": [
|
"dev:schedule": [
|
||||||
"Composer\\Config::disableProcessTimeout",
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
|||||||
@@ -267,7 +267,6 @@ return new class extends Migration
|
|||||||
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.tickets', 'name' => '玩家注单', 'path' => '/admin/tickets', 'route_name' => 'admin.tickets.index', 'component' => 'service/tickets', 'icon' => null, 'sort_order' => 20],
|
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.tickets', 'name' => '玩家注单', 'path' => '/admin/tickets', 'route_name' => 'admin.tickets.index', 'component' => 'service/tickets', 'icon' => null, 'sort_order' => 20],
|
||||||
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.wallet', 'name' => '钱包流水', 'path' => '/admin/wallet/transactions', 'route_name' => 'admin.wallet.transactions', 'component' => 'service/wallet', 'icon' => null, 'sort_order' => 30],
|
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.wallet', 'name' => '钱包流水', 'path' => '/admin/wallet/transactions', 'route_name' => 'admin.wallet.transactions', 'component' => 'service/wallet', 'icon' => null, 'sort_order' => 30],
|
||||||
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.reconcile', 'name' => '对账管理', 'path' => '/admin/reconcile', 'route_name' => 'admin.reconcile.index', 'component' => 'service/reconcile', 'icon' => null, 'sort_order' => 40],
|
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.reconcile', 'name' => '对账管理', 'path' => '/admin/reconcile', 'route_name' => 'admin.reconcile.index', 'component' => 'service/reconcile', 'icon' => null, 'sort_order' => 40],
|
||||||
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.reports', 'name' => '报表导出', 'path' => '/admin/reports', 'route_name' => 'admin.reports.index', 'component' => 'service/reports', 'icon' => null, 'sort_order' => 50],
|
|
||||||
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.audit', 'name' => '审计日志', 'path' => '/admin/audit-logs', 'route_name' => 'admin.audit.index', 'component' => 'service/audit', 'icon' => null, 'sort_order' => 60],
|
['parent_code' => 'service', 'menu_type' => 'page', 'code' => 'service.audit', 'name' => '审计日志', 'path' => '/admin/audit-logs', 'route_name' => 'admin.audit.index', 'component' => 'service/audit', 'icon' => null, 'sort_order' => 60],
|
||||||
['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_user', 'name' => '管理员权限', 'path' => '/admin/admin-users', 'route_name' => 'admin.system.admin-users', 'component' => 'system/admin-users', 'icon' => 'users-round', 'sort_order' => 70],
|
['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_user', 'name' => '管理员权限', 'path' => '/admin/admin-users', 'route_name' => 'admin.system.admin-users', 'component' => 'system/admin-users', 'icon' => 'users-round', 'sort_order' => 70],
|
||||||
['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_role', 'name' => '角色管理', 'path' => '/admin/admin-roles', 'route_name' => 'admin.system.admin-roles', 'component' => 'system/admin-roles', 'icon' => 'shield-check', 'sort_order' => 71],
|
['parent_code' => null, 'menu_type' => 'page', 'code' => 'system.admin_role', 'name' => '角色管理', 'path' => '/admin/admin-roles', 'route_name' => 'admin.system.admin-roles', 'component' => 'system/admin-roles', 'icon' => 'shield-check', 'sort_order' => 71],
|
||||||
@@ -320,8 +319,6 @@ return new class extends Migration
|
|||||||
['menu_code' => 'service.wallet', 'action_code' => 'manage', 'permission_code' => 'service.wallet.manage', 'name' => '钱包流水管理'],
|
['menu_code' => 'service.wallet', 'action_code' => 'manage', 'permission_code' => 'service.wallet.manage', 'name' => '钱包流水管理'],
|
||||||
['menu_code' => 'service.reconcile', 'action_code' => 'view', 'permission_code' => 'service.reconcile.view', 'name' => '对账查看'],
|
['menu_code' => 'service.reconcile', 'action_code' => 'view', 'permission_code' => 'service.reconcile.view', 'name' => '对账查看'],
|
||||||
['menu_code' => 'service.reconcile', 'action_code' => 'manage', 'permission_code' => 'service.reconcile.manage', 'name' => '对账管理'],
|
['menu_code' => 'service.reconcile', 'action_code' => 'manage', 'permission_code' => 'service.reconcile.manage', 'name' => '对账管理'],
|
||||||
['menu_code' => 'service.reports', 'action_code' => 'view', 'permission_code' => 'service.reports.view', 'name' => '报表查看'],
|
|
||||||
['menu_code' => 'service.reports', 'action_code' => 'export', 'permission_code' => 'service.reports.export', 'name' => '报表导出'],
|
|
||||||
['menu_code' => 'service.audit', 'action_code' => 'view', 'permission_code' => 'service.audit.view', 'name' => '审计查看'],
|
['menu_code' => 'service.audit', 'action_code' => 'view', 'permission_code' => 'service.audit.view', 'name' => '审计查看'],
|
||||||
['menu_code' => 'system.admin_user', 'action_code' => 'manage', 'permission_code' => 'system.admin_user.manage', 'name' => '管理员权限管理'],
|
['menu_code' => 'system.admin_user', 'action_code' => 'manage', 'permission_code' => 'system.admin_user.manage', 'name' => '管理员权限管理'],
|
||||||
['menu_code' => 'system.admin_role', 'action_code' => 'manage', 'permission_code' => 'system.admin_role.manage', 'name' => '角色权限管理'],
|
['menu_code' => 'system.admin_role', 'action_code' => 'manage', 'permission_code' => 'system.admin_role.manage', 'name' => '角色权限管理'],
|
||||||
@@ -352,8 +349,6 @@ return new class extends Migration
|
|||||||
['code' => 'admin.wallet.transfer-orders', 'module_code' => 'wallet', 'name' => '转账单查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders', 'route_name' => 'api.v1.admin.wallet.transfer-orders', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.wallet.view', 'service.wallet.manage']],
|
['code' => 'admin.wallet.transfer-orders', 'module_code' => 'wallet', 'name' => '转账单查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/wallet/transfer-orders', 'route_name' => 'api.v1.admin.wallet.transfer-orders', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.wallet.view', 'service.wallet.manage']],
|
||||||
['code' => 'admin.players.wallets', 'module_code' => 'player_service', 'name' => '玩家钱包查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/wallets', 'route_name' => 'api.v1.admin.players.wallets', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.wallet.view']],
|
['code' => 'admin.players.wallets', 'module_code' => 'player_service', 'name' => '玩家钱包查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/wallets', 'route_name' => 'api.v1.admin.players.wallets', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.wallet.view']],
|
||||||
['code' => 'admin.players.ticket-items', 'module_code' => 'player_service', 'name' => '玩家注单查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/ticket-items', 'route_name' => 'api.v1.admin.players.ticket-items.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.tickets.view']],
|
['code' => 'admin.players.ticket-items', 'module_code' => 'player_service', 'name' => '玩家注单查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/players/{player}/ticket-items', 'route_name' => 'api.v1.admin.players.ticket-items.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.players.manage', 'service.tickets.view']],
|
||||||
['code' => 'admin.reports.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, 'permission_codes' => ['service.reports.view', 'service.reports.export']],
|
|
||||||
['code' => 'admin.reports.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, 'permission_codes' => ['service.reports.export']],
|
|
||||||
['code' => 'admin.reconcile.index', 'module_code' => 'reconcile', 'name' => '对账任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reconcile-jobs', 'route_name' => 'api.v1.admin.reconcile-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.reconcile.view', 'service.reconcile.manage']],
|
['code' => 'admin.reconcile.index', 'module_code' => 'reconcile', 'name' => '对账任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reconcile-jobs', 'route_name' => 'api.v1.admin.reconcile-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.reconcile.view', 'service.reconcile.manage']],
|
||||||
['code' => 'admin.reconcile.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, 'permission_codes' => ['service.reconcile.manage']],
|
['code' => 'admin.reconcile.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, 'permission_codes' => ['service.reconcile.manage']],
|
||||||
['code' => 'admin.audit.index', 'module_code' => 'audit', 'name' => '审计日志查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/audit-logs', 'route_name' => 'api.v1.admin.audit-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.audit.view']],
|
['code' => 'admin.audit.index', 'module_code' => 'audit', 'name' => '审计日志查询', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/audit-logs', 'route_name' => 'api.v1.admin.audit-logs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.audit.view']],
|
||||||
@@ -447,10 +442,6 @@ return new class extends Migration
|
|||||||
'prd.wallet_reconcile.manage' => ['service.wallet.manage', 'service.reconcile.manage'],
|
'prd.wallet_reconcile.manage' => ['service.wallet.manage', 'service.reconcile.manage'],
|
||||||
'prd.wallet_reconcile.view' => ['service.wallet.view', 'service.reconcile.view'],
|
'prd.wallet_reconcile.view' => ['service.wallet.view', 'service.reconcile.view'],
|
||||||
'prd.wallet_reconcile.view_cs' => ['service.wallet.view', 'service.reconcile.view'],
|
'prd.wallet_reconcile.view_cs' => ['service.wallet.view', 'service.reconcile.view'],
|
||||||
'prd.report.all' => ['service.reports.view', 'service.reports.export'],
|
|
||||||
'prd.report.risk' => ['service.reports.view'],
|
|
||||||
'prd.report.finance' => ['service.reports.view', 'service.reports.export'],
|
|
||||||
'prd.report.player' => ['service.reports.view'],
|
|
||||||
'prd.audit.all' => ['service.audit.view'],
|
'prd.audit.all' => ['service.audit.view'],
|
||||||
'prd.audit.self' => ['service.audit.view'],
|
'prd.audit.self' => ['service.audit.view'],
|
||||||
'prd.audit.finance' => ['service.audit.view'],
|
'prd.audit.finance' => ['service.audit.view'],
|
||||||
@@ -654,10 +645,6 @@ return new class extends Migration
|
|||||||
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看'],
|
['slug' => 'prd.wallet_reconcile.view', 'name' => '钱包对账·查看'],
|
||||||
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户'],
|
['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户'],
|
||||||
['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理'],
|
['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理'],
|
||||||
['slug' => 'prd.report.all', 'name' => '报表·全部'],
|
|
||||||
['slug' => 'prd.report.risk', 'name' => '报表·风控'],
|
|
||||||
['slug' => 'prd.report.finance', 'name' => '报表·财务'],
|
|
||||||
['slug' => 'prd.report.player', 'name' => '报表·单用户'],
|
|
||||||
['slug' => 'prd.audit.all', 'name' => '审计日志·全部'],
|
['slug' => 'prd.audit.all', 'name' => '审计日志·全部'],
|
||||||
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关'],
|
['slug' => 'prd.audit.self', 'name' => '审计日志·自身相关'],
|
||||||
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关'],
|
['slug' => 'prd.audit.finance', 'name' => '审计日志·资金相关'],
|
||||||
@@ -688,7 +675,6 @@ return new class extends Migration
|
|||||||
'prd.draw_result.manage',
|
'prd.draw_result.manage',
|
||||||
'prd.payout.review',
|
'prd.payout.review',
|
||||||
'prd.wallet_reconcile.view',
|
'prd.wallet_reconcile.view',
|
||||||
'prd.report.risk',
|
|
||||||
'prd.audit.self',
|
'prd.audit.self',
|
||||||
'prd.player_freeze.manage',
|
'prd.player_freeze.manage',
|
||||||
],
|
],
|
||||||
@@ -701,14 +687,12 @@ return new class extends Migration
|
|||||||
'prd.payout.view',
|
'prd.payout.view',
|
||||||
'prd.wallet_reconcile.manage',
|
'prd.wallet_reconcile.manage',
|
||||||
'prd.wallet_adjust.manage',
|
'prd.wallet_adjust.manage',
|
||||||
'prd.report.finance',
|
|
||||||
'prd.audit.finance',
|
'prd.audit.finance',
|
||||||
],
|
],
|
||||||
'customer_service' => [
|
'customer_service' => [
|
||||||
'prd.users.view_cs',
|
'prd.users.view_cs',
|
||||||
'prd.draw_result.view',
|
'prd.draw_result.view',
|
||||||
'prd.wallet_reconcile.view_cs',
|
'prd.wallet_reconcile.view_cs',
|
||||||
'prd.report.player',
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
|
||||||
|
|
||||||
|
$resources = array_values(array_filter(
|
||||||
|
AdminAuthorizationRegistry::resources(),
|
||||||
|
static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.currencies.'),
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($resources as $resource) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')
|
||||||
|
->where('code', $resource['code'])
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'module_code' => $resource['module_code'],
|
||||||
|
'name' => $resource['name'],
|
||||||
|
'http_method' => $resource['http_method'],
|
||||||
|
'uri_pattern' => $resource['uri_pattern'],
|
||||||
|
'route_name' => $resource['route_name'],
|
||||||
|
'auth_mode' => $resource['auth_mode'],
|
||||||
|
'is_audit_required' => $resource['is_audit_required'],
|
||||||
|
'status' => 1,
|
||||||
|
'meta_json' => null,
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($resourceId === null) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')->insertGetId($payload + [
|
||||||
|
'code' => $resource['code'],
|
||||||
|
'created_at' => $now,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
DB::table('admin_api_resources')
|
||||||
|
->where('id', (int) $resourceId)
|
||||||
|
->update($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')
|
||||||
|
->where('api_resource_id', (int) $resourceId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach ($resource['permission_codes'] as $permissionCode) {
|
||||||
|
$menuActionId = $menuActionIds[$permissionCode] ?? null;
|
||||||
|
if ($menuActionId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')->insert([
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
'menu_action_id' => (int) $menuActionId,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleResourceRows = DB::table('admin_role_menu_actions as rma')
|
||||||
|
->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id')
|
||||||
|
->join('admin_api_resources as ar', 'ar.id', '=', 'arb.api_resource_id')
|
||||||
|
->whereIn('ar.code', array_column($resources, 'code'))
|
||||||
|
->select('rma.role_id', 'arb.api_resource_id')
|
||||||
|
->distinct()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($roleResourceRows as $row) {
|
||||||
|
DB::table('admin_role_api_resources')->updateOrInsert([
|
||||||
|
'role_id' => (int) $row->role_id,
|
||||||
|
'api_resource_id' => (int) $row->api_resource_id,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$resourceCodes = ['admin.currencies.index', 'admin.currencies.store', 'admin.currencies.update'];
|
||||||
|
|
||||||
|
$resourceIds = DB::table('admin_api_resources')
|
||||||
|
->whereIn('code', $resourceCodes)
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($resourceIds === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_role_api_resources')
|
||||||
|
->whereIn('api_resource_id', $resourceIds)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')
|
||||||
|
->whereIn('api_resource_id', $resourceIds)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('admin_api_resources')
|
||||||
|
->whereIn('id', $resourceIds)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
|
||||||
|
|
||||||
|
$resource = collect(AdminAuthorizationRegistry::resources())
|
||||||
|
->first(static fn (array $item): bool => $item['code'] === 'admin.currencies.destroy');
|
||||||
|
|
||||||
|
if ($resource === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resourceId = DB::table('admin_api_resources')
|
||||||
|
->where('code', $resource['code'])
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'module_code' => $resource['module_code'],
|
||||||
|
'name' => $resource['name'],
|
||||||
|
'http_method' => $resource['http_method'],
|
||||||
|
'uri_pattern' => $resource['uri_pattern'],
|
||||||
|
'route_name' => $resource['route_name'],
|
||||||
|
'auth_mode' => $resource['auth_mode'],
|
||||||
|
'is_audit_required' => $resource['is_audit_required'],
|
||||||
|
'status' => 1,
|
||||||
|
'meta_json' => null,
|
||||||
|
'updated_at' => $now,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($resourceId === null) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')->insertGetId($payload + [
|
||||||
|
'code' => $resource['code'],
|
||||||
|
'created_at' => $now,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
DB::table('admin_api_resources')
|
||||||
|
->where('id', (int) $resourceId)
|
||||||
|
->update($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')
|
||||||
|
->where('api_resource_id', (int) $resourceId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach ($resource['permission_codes'] as $permissionCode) {
|
||||||
|
$menuActionId = $menuActionIds[$permissionCode] ?? null;
|
||||||
|
if ($menuActionId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')->insert([
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
'menu_action_id' => (int) $menuActionId,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleResourceRows = DB::table('admin_role_menu_actions as rma')
|
||||||
|
->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id')
|
||||||
|
->where('arb.api_resource_id', (int) $resourceId)
|
||||||
|
->select('rma.role_id')
|
||||||
|
->distinct()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($roleResourceRows as $row) {
|
||||||
|
DB::table('admin_role_api_resources')->updateOrInsert([
|
||||||
|
'role_id' => (int) $row->role_id,
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$resourceId = DB::table('admin_api_resources')
|
||||||
|
->where('code', 'admin.currencies.destroy')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($resourceId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_role_api_resources')
|
||||||
|
->where('api_resource_id', (int) $resourceId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')
|
||||||
|
->where('api_resource_id', (int) $resourceId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('admin_api_resources')
|
||||||
|
->where('id', (int) $resourceId)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$serviceMenuId = (int) DB::table('admin_menus')->where('code', 'service')->value('id');
|
||||||
|
$manageActionId = (int) DB::table('admin_action_catalog')->where('code', 'manage')->value('id');
|
||||||
|
|
||||||
|
if ($serviceMenuId > 0) {
|
||||||
|
DB::table('admin_menus')->updateOrInsert(
|
||||||
|
['code' => 'service.currency'],
|
||||||
|
[
|
||||||
|
'parent_id' => $serviceMenuId,
|
||||||
|
'menu_type' => 'page',
|
||||||
|
'name' => '币种管理',
|
||||||
|
'path' => '/admin/settings/currencies',
|
||||||
|
'route_name' => 'admin.settings.currencies',
|
||||||
|
'component' => 'settings/currencies',
|
||||||
|
'icon' => null,
|
||||||
|
'active_menu_code' => null,
|
||||||
|
'sort_order' => 70,
|
||||||
|
'is_visible' => false,
|
||||||
|
'is_cache' => false,
|
||||||
|
'is_external' => false,
|
||||||
|
'status' => 1,
|
||||||
|
'meta_json' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currencyMenuId = (int) DB::table('admin_menus')->where('code', 'service.currency')->value('id');
|
||||||
|
if ($currencyMenuId > 0 && $manageActionId > 0) {
|
||||||
|
DB::table('admin_menu_actions')->updateOrInsert(
|
||||||
|
['permission_code' => 'service.currency.manage'],
|
||||||
|
[
|
||||||
|
'menu_id' => $currencyMenuId,
|
||||||
|
'action_id' => $manageActionId,
|
||||||
|
'name' => '币种管理',
|
||||||
|
'status' => 1,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('admin_permissions')) {
|
||||||
|
DB::table('admin_permissions')->updateOrInsert(
|
||||||
|
['slug' => 'prd.currency.manage'],
|
||||||
|
[
|
||||||
|
'name' => '币种管理·可管理',
|
||||||
|
'updated_at' => $now,
|
||||||
|
'created_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currencyActionId = DB::table('admin_menu_actions')
|
||||||
|
->where('permission_code', 'service.currency.manage')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($currencyActionId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleIds = DB::table('admin_role_legacy_permissions')
|
||||||
|
->where('permission_slug', 'prd.users.manage')
|
||||||
|
->pluck('role_id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
foreach ($roleIds as $roleId) {
|
||||||
|
DB::table('admin_role_legacy_permissions')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'role_id' => (int) $roleId,
|
||||||
|
'permission_slug' => 'prd.currency.manage',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
DB::table('admin_role_menu_actions')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'role_id' => (int) $roleId,
|
||||||
|
'menu_action_id' => (int) $currencyActionId,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currencyResourceIds = DB::table('admin_api_resources')
|
||||||
|
->whereIn('code', [
|
||||||
|
'admin.currencies.index',
|
||||||
|
'admin.currencies.store',
|
||||||
|
'admin.currencies.update',
|
||||||
|
'admin.currencies.destroy',
|
||||||
|
])
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
foreach ($currencyResourceIds as $resourceId) {
|
||||||
|
DB::table('admin_api_resource_bindings')
|
||||||
|
->where('api_resource_id', (int) $resourceId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')->insert([
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
'menu_action_id' => (int) $currencyActionId,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleResourceRows = DB::table('admin_role_menu_actions as rma')
|
||||||
|
->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id')
|
||||||
|
->whereIn('arb.api_resource_id', $currencyResourceIds)
|
||||||
|
->select('rma.role_id', 'arb.api_resource_id')
|
||||||
|
->distinct()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($roleResourceRows as $row) {
|
||||||
|
DB::table('admin_role_api_resources')->updateOrInsert([
|
||||||
|
'role_id' => (int) $row->role_id,
|
||||||
|
'api_resource_id' => (int) $row->api_resource_id,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// 不自动回滚线上角色与资源绑定,避免误删已调整的授权。
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::table('admin_menus')
|
||||||
|
->where('code', 'service.currency')
|
||||||
|
->update([
|
||||||
|
'path' => '/admin/currencies',
|
||||||
|
'route_name' => 'admin.currencies',
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::table('admin_menus')
|
||||||
|
->where('code', 'service.currency')
|
||||||
|
->update([
|
||||||
|
'path' => '/admin/settings/currencies',
|
||||||
|
'route_name' => 'admin.settings.currencies',
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -66,7 +66,6 @@ final class AdminRbacAndUserSeeder extends Seeder
|
|||||||
'prd.draw_result.manage',
|
'prd.draw_result.manage',
|
||||||
'prd.payout.review',
|
'prd.payout.review',
|
||||||
'prd.wallet_reconcile.view',
|
'prd.wallet_reconcile.view',
|
||||||
'prd.report.risk',
|
|
||||||
'prd.audit.self',
|
'prd.audit.self',
|
||||||
'prd.player_freeze.manage',
|
'prd.player_freeze.manage',
|
||||||
]);
|
]);
|
||||||
@@ -84,7 +83,6 @@ final class AdminRbacAndUserSeeder extends Seeder
|
|||||||
'prd.payout.view',
|
'prd.payout.view',
|
||||||
'prd.wallet_reconcile.manage',
|
'prd.wallet_reconcile.manage',
|
||||||
'prd.wallet_adjust.manage',
|
'prd.wallet_adjust.manage',
|
||||||
'prd.report.finance',
|
|
||||||
'prd.audit.finance',
|
'prd.audit.finance',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -96,7 +94,6 @@ final class AdminRbacAndUserSeeder extends Seeder
|
|||||||
'prd.users.view_cs',
|
'prd.users.view_cs',
|
||||||
'prd.draw_result.view',
|
'prd.draw_result.view',
|
||||||
'prd.wallet_reconcile.view_cs',
|
'prd.wallet_reconcile.view_cs',
|
||||||
'prd.report.player',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$username = 'admin';
|
$username = 'admin';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev": "vite"
|
"dev": "vite --host \"${VITE_HOST:-0.0.0.0}\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ Route::prefix('v1')->group(function (): void {
|
|||||||
require __DIR__.'/api/v1/admin/core.php';
|
require __DIR__.'/api/v1/admin/core.php';
|
||||||
require __DIR__.'/api/v1/admin/wallet.php';
|
require __DIR__.'/api/v1/admin/wallet.php';
|
||||||
require __DIR__.'/api/v1/admin/player.php';
|
require __DIR__.'/api/v1/admin/player.php';
|
||||||
|
require __DIR__.'/api/v1/admin/currency.php';
|
||||||
require __DIR__.'/api/v1/admin/ticket.php';
|
require __DIR__.'/api/v1/admin/ticket.php';
|
||||||
require __DIR__.'/api/v1/admin/draw.php';
|
require __DIR__.'/api/v1/admin/draw.php';
|
||||||
require __DIR__.'/api/v1/admin/jackpot.php';
|
require __DIR__.'/api/v1/admin/jackpot.php';
|
||||||
require __DIR__.'/api/v1/admin/config.php';
|
require __DIR__.'/api/v1/admin/config.php';
|
||||||
require __DIR__.'/api/v1/admin/report.php';
|
|
||||||
require __DIR__.'/api/v1/admin/user.php';
|
require __DIR__.'/api/v1/admin/user.php';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
22
routes/api/v1/admin/currency.php
Normal file
22
routes/api/v1/admin/currency.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Currency\AdminCurrencyIndexController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Currency\AdminCurrencyStoreController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Currency\AdminCurrencyUpdateController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Currency\AdminCurrencyDestroyController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员币种主数据路由。
|
||||||
|
*/
|
||||||
|
Route::middleware('admin.api-resource')
|
||||||
|
->group(function (): void {
|
||||||
|
Route::get('currencies', AdminCurrencyIndexController::class)
|
||||||
|
->name('api.v1.admin.currencies.index');
|
||||||
|
Route::post('currencies', AdminCurrencyStoreController::class)
|
||||||
|
->name('api.v1.admin.currencies.store');
|
||||||
|
Route::put('currencies/{currency:code}', AdminCurrencyUpdateController::class)
|
||||||
|
->name('api.v1.admin.currencies.update');
|
||||||
|
Route::delete('currencies/{currency:code}', AdminCurrencyDestroyController::class)
|
||||||
|
->name('api.v1.admin.currencies.destroy');
|
||||||
|
});
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use App\Http\Controllers\Api\V1\Admin\Reports\ReportJobDownloadController;
|
|
||||||
use App\Http\Controllers\Api\V1\Admin\Reports\ReportJobShowController;
|
|
||||||
use App\Http\Controllers\Api\V1\Admin\Reports\ReportJobIndexController;
|
|
||||||
use App\Http\Controllers\Api\V1\Admin\Reports\ReportJobStoreController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理员报表路由。
|
|
||||||
*/
|
|
||||||
Route::middleware('admin.api-resource')
|
|
||||||
->group(function (): void {
|
|
||||||
Route::get('report-jobs', ReportJobIndexController::class)
|
|
||||||
->name('api.v1.admin.report-jobs.index');
|
|
||||||
Route::post('report-jobs', ReportJobStoreController::class)
|
|
||||||
->name('api.v1.admin.report-jobs.store');
|
|
||||||
Route::get('report-jobs/{report_job}', ReportJobShowController::class)
|
|
||||||
->name('api.v1.admin.report-jobs.show');
|
|
||||||
Route::get('report-jobs/{report_job}/download', ReportJobDownloadController::class)
|
|
||||||
->name('api.v1.admin.report-jobs.download');
|
|
||||||
});
|
|
||||||
@@ -5,6 +5,7 @@ use App\Http\Controllers\Api\V1\HealthController;
|
|||||||
use App\Http\Controllers\Api\V1\Draw\DrawCurrentController;
|
use App\Http\Controllers\Api\V1\Draw\DrawCurrentController;
|
||||||
use App\Http\Controllers\Api\V1\Draw\DrawResultShowController;
|
use App\Http\Controllers\Api\V1\Draw\DrawResultShowController;
|
||||||
use App\Http\Controllers\Api\V1\Draw\DrawResultsIndexController;
|
use App\Http\Controllers\Api\V1\Draw\DrawResultsIndexController;
|
||||||
|
use App\Http\Controllers\Api\V1\Currency\CurrencyIndexController;
|
||||||
use App\Http\Controllers\Api\V1\Jackpot\JackpotSummaryController;
|
use App\Http\Controllers\Api\V1\Jackpot\JackpotSummaryController;
|
||||||
use App\Http\Controllers\Api\V1\Play\PlayEffectiveCatalogController;
|
use App\Http\Controllers\Api\V1\Play\PlayEffectiveCatalogController;
|
||||||
use App\Http\Controllers\Api\V1\Player\PingController as PlayerPingController;
|
use App\Http\Controllers\Api\V1\Player\PingController as PlayerPingController;
|
||||||
@@ -16,6 +17,9 @@ use App\Http\Controllers\Api\V1\Player\PingController as PlayerPingController;
|
|||||||
// 健康检查
|
// 健康检查
|
||||||
Route::get('health', HealthController::class)->name('api.v1.health');
|
Route::get('health', HealthController::class)->name('api.v1.health');
|
||||||
|
|
||||||
|
// 币种主数据(公开,只给玩家端展示/金额精度使用)
|
||||||
|
Route::get('currencies', CurrencyIndexController::class)->name('api.v1.currencies.index');
|
||||||
|
|
||||||
// 开奖相关(公开)
|
// 开奖相关(公开)
|
||||||
Route::get('draw/current', DrawCurrentController::class)->name('api.v1.draw.current');
|
Route::get('draw/current', DrawCurrentController::class)->name('api.v1.draw.current');
|
||||||
Route::get('draw/results', DrawResultsIndexController::class)->name('api.v1.draw.results');
|
Route::get('draw/results', DrawResultsIndexController::class)->name('api.v1.draw.results');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Database\Seeders\AdminRbacAndUserSeeder;
|
use Database\Seeders\AdminRbacAndUserSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
@@ -23,6 +23,33 @@ test('admin authorization audit passes on the default authorization catalog', fu
|
|||||||
->assertExitCode(0);
|
->assertExitCode(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('admin authorization sync can repair registry-backed api resources and pass audit', function (): void {
|
||||||
|
DB::table('admin_api_resources')
|
||||||
|
->where('code', 'admin.currencies.destroy')
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$this->artisan('lottery:admin-auth-audit')
|
||||||
|
->expectsOutputToContain('admin.currencies.destroy')
|
||||||
|
->assertExitCode(1);
|
||||||
|
|
||||||
|
$this->artisan('lottery:admin-auth-sync --audit')
|
||||||
|
->expectsOutputToContain('Admin authorization synced')
|
||||||
|
->expectsOutputToContain('Admin authorization audit passed.')
|
||||||
|
->assertExitCode(0);
|
||||||
|
|
||||||
|
$resourceId = DB::table('admin_api_resources')
|
||||||
|
->where('code', 'admin.currencies.destroy')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
expect($resourceId)->not->toBeNull();
|
||||||
|
|
||||||
|
$bindingCount = DB::table('admin_api_resource_bindings')
|
||||||
|
->where('api_resource_id', (int) $resourceId)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
expect($bindingCount)->toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('admin authorization audit detects role api resource drift', function (): void {
|
test('admin authorization audit detects role api resource drift', function (): void {
|
||||||
$this->seed(AdminRbacAndUserSeeder::class);
|
$this->seed(AdminRbacAndUserSeeder::class);
|
||||||
|
|
||||||
|
|||||||
169
tests/Feature/AdminCurrencyApiTest.php
Normal file
169
tests/Feature/AdminCurrencyApiTest.php
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Currency;
|
||||||
|
use App\Models\OddsItem;
|
||||||
|
use App\Models\PlayType;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\JackpotPool;
|
||||||
|
use App\Models\OddsVersion;
|
||||||
|
use App\Lottery\ConfigVersionStatus;
|
||||||
|
use Database\Seeders\CurrencySeeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(CurrencySeeder::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
function mintCurrencyAdminToken(): string
|
||||||
|
{
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'currency_admin',
|
||||||
|
'name' => 'Currency Admin',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($admin);
|
||||||
|
|
||||||
|
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('admin can list currencies', function (): void {
|
||||||
|
$token = mintCurrencyAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/currencies')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.items.0.code', 'NPR');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin can create currency and normalize code', function (): void {
|
||||||
|
$token = mintCurrencyAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/currencies', [
|
||||||
|
'code' => 'eur',
|
||||||
|
'name' => 'Euro',
|
||||||
|
'decimal_places' => 2,
|
||||||
|
'is_enabled' => true,
|
||||||
|
'is_bettable' => true,
|
||||||
|
])
|
||||||
|
->assertStatus(201)
|
||||||
|
->assertJsonPath('data.code', 'EUR')
|
||||||
|
->assertJsonPath('data.is_bettable', true);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('currencies', [
|
||||||
|
'code' => 'EUR',
|
||||||
|
'name' => 'Euro',
|
||||||
|
'is_enabled' => true,
|
||||||
|
'is_bettable' => true,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disabling currency forces bettable off', function (): void {
|
||||||
|
$token = mintCurrencyAdminToken();
|
||||||
|
$currency = Currency::query()->where('code', 'USD')->firstOrFail();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/currencies/'.$currency->code, [
|
||||||
|
'is_enabled' => false,
|
||||||
|
'is_bettable' => true,
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.is_enabled', false)
|
||||||
|
->assertJsonPath('data.is_bettable', false);
|
||||||
|
|
||||||
|
$currency->refresh();
|
||||||
|
expect($currency->is_enabled)->toBeFalse()
|
||||||
|
->and($currency->is_bettable)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enabling currency as bettable bootstraps odds items and jackpot pool', function (): void {
|
||||||
|
$token = mintCurrencyAdminToken();
|
||||||
|
$currency = Currency::query()->where('code', 'USD')->firstOrFail();
|
||||||
|
|
||||||
|
PlayType::query()->create([
|
||||||
|
'play_code' => 'STRAIGHT',
|
||||||
|
'category' => '4d',
|
||||||
|
'dimension' => 4,
|
||||||
|
'bet_mode' => 'single',
|
||||||
|
'display_name_zh' => '直选',
|
||||||
|
'display_name_en' => 'Straight',
|
||||||
|
'display_name_ne' => 'सिधा',
|
||||||
|
'is_enabled' => true,
|
||||||
|
'sort_order' => 10,
|
||||||
|
'supports_multi_number' => false,
|
||||||
|
'reserved_rule_json' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$version = OddsVersion::query()->create([
|
||||||
|
'version_no' => 1,
|
||||||
|
'status' => ConfigVersionStatus::Active->value,
|
||||||
|
'updated_by' => null,
|
||||||
|
'reason' => 'seed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
OddsItem::query()->create([
|
||||||
|
'version_id' => $version->id,
|
||||||
|
'play_code' => 'STRAIGHT',
|
||||||
|
'prize_scope' => 'first',
|
||||||
|
'odds_value' => 250000,
|
||||||
|
'rebate_rate' => 0.05,
|
||||||
|
'commission_rate' => 0.01,
|
||||||
|
'currency_code' => 'NPR',
|
||||||
|
'extra_config_json' => ['sample' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
JackpotPool::query()->updateOrCreate([
|
||||||
|
'currency_code' => 'NPR',
|
||||||
|
], [
|
||||||
|
'current_amount' => 12345,
|
||||||
|
'contribution_rate' => '0.0300',
|
||||||
|
'trigger_threshold' => 200000000,
|
||||||
|
'payout_rate' => '0.4000',
|
||||||
|
'force_trigger_draw_gap' => 88,
|
||||||
|
'min_bet_amount' => 200,
|
||||||
|
'combo_trigger_play_codes' => ['STRAIGHT'],
|
||||||
|
'status' => 1,
|
||||||
|
'last_trigger_draw_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/currencies/'.$currency->code, [
|
||||||
|
'is_bettable' => true,
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.is_bettable', true);
|
||||||
|
|
||||||
|
$currency->refresh();
|
||||||
|
expect($currency->is_bettable)->toBeTrue();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('jackpot_pools', [
|
||||||
|
'currency_code' => 'USD',
|
||||||
|
'current_amount' => 0,
|
||||||
|
'contribution_rate' => '0.0300',
|
||||||
|
'trigger_threshold' => 200000000,
|
||||||
|
'payout_rate' => '0.4000',
|
||||||
|
'force_trigger_draw_gap' => 88,
|
||||||
|
'min_bet_amount' => 200,
|
||||||
|
'status' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('odds_items', [
|
||||||
|
'version_id' => $version->id,
|
||||||
|
'play_code' => 'STRAIGHT',
|
||||||
|
'prize_scope' => 'first',
|
||||||
|
'currency_code' => 'USD',
|
||||||
|
'odds_value' => 250000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('odds_items', [
|
||||||
|
'version_id' => $version->id,
|
||||||
|
'play_code' => 'STRAIGHT',
|
||||||
|
'prize_scope' => 'second',
|
||||||
|
'currency_code' => 'USD',
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Models\AuditLog;
|
|
||||||
use App\Models\AdminUser;
|
|
||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
use App\Models\PlayerWallet;
|
use App\Models\AuditLog;
|
||||||
use App\Models\AdminRole;
|
use App\Models\AdminRole;
|
||||||
use App\Services\AuditLogger;
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\PlayerWallet;
|
||||||
|
use Database\Seeders\CurrencySeeder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(CurrencySeeder::class);
|
||||||
|
});
|
||||||
|
|
||||||
function playerManageAdminToken(): string
|
function playerManageAdminToken(): string
|
||||||
{
|
{
|
||||||
$admin = AdminUser::query()->create([
|
$admin = AdminUser::query()->create([
|
||||||
@@ -180,3 +184,34 @@ test('player manage permission gates write and freeze APIs separately from view
|
|||||||
->getJson('/api/v1/admin/players/'.$player->id.'/ticket-items')
|
->getJson('/api/v1/admin/players/'.$player->id.'/ticket-items')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('admin can update player default currency and validation rejects unknown code', function (): void {
|
||||||
|
$player = Player::query()->create([
|
||||||
|
'site_code' => 'main',
|
||||||
|
'site_player_id' => 'currency-1',
|
||||||
|
'username' => 'currency_user',
|
||||||
|
'nickname' => 'Currency',
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = playerManageAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/players/'.$player->id, [
|
||||||
|
'default_currency' => 'usd',
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.default_currency', 'USD');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('players', [
|
||||||
|
'id' => $player->id,
|
||||||
|
'default_currency' => 'USD',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/players/'.$player->id, [
|
||||||
|
'default_currency' => 'ABC',
|
||||||
|
])
|
||||||
|
->assertStatus(422);
|
||||||
|
});
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ test('permission catalog groups permissions by admin navigation order', function
|
|||||||
'admin_users',
|
'admin_users',
|
||||||
'admin_roles',
|
'admin_roles',
|
||||||
'players',
|
'players',
|
||||||
|
'currencies',
|
||||||
'wallet',
|
'wallet',
|
||||||
'draws',
|
'draws',
|
||||||
'config',
|
'config',
|
||||||
|
|||||||
41
tests/Feature/PublicCurrencyIndexTest.php
Normal file
41
tests/Feature/PublicCurrencyIndexTest.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Currency;
|
||||||
|
use App\Lottery\ErrorCode;
|
||||||
|
use Database\Seeders\CurrencySeeder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(CurrencySeeder::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('public currency index returns enabled currencies with decimal places', function () {
|
||||||
|
Currency::query()->create([
|
||||||
|
'code' => 'JPY',
|
||||||
|
'name' => 'Japanese Yen',
|
||||||
|
'decimal_places' => 0,
|
||||||
|
'is_enabled' => true,
|
||||||
|
'is_bettable' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Currency::query()->create([
|
||||||
|
'code' => 'TEST',
|
||||||
|
'name' => 'Disabled Currency',
|
||||||
|
'decimal_places' => 4,
|
||||||
|
'is_enabled' => false,
|
||||||
|
'is_bettable' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->getJson('/api/v1/currencies');
|
||||||
|
|
||||||
|
$response->assertOk()
|
||||||
|
->assertJsonPath('code', ErrorCode::Success->value)
|
||||||
|
->assertJsonCount(3, 'data.items')
|
||||||
|
->assertJsonPath('data.items.0.code', 'NPR')
|
||||||
|
->assertJsonPath('data.items.0.decimal_places', 2)
|
||||||
|
->assertJsonPath('data.items.1.code', 'JPY')
|
||||||
|
->assertJsonPath('data.items.1.decimal_places', 0)
|
||||||
|
->assertJsonPath('data.items.2.code', 'USD');
|
||||||
|
});
|
||||||
@@ -3,10 +3,15 @@
|
|||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
use App\Models\PlayerWallet;
|
use App\Models\PlayerWallet;
|
||||||
|
use Database\Seeders\CurrencySeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(CurrencySeeder::class);
|
||||||
|
});
|
||||||
|
|
||||||
test('wallet balance creates lottery wallet row and returns zeros', function () {
|
test('wallet balance creates lottery wallet row and returns zeros', function () {
|
||||||
$player = Player::query()->create([
|
$player = Player::query()->create([
|
||||||
'site_code' => 'test',
|
'site_code' => 'test',
|
||||||
@@ -49,3 +54,19 @@ test('wallet balance rejects illegal currency query', function () {
|
|||||||
->assertStatus(400)
|
->assertStatus(400)
|
||||||
->assertJsonPath('code', ErrorCode::WalletInvalidCurrency->value);
|
->assertJsonPath('code', ErrorCode::WalletInvalidCurrency->value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('wallet balance rejects currency that is not configured or disabled', function () {
|
||||||
|
$player = Player::query()->create([
|
||||||
|
'site_code' => 'test',
|
||||||
|
'site_player_id' => 'p3',
|
||||||
|
'username' => null,
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||||
|
->getJson('/api/v1/wallet/balance?currency=ABC')
|
||||||
|
->assertStatus(400)
|
||||||
|
->assertJsonPath('code', ErrorCode::WalletInvalidCurrency->value);
|
||||||
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default defineConfig({
|
|||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
|
host: process.env.VITE_HOST ?? '0.0.0.0',
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/storage/framework/views/**'],
|
ignored: ['**/storage/framework/views/**'],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user