From d5232c756f224c0a0140882cd1ecca6386b590bf Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 1 Jun 2026 16:53:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=8E=A9=E5=AE=B6?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=92=8C=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在多个控制器中更新玩家相关数据的查询,新增 'nickname' 字段以增强玩家信息的完整性。 - 在 AdminDashboardSnapshotBuilder 中引入平台风险统计,提供锁定金额和使用百分比的概览。 - 更新 AdminReportQueryService 以返回更详细的统计数据,包括总投注、总中奖和总派彩金额。 - 增强测试用例以验证新增字段和统计功能的准确性。 --- ...dminJackpotContributionIndexController.php | 7 ++- .../Player/AdminPlayerIndexController.php | 3 +- .../AdminSettlementBatchDetailsController.php | 6 +- .../WalletTransactionListController.php | 1 + .../Admin/AdminDashboardSnapshotBuilder.php | 55 ++++++++++++++++--- .../Admin/AdminReportQueryService.php | 8 ++- app/Services/PlayerTokenResolver.php | 4 +- .../PlayerAutoRegistrationDefaults.php | 48 ++++++++++++++++ tests/Feature/PlayerFoundationTest.php | 11 +++- 9 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 app/Support/PlayerAutoRegistrationDefaults.php diff --git a/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php b/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php index 5e73ae0..671e540 100644 --- a/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php @@ -19,7 +19,7 @@ final class AdminJackpotContributionIndexController extends Controller $drawNo = trim((string) $request->query('draw_no', '')); $q = JackpotContribution::query() - ->with(['draw:id,draw_no', 'pool:id,currency_code', 'player:id,username,site_player_id', 'ticketItem:id,ticket_no']) + ->with(['draw:id,draw_no', 'pool:id,currency_code', 'player:id,site_code,username,nickname,site_player_id', 'ticketItem:id,ticket_no']) ->orderByDesc('id'); if ($drawNo !== '') { @@ -35,7 +35,10 @@ final class AdminJackpotContributionIndexController extends Controller 'jackpot_pool_id' => (int) $r->jackpot_pool_id, 'currency_code' => $r->pool?->currency_code, 'player_id' => (int) $r->player_id, - 'player_username' => $r->player?->username, + 'site_code' => $r->player?->site_code, + 'site_player_id' => $r->player?->site_player_id, + 'username' => $r->player?->username, + 'nickname' => $r->player?->nickname, 'ticket_item_id' => $r->ticket_item_id !== null ? (int) $r->ticket_item_id : null, 'ticket_no' => $r->ticketItem?->ticket_no, 'contribution_amount' => (int) $r->contribution_amount, diff --git a/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerIndexController.php b/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerIndexController.php index 4adc271..5888cc2 100644 --- a/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Player/AdminPlayerIndexController.php @@ -38,8 +38,7 @@ final class AdminPlayerIndexController extends Controller $term = '%'.addcslashes($keyword, '%_\\').'%'; $q->where(static function ($sub) use ($term): void { $sub->where('site_player_id', 'like', $term) - ->orWhere('username', 'like', $term) - ->orWhere('nickname', 'like', $term); + ->orWhere('username', 'like', $term); }); } diff --git a/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php b/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php index 8a58fd2..5e8c533 100644 --- a/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php +++ b/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php @@ -22,7 +22,7 @@ final class AdminSettlementBatchDetailsController extends Controller ->where('settlement_batch_id', $batch->id) ->with([ 'ticketItem:id,ticket_no,play_code,player_id', - 'ticketItem.player:id,username,site_player_id', + 'ticketItem.player:id,site_code,username,nickname,site_player_id', 'ticketItem.order:id,currency_code', ]) ->orderBy('id') @@ -41,8 +41,10 @@ final class AdminSettlementBatchDetailsController extends Controller 'play_code' => $item?->play_code, 'currency_code' => $order?->currency_code, 'player_id' => $item?->player_id, - 'player_username' => $player?->username, + 'site_code' => $player?->site_code, 'site_player_id' => $player?->site_player_id, + 'username' => $player?->username, + 'nickname' => $player?->nickname, 'matched_prize_tier' => $row->matched_prize_tier, 'win_amount' => (int) $row->win_amount, 'jackpot_allocation_amount' => (int) $row->jackpot_allocation_amount, diff --git a/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php b/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php index d5db345..fd88991 100644 --- a/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php +++ b/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php @@ -122,6 +122,7 @@ final class WalletTransactionListController extends Controller 'site_code' => $p?->site_code, 'site_player_id' => $p?->site_player_id, 'username' => $p?->username, + 'nickname' => $p?->nickname, 'wallet_id' => $t->wallet_id, 'biz_type' => $t->biz_type, 'biz_no' => $t->biz_no, diff --git a/app/Services/Admin/AdminDashboardSnapshotBuilder.php b/app/Services/Admin/AdminDashboardSnapshotBuilder.php index 2094654..bb0aa3c 100644 --- a/app/Services/Admin/AdminDashboardSnapshotBuilder.php +++ b/app/Services/Admin/AdminDashboardSnapshotBuilder.php @@ -41,6 +41,7 @@ final class AdminDashboardSnapshotBuilder 'finance' => null, 'draw' => null, 'risk' => null, + 'platform_risk' => null, 'result_batch_queue' => null, 'abnormal_transfer_total' => null, 'warnings' => [], @@ -50,6 +51,14 @@ final class AdminDashboardSnapshotBuilder ], ]; + if ($canDraw) { + $this->fillPlatformOverview($out); + } + + if ($canWallet) { + $out['abnormal_transfer_total'] = $this->abnormalTransferTotal(); + } + if ($hall === null) { return $out; } @@ -72,21 +81,23 @@ final class AdminDashboardSnapshotBuilder ]; if ($canDraw) { - $out['today_finance'] = $this->todayFinanceSummary(); - $out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals(); $out['finance'] = $this->financeSummary($draw); $out['draw'] = $this->drawPanel($draw); $out['risk'] = $this->riskPanel($draw); - $out['result_batch_queue'] = $this->resultBatchQueue(); - } - - if ($canWallet) { - $out['abnormal_transfer_total'] = $this->abnormalTransferTotal(); } return $out; } + /** @param array $out */ + private function fillPlatformOverview(array &$out): void + { + $out['today_finance'] = $this->todayFinanceSummary(); + $out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals(); + $out['platform_risk'] = $this->platformRiskSummary(); + $out['result_batch_queue'] = $this->resultBatchQueue(); + } + private function canDrawFinanceAndRisk(AdminUser $admin): bool { return $admin->hasAdminPermission('prd.dashboard.view') @@ -195,7 +206,14 @@ final class AdminDashboardSnapshotBuilder /** * 全站待审核开奖批次(首页「待审核开奖」卡片用,不限于大厅当前期)。 * - * @return array{pending_review_total: int, pending_draw_count: int, first_pending_draw_id: int|null, first_pending_batch_id: int|null} + * @return array{ + * pending_review_total: int, + * pending_draw_count: int, + * published_total: int, + * batch_total: int, + * first_pending_draw_id: int|null, + * first_pending_batch_id: int|null + * } */ private function resultBatchQueue(): array { @@ -215,11 +233,32 @@ final class AdminDashboardSnapshotBuilder return [ 'pending_review_total' => $pendingTotal, 'pending_draw_count' => $pendingDrawCount, + 'published_total' => (int) DrawResultBatch::query() + ->where('status', DrawResultBatchStatus::Published->value) + ->count(), + 'batch_total' => (int) DrawResultBatch::query()->count(), 'first_pending_draw_id' => $firstPending !== null ? (int) $firstPending->draw_id : null, 'first_pending_batch_id' => $firstPending !== null ? (int) $firstPending->id : null, ]; } + /** @return array{locked_amount: int, cap_amount: int, usage_percent: float} */ + private function platformRiskSummary(): array + { + $sums = RiskPool::query() + ->selectRaw('COALESCE(SUM(locked_amount), 0) as locked, COALESCE(SUM(total_cap_amount), 0) as cap') + ->first(); + + $locked = (int) (($sums?->locked) ?? 0); + $cap = (int) (($sums?->cap) ?? 0); + + return [ + 'locked_amount' => $locked, + 'cap_amount' => $cap, + 'usage_percent' => $cap > 0 ? round(($locked / $cap) * 100, 4) : 0.0, + ]; + } + /** @return array */ private function drawPanel(Draw $draw): array { diff --git a/app/Services/Admin/AdminReportQueryService.php b/app/Services/Admin/AdminReportQueryService.php index 9b71ede..465645b 100644 --- a/app/Services/Admin/AdminReportQueryService.php +++ b/app/Services/Admin/AdminReportQueryService.php @@ -291,7 +291,9 @@ final class AdminReportQueryService $payoutAgg = DB::table('ticket_items') ->selectRaw('COALESCE(SUM(win_amount), 0) as win_minor, COALESCE(SUM(jackpot_win_amount), 0) as jackpot_minor') ->first(); - $totalPayoutMinor = (int) ($payoutAgg->win_minor ?? 0) + (int) ($payoutAgg->jackpot_minor ?? 0); + $totalWinMinor = (int) ($payoutAgg->win_minor ?? 0); + $totalJackpotMinor = (int) ($payoutAgg->jackpot_minor ?? 0); + $totalPayoutMinor = $totalWinMinor + $totalJackpotMinor; $activity = DB::table('draws as d') ->join('ticket_orders as o', 'o.draw_id', '=', 'd.id') @@ -312,8 +314,12 @@ final class AdminReportQueryService return [ 'currency_code' => $currencyCode !== '' ? $currencyCode : null, 'total_bet_minor' => $totalBetMinor, + 'total_win_minor' => $totalWinMinor, + 'total_jackpot_minor' => $totalJackpotMinor, 'total_payout_minor' => $totalPayoutMinor, 'approx_house_gross_minor' => $totalBetMinor - $totalPayoutMinor, + 'order_count' => (int) DB::table('ticket_orders')->count(), + 'ticket_item_count' => (int) DB::table('ticket_items')->count(), 'draw_count' => $drawCount, 'business_day_count' => $businessDayCount, 'date_from' => $dateFrom, diff --git a/app/Services/PlayerTokenResolver.php b/app/Services/PlayerTokenResolver.php index f8d84c0..3fcdc43 100644 --- a/app/Services/PlayerTokenResolver.php +++ b/app/Services/PlayerTokenResolver.php @@ -7,6 +7,7 @@ use Firebase\JWT\Key; use App\Models\Player; use App\Lottery\ErrorCode; use Illuminate\Http\Request; +use App\Support\PlayerAutoRegistrationDefaults; use App\Support\PlayerTokenAesUnwrap; use Illuminate\Database\QueryException; use App\Exceptions\PlayerAuthenticationException; @@ -176,8 +177,7 @@ final class PlayerTokenResolver $now = now(); $defaults = [ - 'username' => null, - 'nickname' => null, + ...PlayerAutoRegistrationDefaults::profileFields(), 'default_currency' => LotterySettings::defaultCurrency(), 'status' => self::PLAYER_STATUS_ACTIVE, 'last_login_at' => $now, diff --git a/app/Support/PlayerAutoRegistrationDefaults.php b/app/Support/PlayerAutoRegistrationDefaults.php new file mode 100644 index 0000000..ec3e341 --- /dev/null +++ b/app/Support/PlayerAutoRegistrationDefaults.php @@ -0,0 +1,48 @@ +where('username', $candidate)->exists()) { + return $candidate; + } + } + + return self::DISPLAY_NAME_PREFIX.self::randomSuffix(); + } + + /** + * @return array{username: string, nickname: string} + */ + public static function profileFields(): array + { + $name = self::displayName(); + + return [ + 'username' => $name, + 'nickname' => $name, + ]; + } + + private static function randomSuffix(): string + { + return str_pad((string) random_int(0, 999_999), self::RANDOM_SUFFIX_DIGITS, '0', STR_PAD_LEFT); + } +} diff --git a/tests/Feature/PlayerFoundationTest.php b/tests/Feature/PlayerFoundationTest.php index 53c9bf1..bcdb32b 100644 --- a/tests/Feature/PlayerFoundationTest.php +++ b/tests/Feature/PlayerFoundationTest.php @@ -98,13 +98,20 @@ test('jwt first successful login auto-registers player mapping', function () { 'exp' => $now + 300, ], 'jwt-test-secret', 'HS256'); - $this->withHeader('Authorization', 'Bearer '.$jwt) + $response = $this->withHeader('Authorization', 'Bearer '.$jwt) ->getJson('/api/v1/player/me') ->assertOk() ->assertJsonPath('data.site_player_id', 'brand-new-sso-1') ->assertJsonPath('data.default_currency', 'NPR'); - expect(Player::query()->where('site_player_id', 'brand-new-sso-1')->count())->toBe(1); + $username = $response->json('data.username'); + expect($username)->toMatch('/^nlotto\d{6}$/') + ->and($response->json('data.nickname'))->toBe($username); + + $player = Player::query()->where('site_player_id', 'brand-new-sso-1')->first(); + expect($player)->not->toBeNull() + ->and($player->username)->toBe($username) + ->and($player->nickname)->toBe($username); }); test('player me rejects non-active status with 8005', function () {