feat: 更新玩家信息和统计功能
- 在多个控制器中更新玩家相关数据的查询,新增 'nickname' 字段以增强玩家信息的完整性。 - 在 AdminDashboardSnapshotBuilder 中引入平台风险统计,提供锁定金额和使用百分比的概览。 - 更新 AdminReportQueryService 以返回更详细的统计数据,包括总投注、总中奖和总派彩金额。 - 增强测试用例以验证新增字段和统计功能的准确性。
This commit is contained in:
@@ -19,7 +19,7 @@ final class AdminJackpotContributionIndexController extends Controller
|
|||||||
$drawNo = trim((string) $request->query('draw_no', ''));
|
$drawNo = trim((string) $request->query('draw_no', ''));
|
||||||
|
|
||||||
$q = JackpotContribution::query()
|
$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');
|
->orderByDesc('id');
|
||||||
|
|
||||||
if ($drawNo !== '') {
|
if ($drawNo !== '') {
|
||||||
@@ -35,7 +35,10 @@ final class AdminJackpotContributionIndexController extends Controller
|
|||||||
'jackpot_pool_id' => (int) $r->jackpot_pool_id,
|
'jackpot_pool_id' => (int) $r->jackpot_pool_id,
|
||||||
'currency_code' => $r->pool?->currency_code,
|
'currency_code' => $r->pool?->currency_code,
|
||||||
'player_id' => (int) $r->player_id,
|
'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_item_id' => $r->ticket_item_id !== null ? (int) $r->ticket_item_id : null,
|
||||||
'ticket_no' => $r->ticketItem?->ticket_no,
|
'ticket_no' => $r->ticketItem?->ticket_no,
|
||||||
'contribution_amount' => (int) $r->contribution_amount,
|
'contribution_amount' => (int) $r->contribution_amount,
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ final class AdminPlayerIndexController extends Controller
|
|||||||
$term = '%'.addcslashes($keyword, '%_\\').'%';
|
$term = '%'.addcslashes($keyword, '%_\\').'%';
|
||||||
$q->where(static function ($sub) use ($term): void {
|
$q->where(static function ($sub) use ($term): void {
|
||||||
$sub->where('site_player_id', 'like', $term)
|
$sub->where('site_player_id', 'like', $term)
|
||||||
->orWhere('username', 'like', $term)
|
->orWhere('username', 'like', $term);
|
||||||
->orWhere('nickname', 'like', $term);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ final class AdminSettlementBatchDetailsController extends Controller
|
|||||||
->where('settlement_batch_id', $batch->id)
|
->where('settlement_batch_id', $batch->id)
|
||||||
->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,site_code,username,nickname,site_player_id',
|
||||||
'ticketItem.order:id,currency_code',
|
'ticketItem.order:id,currency_code',
|
||||||
])
|
])
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
@@ -41,8 +41,10 @@ final class AdminSettlementBatchDetailsController extends Controller
|
|||||||
'play_code' => $item?->play_code,
|
'play_code' => $item?->play_code,
|
||||||
'currency_code' => $order?->currency_code,
|
'currency_code' => $order?->currency_code,
|
||||||
'player_id' => $item?->player_id,
|
'player_id' => $item?->player_id,
|
||||||
'player_username' => $player?->username,
|
'site_code' => $player?->site_code,
|
||||||
'site_player_id' => $player?->site_player_id,
|
'site_player_id' => $player?->site_player_id,
|
||||||
|
'username' => $player?->username,
|
||||||
|
'nickname' => $player?->nickname,
|
||||||
'matched_prize_tier' => $row->matched_prize_tier,
|
'matched_prize_tier' => $row->matched_prize_tier,
|
||||||
'win_amount' => (int) $row->win_amount,
|
'win_amount' => (int) $row->win_amount,
|
||||||
'jackpot_allocation_amount' => (int) $row->jackpot_allocation_amount,
|
'jackpot_allocation_amount' => (int) $row->jackpot_allocation_amount,
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ final class WalletTransactionListController extends Controller
|
|||||||
'site_code' => $p?->site_code,
|
'site_code' => $p?->site_code,
|
||||||
'site_player_id' => $p?->site_player_id,
|
'site_player_id' => $p?->site_player_id,
|
||||||
'username' => $p?->username,
|
'username' => $p?->username,
|
||||||
|
'nickname' => $p?->nickname,
|
||||||
'wallet_id' => $t->wallet_id,
|
'wallet_id' => $t->wallet_id,
|
||||||
'biz_type' => $t->biz_type,
|
'biz_type' => $t->biz_type,
|
||||||
'biz_no' => $t->biz_no,
|
'biz_no' => $t->biz_no,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
'finance' => null,
|
'finance' => null,
|
||||||
'draw' => null,
|
'draw' => null,
|
||||||
'risk' => null,
|
'risk' => null,
|
||||||
|
'platform_risk' => null,
|
||||||
'result_batch_queue' => null,
|
'result_batch_queue' => null,
|
||||||
'abnormal_transfer_total' => null,
|
'abnormal_transfer_total' => null,
|
||||||
'warnings' => [],
|
'warnings' => [],
|
||||||
@@ -50,6 +51,14 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($canDraw) {
|
||||||
|
$this->fillPlatformOverview($out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($canWallet) {
|
||||||
|
$out['abnormal_transfer_total'] = $this->abnormalTransferTotal();
|
||||||
|
}
|
||||||
|
|
||||||
if ($hall === null) {
|
if ($hall === null) {
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
@@ -72,21 +81,23 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($canDraw) {
|
if ($canDraw) {
|
||||||
$out['today_finance'] = $this->todayFinanceSummary();
|
|
||||||
$out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals();
|
|
||||||
$out['finance'] = $this->financeSummary($draw);
|
$out['finance'] = $this->financeSummary($draw);
|
||||||
$out['draw'] = $this->drawPanel($draw);
|
$out['draw'] = $this->drawPanel($draw);
|
||||||
$out['risk'] = $this->riskPanel($draw);
|
$out['risk'] = $this->riskPanel($draw);
|
||||||
$out['result_batch_queue'] = $this->resultBatchQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($canWallet) {
|
|
||||||
$out['abnormal_transfer_total'] = $this->abnormalTransferTotal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $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
|
private function canDrawFinanceAndRisk(AdminUser $admin): bool
|
||||||
{
|
{
|
||||||
return $admin->hasAdminPermission('prd.dashboard.view')
|
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
|
private function resultBatchQueue(): array
|
||||||
{
|
{
|
||||||
@@ -215,11 +233,32 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
return [
|
return [
|
||||||
'pending_review_total' => $pendingTotal,
|
'pending_review_total' => $pendingTotal,
|
||||||
'pending_draw_count' => $pendingDrawCount,
|
'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_draw_id' => $firstPending !== null ? (int) $firstPending->draw_id : null,
|
||||||
'first_pending_batch_id' => $firstPending !== null ? (int) $firstPending->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<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function drawPanel(Draw $draw): array
|
private function drawPanel(Draw $draw): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -291,7 +291,9 @@ final class AdminReportQueryService
|
|||||||
$payoutAgg = DB::table('ticket_items')
|
$payoutAgg = DB::table('ticket_items')
|
||||||
->selectRaw('COALESCE(SUM(win_amount), 0) as win_minor, COALESCE(SUM(jackpot_win_amount), 0) as jackpot_minor')
|
->selectRaw('COALESCE(SUM(win_amount), 0) as win_minor, COALESCE(SUM(jackpot_win_amount), 0) as jackpot_minor')
|
||||||
->first();
|
->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')
|
$activity = DB::table('draws as d')
|
||||||
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
|
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
|
||||||
@@ -312,8 +314,12 @@ final class AdminReportQueryService
|
|||||||
return [
|
return [
|
||||||
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
||||||
'total_bet_minor' => $totalBetMinor,
|
'total_bet_minor' => $totalBetMinor,
|
||||||
|
'total_win_minor' => $totalWinMinor,
|
||||||
|
'total_jackpot_minor' => $totalJackpotMinor,
|
||||||
'total_payout_minor' => $totalPayoutMinor,
|
'total_payout_minor' => $totalPayoutMinor,
|
||||||
'approx_house_gross_minor' => $totalBetMinor - $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,
|
'draw_count' => $drawCount,
|
||||||
'business_day_count' => $businessDayCount,
|
'business_day_count' => $businessDayCount,
|
||||||
'date_from' => $dateFrom,
|
'date_from' => $dateFrom,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Firebase\JWT\Key;
|
|||||||
use App\Models\Player;
|
use App\Models\Player;
|
||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Support\PlayerAutoRegistrationDefaults;
|
||||||
use App\Support\PlayerTokenAesUnwrap;
|
use App\Support\PlayerTokenAesUnwrap;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use App\Exceptions\PlayerAuthenticationException;
|
use App\Exceptions\PlayerAuthenticationException;
|
||||||
@@ -176,8 +177,7 @@ final class PlayerTokenResolver
|
|||||||
|
|
||||||
$now = now();
|
$now = now();
|
||||||
$defaults = [
|
$defaults = [
|
||||||
'username' => null,
|
...PlayerAutoRegistrationDefaults::profileFields(),
|
||||||
'nickname' => null,
|
|
||||||
'default_currency' => LotterySettings::defaultCurrency(),
|
'default_currency' => LotterySettings::defaultCurrency(),
|
||||||
'status' => self::PLAYER_STATUS_ACTIVE,
|
'status' => self::PLAYER_STATUS_ACTIVE,
|
||||||
'last_login_at' => $now,
|
'last_login_at' => $now,
|
||||||
|
|||||||
48
app/Support/PlayerAutoRegistrationDefaults.php
Normal file
48
app/Support/PlayerAutoRegistrationDefaults.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家 SSO 首登自动建档时的默认展示名(username / nickname)。
|
||||||
|
*/
|
||||||
|
final class PlayerAutoRegistrationDefaults
|
||||||
|
{
|
||||||
|
public const DISPLAY_NAME_PREFIX = 'nlotto';
|
||||||
|
|
||||||
|
private const RANDOM_SUFFIX_DIGITS = 6;
|
||||||
|
|
||||||
|
private const MAX_UNIQUE_ATTEMPTS = 10;
|
||||||
|
|
||||||
|
public static function displayName(): string
|
||||||
|
{
|
||||||
|
for ($attempt = 0; $attempt < self::MAX_UNIQUE_ATTEMPTS; $attempt++) {
|
||||||
|
$candidate = self::DISPLAY_NAME_PREFIX.self::randomSuffix();
|
||||||
|
|
||||||
|
if (! Player::query()->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,13 +98,20 @@ test('jwt first successful login auto-registers player mapping', function () {
|
|||||||
'exp' => $now + 300,
|
'exp' => $now + 300,
|
||||||
], 'jwt-test-secret', 'HS256');
|
], 'jwt-test-secret', 'HS256');
|
||||||
|
|
||||||
$this->withHeader('Authorization', 'Bearer '.$jwt)
|
$response = $this->withHeader('Authorization', 'Bearer '.$jwt)
|
||||||
->getJson('/api/v1/player/me')
|
->getJson('/api/v1/player/me')
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.site_player_id', 'brand-new-sso-1')
|
->assertJsonPath('data.site_player_id', 'brand-new-sso-1')
|
||||||
->assertJsonPath('data.default_currency', 'NPR');
|
->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 () {
|
test('player me rejects non-active status with 8005', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user